Browse Source

expand windows compatibility

djeada 1 month ago
parent
commit
a4e1d32a75

+ 59 - 6
.github/workflows/windows.yml

@@ -149,10 +149,14 @@ jobs:
             'setlocal'
             'setlocal'
             'cd /d "%~dp0"'
             'cd /d "%~dp0"'
             'set QT_DEBUG_PLUGINS=1'
             'set QT_DEBUG_PLUGINS=1'
-            'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true'
+            'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true'
+            'set QT_OPENGL=desktop'
             'set QT_QPA_PLATFORM=windows'
             'set QT_QPA_PLATFORM=windows'
+            'echo Starting with Desktop OpenGL...'
+            'echo Logging to runlog.txt'
             '"%~dp0standard_of_iron.exe" 1> "%~dp0runlog.txt" 2>&1'
             '"%~dp0standard_of_iron.exe" 1> "%~dp0runlog.txt" 2>&1'
             'echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"'
             'echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"'
+            'type "%~dp0runlog.txt"'
             'pause'
             'pause'
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug.cmd"
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug.cmd"
 
 
@@ -164,14 +168,37 @@ jobs:
             'setlocal'
             'setlocal'
             'cd /d "%~dp0"'
             'cd /d "%~dp0"'
             'set QT_DEBUG_PLUGINS=1'
             'set QT_DEBUG_PLUGINS=1'
-            'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.quick.*=true'
+            'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.quick.*=true;qt.rhi.*=true'
             'set QT_OPENGL=software'
             'set QT_OPENGL=software'
             'set QT_QPA_PLATFORM=windows'
             'set QT_QPA_PLATFORM=windows'
-            '"%~dp0standard_of_iron.exe" 1> "%~dp0runlog.txt" 2>&1'
-            'echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"'
+            'echo Starting with Software OpenGL...'
+            'echo Logging to runlog_software.txt'
+            '"%~dp0standard_of_iron.exe" 1> "%~dp0runlog_software.txt" 2>&1'
+            'echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_software.txt"'
+            'type "%~dp0runlog_software.txt"'
             'pause'
             'pause'
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug_softwaregl.cmd"
           ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug_softwaregl.cmd"
 
 
+      - name: Add run_debug_angle.cmd
+        shell: pwsh
+        run: |
+          @(
+            '@echo off'
+            'setlocal'
+            'cd /d "%~dp0"'
+            'set QT_DEBUG_PLUGINS=1'
+            'set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true'
+            'set QT_OPENGL=angle'
+            'set QT_ANGLE_PLATFORM=d3d11'
+            'set QT_QPA_PLATFORM=windows'
+            'echo Starting with ANGLE (OpenGL ES via D3D11)...'
+            'echo Logging to runlog_angle.txt'
+            '"%~dp0standard_of_iron.exe" 1> "%~dp0runlog_angle.txt" 2>&1'
+            'echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_angle.txt"'
+            'type "%~dp0runlog_angle.txt"'
+            'pause'
+          ) | Set-Content -Encoding ASCII "$env:APP_DIR\run_debug_angle.cmd"
+
       - name: Add GL/ANGLE fallbacks
       - name: Add GL/ANGLE fallbacks
         shell: pwsh
         shell: pwsh
         run: |
         run: |
@@ -318,13 +345,23 @@ jobs:
           $ErrorActionPreference = 'Stop'
           $ErrorActionPreference = 'Stop'
       
       
           $env:QT_DEBUG_PLUGINS  = "1"
           $env:QT_DEBUG_PLUGINS  = "1"
-          $env:QT_LOGGING_RULES  = "qt.*=true;qt.qml=true;qqml.*=true"
+          $env:QT_LOGGING_RULES  = "qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true"
           $env:QT_OPENGL         = "software"
           $env:QT_OPENGL         = "software"
           $env:QT_QPA_PLATFORM   = "windows"
           $env:QT_QPA_PLATFORM   = "windows"
       
       
+          Write-Host "=== Smoke Test Configuration ==="
+          Write-Host "QT_OPENGL: $env:QT_OPENGL"
+          Write-Host "QT_QPA_PLATFORM: $env:QT_QPA_PLATFORM"
+          Write-Host "================================"
+          
           Push-Location "$env:APP_DIR"
           Push-Location "$env:APP_DIR"
           try {
           try {
-            $process = Start-Process -FilePath ".\${{ env.APP_NAME }}.exe" -PassThru -NoNewWindow
+            $logFile = "smoke_test.log"
+            $process = Start-Process -FilePath ".\${{ env.APP_NAME }}.exe" `
+              -RedirectStandardOutput $logFile `
+              -RedirectStandardError "${logFile}.err" `
+              -PassThru -NoNewWindow
+            
             $timeoutSeconds = 5
             $timeoutSeconds = 5
             $exited = $false
             $exited = $false
             try {
             try {
@@ -338,11 +375,27 @@ jobs:
               Write-Host "Process is still running after $timeoutSeconds s — treating as a PASS for smoke."
               Write-Host "Process is still running after $timeoutSeconds s — treating as a PASS for smoke."
               $process.Kill()
               $process.Kill()
               $process.WaitForExit()
               $process.WaitForExit()
+              
+              # Capture logs for debugging
+              if (Test-Path $logFile) {
+                Write-Host "`n=== Application Output (first 50 lines) ==="
+                Get-Content $logFile -Head 50 | Write-Host
+              }
               exit 0
               exit 0
             }
             }
 
 
             $code = $process.ExitCode
             $code = $process.ExitCode
             Write-Host "Process exited early with code: $code"
             Write-Host "Process exited early with code: $code"
+            
+            # Show logs to help diagnose
+            if (Test-Path $logFile) {
+              Write-Host "`n=== Application Output ==="
+              Get-Content $logFile | Write-Host
+            }
+            if (Test-Path "${logFile}.err") {
+              Write-Host "`n=== Error Output ==="
+              Get-Content "${logFile}.err" | Write-Host
+            }
 
 
             $expectedOnHeadless = @(-1073741819, -1073741510)
             $expectedOnHeadless = @(-1073741819, -1073741510)
             $deploymentIssues = @(-1073741701, -1073741515)
             $deploymentIssues = @(-1073741701, -1073741515)

+ 7 - 0
CMakeLists.txt

@@ -224,6 +224,13 @@ target_link_libraries(standard_of_iron
         audio_system
         audio_system
 )
 )
 
 
+# Windows-specific OpenGL linking
+if(WIN32)
+    target_link_libraries(standard_of_iron PRIVATE opengl32)
+    # Ensure we have access to wglGetProcAddress and related functions
+    target_compile_definitions(standard_of_iron PRIVATE NOMINMAX)
+endif()
+
 if(QT_VERSION_MAJOR EQUAL 6)
 if(QT_VERSION_MAJOR EQUAL 6)
     target_link_libraries(standard_of_iron PRIVATE Qt6::QuickControls2)
     target_link_libraries(standard_of_iron PRIVATE Qt6::QuickControls2)
 else()
 else()

+ 9 - 1
game/map/world_bootstrap.cpp

@@ -10,7 +10,15 @@ auto WorldBootstrap::initialize(Render::GL::Renderer &renderer,
                                 QString *out_error) -> bool {
                                 QString *out_error) -> bool {
   if (!Render::GL::RenderBootstrap::initialize(renderer, camera)) {
   if (!Render::GL::RenderBootstrap::initialize(renderer, camera)) {
     if (out_error != nullptr) {
     if (out_error != nullptr) {
-      *out_error = "Failed to initialize OpenGL renderer";
+      *out_error = "Failed to initialize OpenGL renderer.\n\n"
+                   "This usually means:\n"
+                   "1. Running in software rendering mode (QT_QUICK_BACKEND=software)\n"
+                   "2. Graphics drivers don't support required OpenGL version\n"
+                   "3. Running in a VM with incomplete OpenGL support\n\n"
+                   "To fix:\n"
+                   "- For full 3D functionality, run without QT_QUICK_BACKEND set\n"
+                   "- Update graphics drivers\n"
+                   "- On VMs: Enable 3D acceleration in VM settings";
     }
     }
     return false;
     return false;
   }
   }

+ 243 - 2
main.cpp

@@ -5,6 +5,8 @@
 #include <QFile>
 #include <QFile>
 #include <QGuiApplication>
 #include <QGuiApplication>
 #include <QOpenGLContext>
 #include <QOpenGLContext>
+#include <QOpenGLFunctions>
+#include <QOffscreenSurface>
 #include <QQmlApplicationEngine>
 #include <QQmlApplicationEngine>
 #include <QQmlContext>
 #include <QQmlContext>
 #include <QQuickWindow>
 #include <QQuickWindow>
@@ -22,6 +24,13 @@
 #include <qsurfaceformat.h>
 #include <qsurfaceformat.h>
 #include <qurl.h>
 #include <qurl.h>
 
 
+#ifdef Q_OS_WIN
+#include <windows.h>
+#include <QProcess>
+#include <gl/gl.h>
+#pragma comment(lib, "opengl32.lib")
+#endif
+
 #include "app/core/game_engine.h"
 #include "app/core/game_engine.h"
 #include "app/core/language_manager.h"
 #include "app/core/language_manager.h"
 #include "ui/gl_view.h"
 #include "ui/gl_view.h"
@@ -31,62 +40,269 @@
 constexpr int k_depth_buffer_bits = 24;
 constexpr int k_depth_buffer_bits = 24;
 constexpr int k_stencil_buffer_bits = 8;
 constexpr int k_stencil_buffer_bits = 8;
 
 
+#ifdef Q_OS_WIN
+// Test OpenGL using native Win32 API (before any Qt initialization)
+// Returns true if OpenGL is available, false otherwise
+static bool testNativeOpenGL() {
+  WNDCLASSA wc = {};
+  wc.lpfnWndProc = DefWindowProcA;
+  wc.hInstance = GetModuleHandle(nullptr);
+  wc.lpszClassName = "OpenGLTest";
+  
+  if (!RegisterClassA(&wc)) {
+    return false;
+  }
+  
+  HWND hwnd = CreateWindowExA(0, "OpenGLTest", "", WS_OVERLAPPEDWINDOW,
+                               0, 0, 1, 1, nullptr, nullptr, wc.hInstance, nullptr);
+  if (!hwnd) {
+    UnregisterClassA("OpenGLTest", wc.hInstance);
+    return false;
+  }
+  
+  HDC hdc = GetDC(hwnd);
+  if (!hdc) {
+    DestroyWindow(hwnd);
+    UnregisterClassA("OpenGLTest", wc.hInstance);
+    return false;
+  }
+  
+  PIXELFORMATDESCRIPTOR pfd = {};
+  pfd.nSize = sizeof(pfd);
+  pfd.nVersion = 1;
+  pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
+  pfd.iPixelType = PFD_TYPE_RGBA;
+  pfd.cColorBits = 24;
+  pfd.cDepthBits = 24;
+  pfd.cStencilBits = 8;
+  pfd.iLayerType = PFD_MAIN_PLANE;
+  
+  int pixelFormat = ChoosePixelFormat(hdc, &pfd);
+  bool success = false;
+  
+  if (pixelFormat != 0 && SetPixelFormat(hdc, pixelFormat, &pfd)) {
+    HGLRC hglrc = wglCreateContext(hdc);
+    if (hglrc) {
+      if (wglMakeCurrent(hdc, hglrc)) {
+        // Successfully created OpenGL context
+        const char* vendor = (const char*)glGetString(GL_VENDOR);
+        const char* renderer = (const char*)glGetString(GL_RENDERER);
+        const char* version = (const char*)glGetString(GL_VERSION);
+        
+        if (vendor && renderer && version) {
+          fprintf(stderr, "[OpenGL Test] Native context created successfully\n");
+          fprintf(stderr, "[OpenGL Test] Vendor: %s\n", vendor);
+          fprintf(stderr, "[OpenGL Test] Renderer: %s\n", renderer);
+          fprintf(stderr, "[OpenGL Test] Version: %s\n", version);
+          success = true;
+        }
+        
+        wglMakeCurrent(nullptr, nullptr);
+      }
+      wglDeleteContext(hglrc);
+    }
+  }
+  
+  ReleaseDC(hwnd, hdc);
+  DestroyWindow(hwnd);
+  UnregisterClassA("OpenGLTest", wc.hInstance);
+  
+  return success;
+}
+
+// Windows crash handler to detect OpenGL failures and suggest fallback
+static bool g_opengl_crashed = false;
+static LONG WINAPI crashHandler(EXCEPTION_POINTERS* exceptionInfo) {
+  if (exceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
+    // Log crash
+    FILE* crash_log = fopen("opengl_crash.txt", "w");
+    if (crash_log) {
+      fprintf(crash_log, "OpenGL/Qt rendering crash detected (Access Violation)\n");
+      fprintf(crash_log, "Try running with: run_debug_softwaregl.cmd\n");
+      fprintf(crash_log, "Or set environment variable: QT_QUICK_BACKEND=software\n");
+      fclose(crash_log);
+    }
+    
+    qCritical() << "=== CRASH DETECTED ===";
+    qCritical() << "OpenGL rendering failed. This usually means:";
+    qCritical() << "1. Graphics drivers are outdated";
+    qCritical() << "2. Running in a VM with incomplete OpenGL support";
+    qCritical() << "3. GPU doesn't support required OpenGL version";
+    qCritical() << "";
+    qCritical() << "To fix: Run run_debug_softwaregl.cmd instead";
+    qCritical() << "Or set: set QT_QUICK_BACKEND=software";
+    
+    g_opengl_crashed = true;
+  }
+  return EXCEPTION_CONTINUE_SEARCH;
+}
+#endif
+
 auto main(int argc, char *argv[]) -> int {
 auto main(int argc, char *argv[]) -> int {
+#ifdef Q_OS_WIN
+  // Install crash handler to detect OpenGL failures
+  SetUnhandledExceptionFilter(crashHandler);
+  
+  // Test OpenGL BEFORE any Qt initialization (using native Win32 API)
+  fprintf(stderr, "[Pre-Init] Testing native OpenGL availability...\n");
+  bool opengl_available = testNativeOpenGL();
+  
+  if (!opengl_available) {
+    fprintf(stderr, "[Pre-Init] WARNING: OpenGL test failed!\n");
+    fprintf(stderr, "[Pre-Init] Forcing software rendering mode\n");
+    _putenv("QT_QUICK_BACKEND=software");
+    _putenv("QT_OPENGL=software");
+  } else {
+    fprintf(stderr, "[Pre-Init] OpenGL test passed\n");
+  }
+  
+  // Check if we should use software rendering
+  bool use_software = qEnvironmentVariableIsSet("QT_QUICK_BACKEND") &&
+                      qEnvironmentVariable("QT_QUICK_BACKEND") == "software";
+  
+  if (use_software) {
+    qInfo() << "=== SOFTWARE RENDERING MODE ===";
+    qInfo() << "Using Qt Quick Software renderer (CPU-based)";
+    qInfo() << "Performance will be limited but should work on all systems";
+  }
+#endif
+
+  // Setup message handler for debugging
+  qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &context, const QString &msg) {
+    QByteArray localMsg = msg.toLocal8Bit();
+    const char *file = context.file ? context.file : "";
+    const char *function = context.function ? context.function : "";
+    
+    FILE *out = stderr;
+    switch (type) {
+    case QtDebugMsg:
+      fprintf(out, "[DEBUG] %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+      break;
+    case QtInfoMsg:
+      fprintf(out, "[INFO] %s\n", localMsg.constData());
+      break;
+    case QtWarningMsg:
+      fprintf(out, "[WARNING] %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+      // Check for critical OpenGL warnings
+      if (msg.contains("OpenGL", Qt::CaseInsensitive) || 
+          msg.contains("scene graph", Qt::CaseInsensitive) ||
+          msg.contains("RHI", Qt::CaseInsensitive)) {
+        fprintf(out, "[HINT] If you see crashes, try software rendering: set QT_QUICK_BACKEND=software\n");
+      }
+      break;
+    case QtCriticalMsg:
+      fprintf(out, "[CRITICAL] %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+      fprintf(out, "[CRITICAL] Try running with software rendering if this persists\n");
+      break;
+    case QtFatalMsg:
+      fprintf(out, "[FATAL] %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
+      fprintf(out, "[FATAL] === RECOVERY SUGGESTION ===\n");
+      fprintf(out, "[FATAL] Run: run_debug_softwaregl.cmd\n");
+      fprintf(out, "[FATAL] Or set: QT_QUICK_BACKEND=software\n");
+      abort();
+    }
+    fflush(out);
+  });
+
+  qInfo() << "=== Standard of Iron - Starting ===";
+  qInfo() << "Qt version:" << QT_VERSION_STR;
+  
+  // Linux-specific: prefer X11 over Wayland for better OpenGL compatibility
+#ifndef Q_OS_WIN
   if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY") &&
   if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY") &&
       qEnvironmentVariableIsSet("DISPLAY")) {
       qEnvironmentVariableIsSet("DISPLAY")) {
     qputenv("QT_QPA_PLATFORM", "xcb");
     qputenv("QT_QPA_PLATFORM", "xcb");
+    qInfo() << "Linux: Using X11 (xcb) platform";
   }
   }
+#endif
+
+  qInfo() << "Setting OpenGL environment...";
   qputenv("QT_OPENGL", "desktop");
   qputenv("QT_OPENGL", "desktop");
   qputenv("QSG_RHI_BACKEND", "opengl");
   qputenv("QSG_RHI_BACKEND", "opengl");
 
 
 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+  qInfo() << "Setting graphics API to OpenGLRhi...";
   QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
   QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
 #endif
 #endif
 
 
+  qInfo() << "Configuring OpenGL surface format...";
   QSurfaceFormat fmt;
   QSurfaceFormat fmt;
   fmt.setVersion(3, 3);
   fmt.setVersion(3, 3);
   fmt.setProfile(QSurfaceFormat::CoreProfile);
   fmt.setProfile(QSurfaceFormat::CoreProfile);
   fmt.setDepthBufferSize(k_depth_buffer_bits);
   fmt.setDepthBufferSize(k_depth_buffer_bits);
   fmt.setStencilBufferSize(k_stencil_buffer_bits);
   fmt.setStencilBufferSize(k_stencil_buffer_bits);
   fmt.setSamples(0);
   fmt.setSamples(0);
+  
+#ifdef Q_OS_WIN
+  // Windows: Request compatibility profile for better driver support
+  // Some Windows drivers have issues with Core profile on older hardware
+  fmt.setProfile(QSurfaceFormat::CompatibilityProfile);
+  qInfo() << "Windows detected: Using OpenGL Compatibility Profile";
+#endif
+  
   QSurfaceFormat::setDefaultFormat(fmt);
   QSurfaceFormat::setDefaultFormat(fmt);
+  qInfo() << "Surface format configured: OpenGL" << fmt.majorVersion() << "." << fmt.minorVersion();
 
 
+  qInfo() << "Creating QGuiApplication...";
   QGuiApplication app(argc, argv);
   QGuiApplication app(argc, argv);
-
+  qInfo() << "QGuiApplication created successfully";
+  
+  qInfo() << "Creating LanguageManager...";
   auto *language_manager = new LanguageManager();
   auto *language_manager = new LanguageManager();
+  qInfo() << "LanguageManager created";
+
+  qInfo() << "Creating GameEngine...";
   auto *game_engine = new GameEngine();
   auto *game_engine = new GameEngine();
+  qInfo() << "GameEngine created";
 
 
+  qInfo() << "Setting up QML engine...";
   QQmlApplicationEngine engine;
   QQmlApplicationEngine engine;
+  qInfo() << "Adding context properties...";
   engine.rootContext()->setContextProperty("language_manager",
   engine.rootContext()->setContextProperty("language_manager",
                                            language_manager);
                                            language_manager);
   engine.rootContext()->setContextProperty("game", game_engine);
   engine.rootContext()->setContextProperty("game", game_engine);
+  qInfo() << "Adding import path...";
   engine.addImportPath("qrc:/StandardOfIron/ui/qml");
   engine.addImportPath("qrc:/StandardOfIron/ui/qml");
+  qInfo() << "Registering QML types...";
   qmlRegisterType<GLView>("StandardOfIron", 1, 0, "GLView");
   qmlRegisterType<GLView>("StandardOfIron", 1, 0, "GLView");
 
 
   // Register Theme singleton
   // Register Theme singleton
   qmlRegisterSingletonType<Theme>("StandardOfIron.UI", 1, 0, "Theme",
   qmlRegisterSingletonType<Theme>("StandardOfIron.UI", 1, 0, "Theme",
                                   &Theme::create);
                                   &Theme::create);
 
 
+  qInfo() << "Loading Main.qml...";
+  qInfo() << "Loading Main.qml...";
   engine.load(QUrl(QStringLiteral("qrc:/StandardOfIron/ui/qml/Main.qml")));
   engine.load(QUrl(QStringLiteral("qrc:/StandardOfIron/ui/qml/Main.qml")));
+  
+  qInfo() << "Checking if QML loaded...";
   if (engine.rootObjects().isEmpty()) {
   if (engine.rootObjects().isEmpty()) {
     qWarning() << "Failed to load QML file";
     qWarning() << "Failed to load QML file";
     return -1;
     return -1;
   }
   }
+  qInfo() << "QML loaded successfully, root objects count:" << engine.rootObjects().size();
 
 
+  qInfo() << "Finding QQuickWindow...";
   auto *root_obj = engine.rootObjects().first();
   auto *root_obj = engine.rootObjects().first();
   auto *window = qobject_cast<QQuickWindow *>(root_obj);
   auto *window = qobject_cast<QQuickWindow *>(root_obj);
   if (window == nullptr) {
   if (window == nullptr) {
+    qInfo() << "Root object is not a window, searching children...";
     window = root_obj->findChild<QQuickWindow *>();
     window = root_obj->findChild<QQuickWindow *>();
   }
   }
   if (window == nullptr) {
   if (window == nullptr) {
     qWarning() << "No QQuickWindow found for OpenGL initialization.";
     qWarning() << "No QQuickWindow found for OpenGL initialization.";
     return -2;
     return -2;
   }
   }
+  qInfo() << "QQuickWindow found";
 
 
+  qInfo() << "Setting window in GameEngine...";
   game_engine->setWindow(window);
   game_engine->setWindow(window);
+  qInfo() << "Window set successfully";
 
 
+  qInfo() << "Connecting scene graph signals...";
+  qInfo() << "Connecting scene graph signals...";
   QObject::connect(
   QObject::connect(
       window, &QQuickWindow::sceneGraphInitialized, window, [window]() {
       window, &QQuickWindow::sceneGraphInitialized, window, [window]() {
+        qInfo() << "Scene graph initialized!";
         if (auto *renderer_interface = window->rendererInterface()) {
         if (auto *renderer_interface = window->rendererInterface()) {
           const auto api = renderer_interface->graphicsApi();
           const auto api = renderer_interface->graphicsApi();
 
 
@@ -123,5 +339,30 @@ auto main(int argc, char *argv[]) -> int {
                      QGuiApplication::exit(3);
                      QGuiApplication::exit(3);
                    });
                    });
 
 
-  return QGuiApplication::exec();
+  qInfo() << "Starting event loop...";
+  
+  int result = QGuiApplication::exec();
+  
+#ifdef Q_OS_WIN
+  // Check if we crashed during OpenGL initialization
+  if (g_opengl_crashed) {
+    qCritical() << "";
+    qCritical() << "========================================";
+    qCritical() << "OPENGL CRASH RECOVERY";
+    qCritical() << "========================================";
+    qCritical() << "";
+    qCritical() << "The application crashed during OpenGL initialization.";
+    qCritical() << "This is a known issue with Qt + some Windows graphics drivers.";
+    qCritical() << "";
+    qCritical() << "SOLUTION: Set environment variable before running:";
+    qCritical() << "  set QT_QUICK_BACKEND=software";
+    qCritical() << "";
+    qCritical() << "Or use the provided launcher:";
+    qCritical() << "  run_debug_softwaregl.cmd";
+    qCritical() << "";
+    return -1;
+  }
+#endif
+  
+  return result;
 }
 }

+ 25 - 0
render/gl/backend.cpp

@@ -55,47 +55,71 @@ Backend::~Backend() {
 }
 }
 
 
 void Backend::initialize() {
 void Backend::initialize() {
+  qInfo() << "Backend::initialize() - Starting...";
+  
+  qInfo() << "Backend: Initializing OpenGL functions...";
   initializeOpenGLFunctions();
   initializeOpenGLFunctions();
+  qInfo() << "Backend: OpenGL functions initialized";
 
 
+  qInfo() << "Backend: Setting up depth test...";
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_DEPTH_TEST);
   glDepthFunc(GL_LESS);
   glDepthFunc(GL_LESS);
   glDepthRange(0.0, 1.0);
   glDepthRange(0.0, 1.0);
   glDepthMask(GL_TRUE);
   glDepthMask(GL_TRUE);
 
 
+  qInfo() << "Backend: Setting up blending...";
   glEnable(GL_BLEND);
   glEnable(GL_BLEND);
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
   glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 
 
+  qInfo() << "Backend: Creating ResourceManager...";
   m_resources = std::make_unique<ResourceManager>();
   m_resources = std::make_unique<ResourceManager>();
   if (!m_resources->initialize()) {
   if (!m_resources->initialize()) {
     qWarning() << "Backend: failed to initialize ResourceManager";
     qWarning() << "Backend: failed to initialize ResourceManager";
   }
   }
+  qInfo() << "Backend: ResourceManager created";
+  
+  qInfo() << "Backend: Creating ShaderCache...";
   m_shaderCache = std::make_unique<ShaderCache>();
   m_shaderCache = std::make_unique<ShaderCache>();
   m_shaderCache->initializeDefaults();
   m_shaderCache->initializeDefaults();
+  qInfo() << "Backend: ShaderCache created";
 
 
+  qInfo() << "Backend: Creating CylinderPipeline...";
   m_cylinderPipeline =
   m_cylinderPipeline =
       std::make_unique<BackendPipelines::CylinderPipeline>(m_shaderCache.get());
       std::make_unique<BackendPipelines::CylinderPipeline>(m_shaderCache.get());
   m_cylinderPipeline->initialize();
   m_cylinderPipeline->initialize();
+  qInfo() << "Backend: CylinderPipeline initialized";
 
 
+  qInfo() << "Backend: Creating VegetationPipeline...";
   m_vegetationPipeline = std::make_unique<BackendPipelines::VegetationPipeline>(
   m_vegetationPipeline = std::make_unique<BackendPipelines::VegetationPipeline>(
       m_shaderCache.get());
       m_shaderCache.get());
   m_vegetationPipeline->initialize();
   m_vegetationPipeline->initialize();
+  qInfo() << "Backend: VegetationPipeline initialized";
 
 
+  qInfo() << "Backend: Creating TerrainPipeline...";
   m_terrainPipeline = std::make_unique<BackendPipelines::TerrainPipeline>(
   m_terrainPipeline = std::make_unique<BackendPipelines::TerrainPipeline>(
       this, m_shaderCache.get());
       this, m_shaderCache.get());
   m_terrainPipeline->initialize();
   m_terrainPipeline->initialize();
+  qInfo() << "Backend: TerrainPipeline initialized";
 
 
+  qInfo() << "Backend: Creating CharacterPipeline...";
   m_characterPipeline = std::make_unique<BackendPipelines::CharacterPipeline>(
   m_characterPipeline = std::make_unique<BackendPipelines::CharacterPipeline>(
       this, m_shaderCache.get());
       this, m_shaderCache.get());
   m_characterPipeline->initialize();
   m_characterPipeline->initialize();
+  qInfo() << "Backend: CharacterPipeline initialized";
 
 
+  qInfo() << "Backend: Creating WaterPipeline...";
   m_waterPipeline = std::make_unique<BackendPipelines::WaterPipeline>(
   m_waterPipeline = std::make_unique<BackendPipelines::WaterPipeline>(
       this, m_shaderCache.get());
       this, m_shaderCache.get());
   m_waterPipeline->initialize();
   m_waterPipeline->initialize();
+  qInfo() << "Backend: WaterPipeline initialized";
 
 
+  qInfo() << "Backend: Creating EffectsPipeline...";
   m_effectsPipeline = std::make_unique<BackendPipelines::EffectsPipeline>(
   m_effectsPipeline = std::make_unique<BackendPipelines::EffectsPipeline>(
       this, m_shaderCache.get());
       this, m_shaderCache.get());
   m_effectsPipeline->initialize();
   m_effectsPipeline->initialize();
+  qInfo() << "Backend: EffectsPipeline initialized";
 
 
+  qInfo() << "Backend: Loading basic shaders...";
   m_basicShader = m_shaderCache->get(QStringLiteral("basic"));
   m_basicShader = m_shaderCache->get(QStringLiteral("basic"));
   m_gridShader = m_shaderCache->get(QStringLiteral("grid"));
   m_gridShader = m_shaderCache->get(QStringLiteral("grid"));
   if (m_basicShader == nullptr) {
   if (m_basicShader == nullptr) {
@@ -104,6 +128,7 @@ void Backend::initialize() {
   if (m_gridShader == nullptr) {
   if (m_gridShader == nullptr) {
     qWarning() << "Backend: grid shader missing";
     qWarning() << "Backend: grid shader missing";
   }
   }
+  qInfo() << "Backend::initialize() - Complete!";
 }
 }
 
 
 void Backend::beginFrame() {
 void Backend::beginFrame() {

+ 18 - 2
render/gl/bootstrap.cpp

@@ -1,6 +1,7 @@
 #include "bootstrap.h"
 #include "bootstrap.h"
 #include "../scene_renderer.h"
 #include "../scene_renderer.h"
 #include "camera.h"
 #include "camera.h"
+#include "gl_capabilities.h"
 #include <QDebug>
 #include <QDebug>
 #include <QOpenGLContext>
 #include <QOpenGLContext>
 #include <qglobal.h>
 #include <qglobal.h>
@@ -8,16 +9,31 @@
 namespace Render::GL {
 namespace Render::GL {
 
 
 auto RenderBootstrap::initialize(Renderer &renderer, Camera &camera) -> bool {
 auto RenderBootstrap::initialize(Renderer &renderer, Camera &camera) -> bool {
+  qInfo() << "RenderBootstrap::initialize() - Starting OpenGL initialization...";
+  
   QOpenGLContext *ctx = QOpenGLContext::currentContext();
   QOpenGLContext *ctx = QOpenGLContext::currentContext();
   if ((ctx == nullptr) || !ctx->isValid()) {
   if ((ctx == nullptr) || !ctx->isValid()) {
-    qWarning() << "RenderBootstrap: no current valid OpenGL context";
+    qCritical() << "RenderBootstrap: no current valid OpenGL context";
     return false;
     return false;
   }
   }
+  qInfo() << "RenderBootstrap: OpenGL context is valid";
+  
+  // Log OpenGL capabilities for debugging (especially useful on Windows)
+  qInfo() << "RenderBootstrap: Logging OpenGL capabilities...";
+  GLCapabilities::logCapabilities();
+  qInfo() << "RenderBootstrap: Capabilities logged";
+  
+  qInfo() << "RenderBootstrap: Calling renderer.initialize()...";
   if (!renderer.initialize()) {
   if (!renderer.initialize()) {
-    qWarning() << "RenderBootstrap: renderer initialize failed";
+    qCritical() << "RenderBootstrap: renderer initialize failed";
     return false;
     return false;
   }
   }
+  qInfo() << "RenderBootstrap: Renderer initialized successfully";
+  
+  qInfo() << "RenderBootstrap: Setting camera...";
   renderer.setCamera(&camera);
   renderer.setCamera(&camera);
+  qInfo() << "RenderBootstrap: Camera set, initialization complete";
+  
   return true;
   return true;
 }
 }
 
 

+ 77 - 0
render/gl/gl_capabilities.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#include <QDebug>
+#include <QOpenGLContext>
+#include <QOpenGLExtraFunctions>
+#include <QString>
+
+namespace Render::GL {
+
+// Helper class to detect and report OpenGL capabilities at runtime
+class GLCapabilities {
+public:
+  static void logCapabilities() {
+    auto *ctx = QOpenGLContext::currentContext();
+    if (ctx == nullptr) {
+      qWarning() << "GLCapabilities: No current OpenGL context";
+      return;
+    }
+
+    auto *gl = ctx->extraFunctions();
+    const auto format = ctx->format();
+
+    qInfo() << "=== OpenGL Context Information ===";
+    qInfo() << "Vendor:" << reinterpret_cast<const char*>(gl->glGetString(GL_VENDOR));
+    qInfo() << "Renderer:" << reinterpret_cast<const char*>(gl->glGetString(GL_RENDERER));
+    qInfo() << "Version:" << reinterpret_cast<const char*>(gl->glGetString(GL_VERSION));
+    qInfo() << "GLSL Version:" << reinterpret_cast<const char*>(gl->glGetString(GL_SHADING_LANGUAGE_VERSION));
+    qInfo() << "Context Version:" << format.majorVersion() << "." << format.minorVersion();
+    qInfo() << "Profile:" << (format.profile() == QSurfaceFormat::CoreProfile ? "Core" : 
+                              format.profile() == QSurfaceFormat::CompatibilityProfile ? "Compatibility" : 
+                              "NoProfile");
+
+#ifdef Q_OS_WIN
+    qInfo() << "Platform: Windows";
+#elif defined(Q_OS_LINUX)
+    qInfo() << "Platform: Linux";
+#elif defined(Q_OS_MAC)
+    qInfo() << "Platform: macOS";
+#else
+    qInfo() << "Platform: Unknown";
+#endif
+
+    // Check for important extensions
+    const QString extensions = QString::fromLatin1(
+        reinterpret_cast<const char*>(gl->glGetString(GL_EXTENSIONS)));
+    
+    qInfo() << "=== Extension Support ===";
+    qInfo() << "GL_ARB_buffer_storage:" << extensions.contains("GL_ARB_buffer_storage");
+    qInfo() << "GL_ARB_direct_state_access:" << extensions.contains("GL_ARB_direct_state_access");
+    qInfo() << "GL_ARB_vertex_array_object:" << extensions.contains("GL_ARB_vertex_array_object");
+    qInfo() << "GL_ARB_uniform_buffer_object:" << extensions.contains("GL_ARB_uniform_buffer_object");
+    
+    // Check for persistent mapping support
+    const bool hasPersistentMapping = 
+        (format.majorVersion() > 4 || (format.majorVersion() == 4 && format.minorVersion() >= 4)) ||
+        extensions.contains("GL_ARB_buffer_storage");
+    
+    qInfo() << "Persistent Buffer Mapping:" << (hasPersistentMapping ? "Supported" : "Not Supported");
+    
+    qInfo() << "==================================";
+  }
+
+  static auto isExtensionSupported(const char *extension) -> bool {
+    auto *ctx = QOpenGLContext::currentContext();
+    if (ctx == nullptr) {
+      return false;
+    }
+
+    auto *gl = ctx->extraFunctions();
+    const QString extensions = QString::fromLatin1(
+        reinterpret_cast<const char*>(gl->glGetString(GL_EXTENSIONS)));
+    
+    return extensions.contains(extension);
+  }
+};
+
+} // namespace Render::GL

+ 51 - 27
render/gl/persistent_buffer.h

@@ -1,5 +1,6 @@
 #pragma once
 #pragma once
 
 
+#include "platform_gl.h"
 #include "render_constants.h"
 #include "render_constants.h"
 #include <QDebug>
 #include <QDebug>
 #include <QOpenGLContext>
 #include <QOpenGLContext>
@@ -41,9 +42,6 @@ public:
     glGenBuffers(1, &m_buffer);
     glGenBuffers(1, &m_buffer);
     glBindBuffer(GL_ARRAY_BUFFER, m_buffer);
     glBindBuffer(GL_ARRAY_BUFFER, m_buffer);
 
 
-    const GLbitfield storageFlags = 0x0100;
-    const GLbitfield mapFlags = 0x0002 | 0x0040 | 0x0080;
-
     QOpenGLContext *ctx = QOpenGLContext::currentContext();
     QOpenGLContext *ctx = QOpenGLContext::currentContext();
     if (ctx == nullptr) {
     if (ctx == nullptr) {
       qWarning() << "PersistentRingBuffer: No current OpenGL context";
       qWarning() << "PersistentRingBuffer: No current OpenGL context";
@@ -53,41 +51,36 @@ public:
       return false;
       return false;
     }
     }
 
 
-    typedef void(QOPENGLF_APIENTRYP type_glBufferStorage)(
-        GLenum target, GLsizeiptr size, const void *data, GLbitfield flags);
-    auto glBufferStorage = reinterpret_cast<type_glBufferStorage>(
-        ctx->getProcAddress("glBufferStorage"));
-
-    if (glBufferStorage == nullptr) {
-      glBindBuffer(GL_ARRAY_BUFFER, 0);
-      glDeleteBuffers(1, &m_buffer);
-      m_buffer = 0;
-      return false;
-    }
-
-    glBufferStorage(GL_ARRAY_BUFFER, m_totalSize, nullptr,
-                    storageFlags | mapFlags);
-
-    GLenum const err = glGetError();
-    if (err != GL_NO_ERROR) {
-      qWarning() << "PersistentRingBuffer: glBufferStorage failed with error:"
-                 << err;
+    // Try to create buffer using platform-aware helper with fallback
+    Platform::BufferStorageHelper::Mode mode;
+    if (!Platform::BufferStorageHelper::createBuffer(m_buffer, m_totalSize, &mode)) {
+      qWarning() << "PersistentRingBuffer: Failed to create buffer storage";
       glBindBuffer(GL_ARRAY_BUFFER, 0);
       glBindBuffer(GL_ARRAY_BUFFER, 0);
       glDeleteBuffers(1, &m_buffer);
       glDeleteBuffers(1, &m_buffer);
       m_buffer = 0;
       m_buffer = 0;
       return false;
       return false;
     }
     }
 
 
-    m_mappedPtr = glMapBufferRange(GL_ARRAY_BUFFER, 0, m_totalSize, mapFlags);
+    m_bufferMode = mode;
 
 
-    glBindBuffer(GL_ARRAY_BUFFER, 0);
+    // Map the buffer with appropriate mode
+    m_mappedPtr = Platform::BufferStorageHelper::mapBuffer(m_totalSize, mode);
 
 
     if (m_mappedPtr == nullptr) {
     if (m_mappedPtr == nullptr) {
-      qWarning() << "PersistentRingBuffer: glMapBufferRange failed";
+      qWarning() << "PersistentRingBuffer: Failed to map buffer";
+      glBindBuffer(GL_ARRAY_BUFFER, 0);
       destroy();
       destroy();
       return false;
       return false;
     }
     }
 
 
+    // For fallback mode, we need to unmap after each write
+    if (mode == Platform::BufferStorageHelper::Mode::Fallback) {
+      qInfo() << "PersistentRingBuffer: Running in fallback mode (non-persistent mapping)";
+      glUnmapBuffer(GL_ARRAY_BUFFER);
+      m_mappedPtr = nullptr;  // Will be remapped on each write
+    }
+
+    glBindBuffer(GL_ARRAY_BUFFER, 0);
     return true;
     return true;
   }
   }
 
 
@@ -118,7 +111,35 @@ public:
   }
   }
 
 
   auto write(const T *data, std::size_t count) -> std::size_t {
   auto write(const T *data, std::size_t count) -> std::size_t {
-    if ((m_mappedPtr == nullptr) || count == 0 || count > m_capacity) {
+    if (count == 0 || count > m_capacity || m_buffer == 0) {
+      return 0;
+    }
+
+    // For fallback mode, we need to map/unmap on each write
+    if (m_bufferMode == Platform::BufferStorageHelper::Mode::Fallback) {
+      glBindBuffer(GL_ARRAY_BUFFER, m_buffer);
+      
+      std::size_t const writeOffset = m_frameOffset + m_currentCount * sizeof(T);
+      void *ptr = glMapBufferRange(GL_ARRAY_BUFFER, writeOffset, count * sizeof(T), 
+                                    GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
+      
+      if (ptr == nullptr) {
+        qWarning() << "PersistentRingBuffer: Failed to map buffer for write";
+        glBindBuffer(GL_ARRAY_BUFFER, 0);
+        return 0;
+      }
+      
+      std::memcpy(ptr, data, count * sizeof(T));
+      glUnmapBuffer(GL_ARRAY_BUFFER);
+      glBindBuffer(GL_ARRAY_BUFFER, 0);
+      
+      std::size_t const elementOffset = m_currentCount;
+      m_currentCount += count;
+      return elementOffset;
+    }
+
+    // Persistent mode: direct write to mapped memory
+    if (m_mappedPtr == nullptr) {
       return 0;
       return 0;
     }
     }
 
 
@@ -143,7 +164,9 @@ public:
   [[nodiscard]] auto count() const -> std::size_t { return m_currentCount; }
   [[nodiscard]] auto count() const -> std::size_t { return m_currentCount; }
 
 
   [[nodiscard]] auto isValid() const -> bool {
   [[nodiscard]] auto isValid() const -> bool {
-    return m_buffer != 0 && m_mappedPtr != nullptr;
+    return m_buffer != 0 && 
+           (m_bufferMode == Platform::BufferStorageHelper::Mode::Fallback || 
+            m_mappedPtr != nullptr);
   }
   }
 
 
 private:
 private:
@@ -155,6 +178,7 @@ private:
   std::size_t m_currentCount = 0;
   std::size_t m_currentCount = 0;
   int m_buffersInFlight = BufferCapacity::BuffersInFlight;
   int m_buffersInFlight = BufferCapacity::BuffersInFlight;
   int m_currentFrame = 0;
   int m_currentFrame = 0;
+  Platform::BufferStorageHelper::Mode m_bufferMode = Platform::BufferStorageHelper::Mode::Persistent;
 };
 };
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 135 - 0
render/gl/platform_gl.h

@@ -0,0 +1,135 @@
+#pragma once
+
+#include <QOpenGLContext>
+#include <QOpenGLExtraFunctions>
+#include <QDebug>
+
+// Platform-specific OpenGL helpers for Windows/Linux compatibility
+namespace Render::GL::Platform {
+
+// Check if persistent mapped buffers are supported (ARB_buffer_storage)
+inline auto supportsPersistentMapping() -> bool {
+  auto *ctx = QOpenGLContext::currentContext();
+  if (ctx == nullptr) {
+    return false;
+  }
+
+  // Check for GL 4.4+ or ARB_buffer_storage extension
+  const auto glVersion = ctx->format().version();
+  const int majorVersion = glVersion.first;
+  const int minorVersion = glVersion.second;
+  
+  if (majorVersion > 4 || (majorVersion == 4 && minorVersion >= 4)) {
+    return true;
+  }
+
+  // Check for extension on older OpenGL versions
+  const auto extensions = QString::fromLatin1(
+      reinterpret_cast<const char*>(ctx->extraFunctions()->glGetString(GL_EXTENSIONS)));
+  return extensions.contains("GL_ARB_buffer_storage");
+}
+
+// Platform-agnostic way to get glBufferStorage function pointer
+inline auto getBufferStorageFunction() -> void* {
+  auto *ctx = QOpenGLContext::currentContext();
+  if (ctx == nullptr) {
+    return nullptr;
+  }
+
+  // Try core function first (GL 4.4+)
+  void *func = reinterpret_cast<void*>(ctx->getProcAddress("glBufferStorage"));
+  
+  // Fallback to ARB extension on older drivers
+  if (func == nullptr) {
+    func = reinterpret_cast<void*>(ctx->getProcAddress("glBufferStorageARB"));
+  }
+
+#ifdef Q_OS_WIN
+  // Windows may need explicit wglGetProcAddress fallback
+  if (func == nullptr) {
+    qWarning() << "Platform::getBufferStorageFunction: glBufferStorage not available on Windows";
+  }
+#endif
+
+  return func;
+}
+
+// GL constants for persistent mapping (may not be defined on all platforms)
+#ifndef GL_MAP_PERSISTENT_BIT
+#define GL_MAP_PERSISTENT_BIT 0x0040
+#endif
+
+#ifndef GL_MAP_COHERENT_BIT
+#define GL_MAP_COHERENT_BIT 0x0080
+#endif
+
+#ifndef GL_MAP_WRITE_BIT
+#define GL_MAP_WRITE_BIT 0x0002
+#endif
+
+#ifndef GL_DYNAMIC_STORAGE_BIT
+#define GL_DYNAMIC_STORAGE_BIT 0x0100
+#endif
+
+// Wrapper for buffer storage creation with fallback
+class BufferStorageHelper {
+public:
+  enum class Mode {
+    Persistent,  // Use persistent mapping (GL 4.4+)
+    Fallback     // Use traditional glBufferData + map/unmap
+  };
+
+  static auto createBuffer(GLuint buffer, GLsizeiptr size, Mode *outMode) -> bool {
+    if (supportsPersistentMapping()) {
+      typedef void(QOPENGLF_APIENTRYP type_glBufferStorage)(
+          GLenum target, GLsizeiptr size, const void *data, GLbitfield flags);
+      
+      auto glBufferStorage = reinterpret_cast<type_glBufferStorage>(
+          getBufferStorageFunction());
+
+      if (glBufferStorage != nullptr) {
+        const GLbitfield storageFlags = GL_DYNAMIC_STORAGE_BIT;
+        const GLbitfield mapFlags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
+        
+        glBufferStorage(GL_ARRAY_BUFFER, size, nullptr, storageFlags | mapFlags);
+        
+        GLenum err = QOpenGLContext::currentContext()->extraFunctions()->glGetError();
+        if (err == GL_NO_ERROR) {
+          if (outMode != nullptr) {
+            *outMode = Mode::Persistent;
+          }
+          return true;
+        }
+        qWarning() << "BufferStorageHelper: glBufferStorage failed with error:" << err;
+      }
+    }
+
+    // Fallback to traditional buffer allocation
+    qInfo() << "BufferStorageHelper: Using fallback buffer mode (glBufferData)";
+    QOpenGLContext::currentContext()->extraFunctions()->glBufferData(
+        GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);
+    
+    if (outMode != nullptr) {
+      *outMode = Mode::Fallback;
+    }
+    return true;
+  }
+
+  static auto mapBuffer(GLsizeiptr size, Mode mode) -> void* {
+    auto *gl = QOpenGLContext::currentContext()->extraFunctions();
+    
+    if (mode == Mode::Persistent) {
+      const GLbitfield mapFlags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
+      void *ptr = gl->glMapBufferRange(GL_ARRAY_BUFFER, 0, size, mapFlags);
+      if (ptr != nullptr) {
+        return ptr;
+      }
+      qWarning() << "BufferStorageHelper: Persistent mapping failed, falling back";
+    }
+
+    // Fallback mapping (non-persistent)
+    return gl->glMapBufferRange(GL_ARRAY_BUFFER, 0, size, GL_MAP_WRITE_BIT);
+  }
+};
+
+} // namespace Render::GL::Platform

+ 606 - 35
scripts/build-windows.py

@@ -5,27 +5,47 @@ Verifies dependencies, sets up MSVC environment, and builds the project.
 
 
 This script automates the Windows build process by:
 This script automates the Windows build process by:
 1. Checking for required tools (CMake, Ninja, MSVC, Qt)
 1. Checking for required tools (CMake, Ninja, MSVC, Qt)
-2. Guiding installation of missing dependencies
-3. Configuring and building the project with proper MSVC setup
-4. Deploying Qt dependencies with runtime libraries
-5. Writing qt.conf and diagnostic scripts (run_debug.cmd, run_debug_softwaregl.cmd)
-6. Copying GL/ANGLE fallback DLLs for graphics compatibility
-7. Copying assets and creating a distributable package
+2. Auto-installing missing dependencies (optional, uses winget)
+3. Guiding installation of missing dependencies
+4. Configuring and building the project with proper MSVC setup
+5. Deploying Qt dependencies with runtime libraries
+6. Writing qt.conf and diagnostic scripts (run_debug.cmd, etc.)
+7. Copying GL/ANGLE fallback DLLs for graphics compatibility
+8. Copying assets and creating a distributable package
 
 
 Usage:
 Usage:
-    python scripts/build-windows.py                    # Full build with checks
-    python scripts/build-windows.py --skip-checks      # Skip dependency checks
+    python scripts/build-windows.py                    # Auto-installs missing deps, then builds
+    python scripts/build-windows.py --no-auto-install  # Show manual install instructions instead
+    python scripts/build-windows.py --skip-checks      # Skip dependency checks entirely
     python scripts/build-windows.py --build-type Debug # Build in Debug mode
     python scripts/build-windows.py --build-type Debug # Build in Debug mode
     python scripts/build-windows.py --clean            # Clean build directory first
     python scripts/build-windows.py --clean            # Clean build directory first
     python scripts/build-windows.py --deploy-only      # Only deploy Qt (assumes built)
     python scripts/build-windows.py --deploy-only      # Only deploy Qt (assumes built)
+    python scripts/build-windows.py --no-package       # Skip ZIP creation (faster for dev)
     python scripts/build-windows.py --help             # Show this help
     python scripts/build-windows.py --help             # Show this help
 
 
+Behavior:
+    By default, the script will automatically install missing dependencies using winget.
+    Use --no-auto-install to disable this and see manual installation instructions instead.
+    
+Performance Tips for VMs:
+    set CMAKE_BUILD_PARALLEL_LEVEL=2    # Limit parallel jobs to avoid thrashing
+    set PYTHONUNBUFFERED=1              # Show output immediately (no buffering)
+    set NINJA_STATUS=[%%f/%%t] %%e sec  # Show Ninja build progress
+    
+    Use --no-package during development to skip slow ZIP creation
+
 Requirements:
 Requirements:
     - Python 3.7+
     - Python 3.7+
-    - CMake 3.21+
-    - Ninja build system
-    - Visual Studio 2019/2022 with C++ tools
-    - Qt 6.6.3 or compatible (with msvc2019_64 or msvc2022_64)
+    - CMake 3.21+ (auto-installed if missing)
+    - Ninja build system (auto-installed if missing)
+    - Visual Studio 2019/2022 with C++ tools (auto-installed if missing)
+    - Qt 6.6.3 or compatible (must be installed manually from qt.io)
+    
+Note:
+    - Auto-install requires Windows 10/11 with winget
+    - Qt cannot be auto-installed (winget Qt packages lack required components)
+    - Run as Administrator if auto-install fails with permission errors
+    - Long operations (compile, windeployqt, ZIP) show warnings but no progress bars
 """
 """
 
 
 import argparse
 import argparse
@@ -53,7 +73,7 @@ def info(msg: str) -> None:
 
 
 def success(msg: str) -> None:
 def success(msg: str) -> None:
     """Print success message."""
     """Print success message."""
-    print(f"{Color.GREEN}[]{Color.RESET} {msg}")
+    print(f"{Color.GREEN}[+]{Color.RESET} {msg}")
 
 
 def warning(msg: str) -> None:
 def warning(msg: str) -> None:
     """Print warning message."""
     """Print warning message."""
@@ -61,7 +81,7 @@ def warning(msg: str) -> None:
 
 
 def error(msg: str) -> None:
 def error(msg: str) -> None:
     """Print error message."""
     """Print error message."""
-    print(f"{Color.RED}[]{Color.RESET} {msg}")
+    print(f"{Color.RED}[x]{Color.RESET} {msg}")
 
 
 def run_command(cmd: list, capture_output: bool = False, check: bool = True, 
 def run_command(cmd: list, capture_output: bool = False, check: bool = True, 
                 env: Optional[dict] = None) -> subprocess.CompletedProcess:
                 env: Optional[dict] = None) -> subprocess.CompletedProcess:
@@ -92,6 +112,12 @@ def check_windows() -> None:
         error(f"Detected OS: {platform.system()}")
         error(f"Detected OS: {platform.system()}")
         sys.exit(1)
         sys.exit(1)
     success("Running on Windows")
     success("Running on Windows")
+    
+    # Check for unbuffered output
+    if not os.environ.get('PYTHONUNBUFFERED'):
+        warning("Tip: Set PYTHONUNBUFFERED=1 for immediate output visibility")
+        warning("     Run: set PYTHONUNBUFFERED=1 (before running this script)")
+        print()
 
 
 def check_cmake() -> Tuple[bool, Optional[str]]:
 def check_cmake() -> Tuple[bool, Optional[str]]:
     """Check if CMake is installed and get version."""
     """Check if CMake is installed and get version."""
@@ -156,34 +182,137 @@ def find_qt() -> Optional[Path]:
     # Common Qt installation locations
     # Common Qt installation locations
     possible_paths = [
     possible_paths = [
         Path(r"C:\Qt"),
         Path(r"C:\Qt"),
+        Path(r"C:\Program Files\Qt"),
+        Path(r"C:\Program Files (x86)\Qt"),
         Path.home() / "Qt",
         Path.home() / "Qt",
         Path(os.environ.get('QT_ROOT', '')) if os.environ.get('QT_ROOT') else None,
         Path(os.environ.get('QT_ROOT', '')) if os.environ.get('QT_ROOT') else None,
     ]
     ]
-    
+
+    # 1) Check PATH for qmake.exe first (useful if user added Qt to PATH)
+    qmake_in_path = shutil.which('qmake') or shutil.which('qmake.exe')
+    if qmake_in_path:
+        try:
+            qmake_path = Path(qmake_in_path)
+            # qmake lives in <qtdir>/bin/qmake.exe - return the arch dir
+            arch_dir = qmake_path.parent.parent
+            # Check if it's MSVC variant (required for this project)
+            if 'msvc' in arch_dir.name.lower():
+                return arch_dir
+            # If it's MinGW, we'll report it later but keep searching
+        except Exception:
+            pass
+
+    # 2) Search common installation roots for MSVC builds
     for base_path in possible_paths:
     for base_path in possible_paths:
-        if base_path and base_path.exists():
+        if not base_path:
+            continue
+        if base_path.exists():
             # Look for Qt 6.x with msvc2019_64 or msvc2022_64
             # Look for Qt 6.x with msvc2019_64 or msvc2022_64
-            for qt_version_dir in base_path.glob("6.*"):
+            for qt_version_dir in sorted(base_path.glob("6.*"), reverse=True):
                 for arch_dir in qt_version_dir.glob("msvc*_64"):
                 for arch_dir in qt_version_dir.glob("msvc*_64"):
                     qmake = arch_dir / "bin" / "qmake.exe"
                     qmake = arch_dir / "bin" / "qmake.exe"
                     if qmake.exists():
                     if qmake.exists():
                         return arch_dir
                         return arch_dir
+
+    # 3) Try to read QT_HOME from common environment variables
+    env_qt = os.environ.get('QT_HOME') or os.environ.get('QT_INSTALL_DIR')
+    if env_qt:
+        env_path = Path(env_qt)
+        if env_path.exists():
+            for qt_version_dir in sorted(env_path.glob("6.*"), reverse=True):
+                for arch_dir in qt_version_dir.glob("msvc*_64"):
+                    qmake = arch_dir / "bin" / "qmake.exe"
+                    if qmake.exists():
+                        return arch_dir
+
+    return None
+
+
+def find_qt_mingw() -> Optional[Path]:
+    """Check if MinGW Qt is installed (to warn user)."""
+    possible_paths = [
+        Path(r"C:\Qt"),
+        Path.home() / "Qt",
+    ]
     
     
+    for base_path in possible_paths:
+        if base_path.exists():
+            for qt_version_dir in sorted(base_path.glob("6.*"), reverse=True):
+                for arch_dir in qt_version_dir.glob("mingw*"):
+                    qmake = arch_dir / "bin" / "qmake.exe"
+                    if qmake.exists():
+                        return arch_dir
     return None
     return None
 
 
 def check_qt() -> Tuple[bool, Optional[Path], Optional[str]]:
 def check_qt() -> Tuple[bool, Optional[Path], Optional[str]]:
     """Check if Qt is installed."""
     """Check if Qt is installed."""
     qt_path = find_qt()
     qt_path = find_qt()
+    # If we found a qmake path via PATH, verify by running it
     if qt_path:
     if qt_path:
         qmake = qt_path / "bin" / "qmake.exe"
         qmake = qt_path / "bin" / "qmake.exe"
+        if qmake.exists():
+            try:
+                result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
+                version = result.stdout.strip()
+                success(f"Qt {version} (MSVC) found at {qt_path}")
+                return True, qt_path, version
+            except (subprocess.CalledProcessError, FileNotFoundError):
+                pass
+
+    # Check if MinGW Qt is installed instead
+    mingw_qt = find_qt_mingw()
+    if mingw_qt:
+        qmake = mingw_qt / "bin" / "qmake.exe"
         try:
         try:
             result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
             result = run_command([str(qmake), '-query', 'QT_VERSION'], capture_output=True)
             version = result.stdout.strip()
             version = result.stdout.strip()
-            success(f"Qt {version} found at {qt_path}")
-            return True, qt_path, version
-        except (subprocess.CalledProcessError, FileNotFoundError):
+            error(f"Qt {version} (MinGW) found at {mingw_qt}")
+            error("This project requires Qt with MSVC compiler, not MinGW")
+            print()
+            info(f"{Color.BOLD}Solution:{Color.RESET}")
+            info("Run the Qt Maintenance Tool to add MSVC component:")
+            print()
+            maintenance_tool = mingw_qt.parent.parent / "MaintenanceTool.exe"
+            if maintenance_tool.exists():
+                info(f"1. Run: {maintenance_tool}")
+            else:
+                info(f"1. Find Qt Maintenance Tool in: {mingw_qt.parent.parent}")
+            info("2. Click 'Add or remove components'")
+            info(f"3. Expand 'Qt {version.split('.')[0]}.{version.split('.')[1]}'")
+            info("4. CHECK: 'MSVC 2019 64-bit' or 'MSVC 2022 64-bit'")
+            info("5. CHECK: 'Qt 5 Compatibility Module'")
+            info("6. CHECK: 'Qt Multimedia'")
+            info("7. Click 'Update' and wait for installation")
+            info("8. Restart terminal and run this script again")
+            print()
+        except Exception:
             pass
             pass
-    
+
+    # Last resort: try running 'qmake -v' if on PATH
+    qmake_exec = shutil.which('qmake') or shutil.which('qmake.exe')
+    if qmake_exec and not mingw_qt:
+        try:
+            result = run_command([qmake_exec, '-v'], capture_output=True)
+            # Check if it's MinGW
+            if 'mingw' in result.stdout.lower():
+                error("Qt found but it's MinGW build - MSVC build required")
+                return False, None, None
+            
+            # Try to query QT_VERSION properly
+            try:
+                out = run_command([qmake_exec, '-query', 'QT_VERSION'], capture_output=True)
+                version = out.stdout.strip()
+            except Exception:
+                version = "unknown"
+            
+            success(f"Qt {version} found (qmake on PATH: {qmake_exec})")
+            # Try to compute arch dir
+            qmake_path = Path(qmake_exec)
+            arch_dir = qmake_path.parent.parent
+            return True, arch_dir, version
+        except Exception:
+            pass
+
     return False, None, None
     return False, None, None
 
 
 def setup_msvc_environment() -> dict:
 def setup_msvc_environment() -> dict:
@@ -260,6 +389,327 @@ def print_installation_guide() -> None:
     print("   Required modules: qt5compat, qtmultimedia")
     print("   Required modules: qt5compat, qtmultimedia")
     print("   Or set QT_ROOT environment variable to your Qt installation\n")
     print("   Or set QT_ROOT environment variable to your Qt installation\n")
 
 
+def check_winget_available() -> bool:
+    """Check if winget is available."""
+    try:
+        result = run_command(['winget', '--version'], capture_output=True, check=False)
+        return result.returncode == 0
+    except FileNotFoundError:
+        return False
+
+def install_visual_studio_direct() -> bool:
+    """Download and install Visual Studio using the bootstrapper directly."""
+    import tempfile
+    import urllib.request
+    
+    info("Attempting direct Visual Studio installation...")
+    info("This will download ~3MB bootstrapper, then download the full installer")
+    print()
+    
+    # Download the bootstrapper
+    vs_url = "https://aka.ms/vs/17/release/vs_community.exe"
+    
+    try:
+        with tempfile.TemporaryDirectory() as tmpdir:
+            installer_path = os.path.join(tmpdir, "vs_community.exe")
+            
+            info("Downloading Visual Studio installer...")
+            urllib.request.urlretrieve(vs_url, installer_path)
+            success("Installer downloaded")
+            
+            info("Running Visual Studio installer...")
+            info("This may take 10-30 minutes depending on your internet connection")
+            print()
+            
+            # Run the installer with the C++ workload
+            cmd = [
+                installer_path,
+                "--add", "Microsoft.VisualStudio.Workload.NativeDesktop",
+                "--includeRecommended",
+                "--passive",  # Show progress but don't require interaction
+                "--norestart",
+                "--wait"
+            ]
+            
+            result = subprocess.run(cmd, check=False)
+            
+            if result.returncode == 0 or result.returncode == 3010:  # 3010 = success but reboot required
+                success("Visual Studio installed successfully!")
+                if result.returncode == 3010:
+                    warning("A restart may be required to complete the installation")
+                return True
+            else:
+                error(f"Visual Studio installation failed (exit code: {result.returncode})")
+                return False
+                
+    except Exception as e:
+        error(f"Failed to download/install Visual Studio: {e}")
+        return False
+
+
+def install_qt_interactive() -> bool:
+    """Download and launch Qt online installer with instructions."""
+    import tempfile
+    import urllib.request
+    
+    print()
+    info(f"{Color.BOLD}Qt Installation Helper{Color.RESET}")
+    info("Qt requires an interactive installation (needs Qt account)")
+    print()
+    info("This will:")
+    info("  1. Download the Qt online installer (~2MB)")
+    info("  2. Launch it for you")
+    info("  3. Guide you through the installation")
+    print()
+    
+    # Qt online installer URL - try multiple mirrors
+    qt_urls = [
+        "https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-windows-x64-online.exe",
+        "https://download.qt.io/archive/online_installers/4.8/qt-unified-windows-x64-4.8.0-online.exe",
+        "https://download.qt.io/official_releases/online_installers/qt-unified-windows-x64-4.8-online.exe",
+    ]
+    qt_urls = [
+        "https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-windows-x64-online.exe",
+        "https://download.qt.io/archive/online_installers/4.8/qt-unified-windows-x64-4.8.0-online.exe",
+        "https://download.qt.io/official_releases/online_installers/qt-unified-windows-x64-4.8-online.exe",
+    ]
+    
+    try:
+        with tempfile.TemporaryDirectory() as tmpdir:
+            installer_path = os.path.join(tmpdir, "qt-installer.exe")
+            
+            # Try each URL until one works
+            downloaded = False
+            for qt_url in qt_urls:
+                try:
+                    info(f"Downloading Qt online installer from {qt_url.split('/')[2]}...")
+                    urllib.request.urlretrieve(qt_url, installer_path)
+                    success("Qt installer downloaded")
+                    downloaded = True
+                    break
+                except Exception as e:
+                    warning(f"Failed to download from {qt_url.split('/')[2]}: {e}")
+                    continue
+            
+            if not downloaded:
+                error("Could not download Qt installer from any mirror")
+                print()
+                info("Please download manually from:")
+                info("https://www.qt.io/download-qt-installer")
+                print()
+                info("Then run the installer and follow the steps below:")
+                print()
+                print(f"{Color.BOLD}=== Installation Steps ==={Color.RESET}")
+                print()
+                print(f"{Color.BOLD}1. Qt Account:{Color.RESET}")
+                print("   - Login with your Qt account (or create one - it's free)")
+                print()
+                print(f"{Color.BOLD}2. Installation Path:{Color.RESET}")
+                print("   - Use default: C:\\Qt")
+                print("   - Or custom path and set QT_ROOT environment variable")
+                print()
+                print(f"{Color.BOLD}3. Select Components (CRITICAL):{Color.RESET}")
+                print("   - Expand 'Qt 6.6.3' (or latest 6.x)")
+                print("   - CHECK: 'MSVC 2019 64-bit' (or 'MSVC 2022 64-bit')")
+                print("   - CHECK: 'Qt 5 Compatibility Module'")
+                print("   - CHECK: 'Qt Multimedia'")
+                print("   - Uncheck everything else to save space")
+                print()
+                print(f"{Color.BOLD}4. After Installation:{Color.RESET}")
+                print("   - Restart this terminal")
+                print("   - Run this script again")
+                print()
+                
+                # Try to open browser to Qt download page
+                try:
+                    import webbrowser
+                    webbrowser.open("https://www.qt.io/download-qt-installer")
+                    success("Opened Qt download page in your browser")
+                except Exception:
+                    pass
+                
+                return False
+            
+            print()
+            
+            print(f"{Color.BOLD}=== IMPORTANT: Follow these steps in the installer ==={Color.RESET}")
+            print()
+            print(f"{Color.BOLD}1. Qt Account:{Color.RESET}")
+            print("   - Login with your Qt account (or create one - it's free)")
+            print()
+            print(f"{Color.BOLD}2. Installation Path:{Color.RESET}")
+            print("   - Use default: C:\\Qt")
+            print("   - Or custom path and set QT_ROOT environment variable")
+            print()
+            print(f"{Color.BOLD}3. Select Components (CRITICAL):{Color.RESET}")
+            print("   - Expand 'Qt 6.6.3' (or latest 6.x)")
+            print("   - CHECK: 'MSVC 2019 64-bit' (or 'MSVC 2022 64-bit')")
+            print("   - CHECK: 'Qt 5 Compatibility Module'")
+            print("   - CHECK: 'Qt Multimedia'")
+            print("   - Uncheck everything else to save space")
+            print()
+            print(f"{Color.BOLD}4. After Installation:{Color.RESET}")
+            print("   - Restart this terminal")
+            print("   - Run this script again")
+            print()
+            print(f"{Color.BOLD}Press Enter to launch the Qt installer...{Color.RESET}")
+            input()
+            
+            info("Launching Qt installer...")
+            # Launch installer (non-blocking so script can continue)
+            subprocess.Popen([installer_path], shell=True)
+            
+            print()
+            success("Qt installer launched!")
+            warning("Complete the installation, then restart your terminal and run this script again")
+            print()
+            return False  # Return False since Qt isn't installed yet
+                
+    except Exception as e:
+        error(f"Failed to download/launch Qt installer: {e}")
+        return False
+
+
+def auto_install_dependencies() -> bool:
+    """Attempt to auto-install missing dependencies using winget."""
+    if not check_winget_available():
+        warning("winget not available - cannot auto-install")
+        warning("Please install dependencies manually or update to Windows 10/11 with winget")
+        return False
+    
+    info("Attempting to auto-install missing dependencies using winget...")
+    print()
+    
+    # Check what's missing
+    cmake_ok, _ = check_cmake()
+    ninja_ok = check_ninja()
+    msvc_ok, _ = check_msvc()
+    qt_ok, _, _ = check_qt()
+    
+    installs_needed = []
+    if not cmake_ok:
+        installs_needed.append(('CMake', 'Kitware.CMake'))
+    if not ninja_ok:
+        installs_needed.append(('Ninja', 'Ninja-build.Ninja'))
+    if not msvc_ok:
+        installs_needed.append(('Visual Studio 2022', 'Microsoft.VisualStudio.2022.Community', 
+                               '--override "--add Microsoft.VisualStudio.Workload.NativeDesktop"'))
+    
+    if not installs_needed and not qt_ok:
+        warning("Qt installation requires manual download from qt.io")
+        warning("winget Qt packages may not include required MSVC toolchain")
+        print()
+        info("Would you like to download and run the Qt installer now?")
+        
+        # Try to launch Qt installer helper
+        if install_qt_interactive():
+            return True
+        else:
+            # Installer was launched or failed - either way, user needs to complete it
+            return False
+    
+    if not installs_needed:
+        return True
+    
+    print(f"{Color.BOLD}Installing the following packages:{Color.RESET}")
+    for item in installs_needed:
+        print(f"  - {item[0]}")
+    print()
+    
+    success_count = 0
+    needs_restart = False
+    vs_failed = False
+    vs_needed = False
+    
+    for item in installs_needed:
+        name = item[0]
+        package_id = item[1]
+        extra_args = item[2] if len(item) > 2 else None
+        
+        info(f"Installing {name}...")
+        print(f"  This may take several minutes, please wait...")
+        print()
+        
+        cmd = ['winget', 'install', '--id', package_id, '--accept-package-agreements', '--accept-source-agreements']
+        if extra_args:
+            cmd.extend(extra_args.split())
+        
+        # Show live output - don't capture
+        try:
+            result = subprocess.run(cmd, check=False)
+            print()  # Add spacing after output
+            
+            if result.returncode == 0:
+                success(f"{name} installed successfully")
+                success_count += 1
+                
+                # CMake and Ninja need PATH restart
+                if 'CMake' in name or 'Ninja' in name:
+                    needs_restart = True
+            else:
+                error(f"Failed to install {name} (exit code: {result.returncode})")
+                
+                # Visual Studio often needs admin rights or has winget issues
+                if 'Visual Studio' in name:
+                    vs_failed = True
+                    vs_needed = True
+                else:
+                    warning("You may need to run this script as Administrator")
+        except Exception as e:
+            error(f"Error installing {name}: {e}")
+        
+        print()  # Add spacing between installs
+    
+    print()
+    
+    # Try direct VS install if winget failed
+    if vs_failed:
+        print()
+        warning("winget failed to install Visual Studio (common issue)")
+        info("Attempting direct installation using VS bootstrapper...")
+        print()
+        
+        if install_visual_studio_direct():
+            success("Visual Studio installed via direct download!")
+            success_count += 1
+            vs_failed = False
+        else:
+            print()
+            info(f"{Color.BOLD}Visual Studio Installation Failed{Color.RESET}")
+            info("Please install manually:")
+            info("1. Download: https://visualstudio.microsoft.com/downloads/")
+            info("2. Run the installer")
+            info("3. Select 'Desktop development with C++'")
+            info("4. Install and restart this script")
+            print()
+    
+    # Final summary
+    print()
+    if success_count == len(installs_needed):
+        success("All dependencies installed successfully!")
+        if needs_restart:
+            warning("CMake/Ninja were installed - you must restart this terminal!")
+            warning("Close this terminal, open a new one, and run this script again.")
+        return True
+    elif success_count > 0:
+        warning(f"Installed {success_count}/{len(installs_needed)} dependencies")
+        
+        if needs_restart:
+            print()
+            info(f"{Color.BOLD}IMPORTANT: CMake/Ninja were just installed!{Color.RESET}")
+            info("You MUST restart your terminal for PATH changes to take effect.")
+            info("Then run this script again to continue.")
+            print()
+        
+        warning("Please install remaining dependencies manually")
+        print_installation_guide()
+        return False
+    else:
+        error("Auto-install failed")
+        warning("Please install dependencies manually")
+        print_installation_guide()
+        return False
+
 def check_dependencies() -> Tuple[bool, Optional[Path]]:
 def check_dependencies() -> Tuple[bool, Optional[Path]]:
     """Check all required dependencies."""
     """Check all required dependencies."""
     info("Checking dependencies...")
     info("Checking dependencies...")
@@ -292,7 +742,6 @@ def check_dependencies() -> Tuple[bool, Optional[Path]]:
     
     
     if not all_ok:
     if not all_ok:
         error("Some dependencies are missing!")
         error("Some dependencies are missing!")
-        print_installation_guide()
         return False, None
         return False, None
     
     
     success("All dependencies found!")
     success("All dependencies found!")
@@ -302,6 +751,7 @@ def configure_project(build_dir: Path, build_type: str, qt_path: Optional[Path],
                      msvc_env: dict) -> None:
                      msvc_env: dict) -> None:
     """Configure the project with CMake."""
     """Configure the project with CMake."""
     info(f"Configuring project (Build type: {build_type})...")
     info(f"Configuring project (Build type: {build_type})...")
+    print("  Running CMake configuration...")
     
     
     cmake_args = [
     cmake_args = [
         'cmake',
         'cmake',
@@ -315,6 +765,11 @@ def configure_project(build_dir: Path, build_type: str, qt_path: Optional[Path],
     if qt_path:
     if qt_path:
         cmake_args.append(f'-DCMAKE_PREFIX_PATH={qt_path}')
         cmake_args.append(f'-DCMAKE_PREFIX_PATH={qt_path}')
     
     
+    # Check for CMAKE_BUILD_PARALLEL_LEVEL env var
+    parallel = os.environ.get('CMAKE_BUILD_PARALLEL_LEVEL')
+    if parallel:
+        info(f"  Build parallelism limited to {parallel} jobs")
+    
     run_command(cmake_args, env=msvc_env)
     run_command(cmake_args, env=msvc_env)
     success("Project configured")
     success("Project configured")
 
 
@@ -322,6 +777,14 @@ def build_project(build_dir: Path, msvc_env: dict) -> None:
     """Build the project."""
     """Build the project."""
     info("Building project...")
     info("Building project...")
     
     
+    parallel = os.environ.get('CMAKE_BUILD_PARALLEL_LEVEL')
+    if parallel:
+        info(f"  Using {parallel} parallel jobs (set via CMAKE_BUILD_PARALLEL_LEVEL)")
+    else:
+        warning("  Using all CPU cores - set CMAKE_BUILD_PARALLEL_LEVEL=2 to reduce load in VMs")
+    
+    print("  Compiling (this may take several minutes)...")
+    
     run_command(['cmake', '--build', str(build_dir)], env=msvc_env)
     run_command(['cmake', '--build', str(build_dir)], env=msvc_env)
     success("Project built successfully")
     success("Project built successfully")
 
 
@@ -379,38 +842,112 @@ def write_debug_scripts(app_dir: Path, app_name: str) -> None:
     """Write diagnostic scripts for troubleshooting."""
     """Write diagnostic scripts for troubleshooting."""
     info("Writing diagnostic scripts...")
     info("Writing diagnostic scripts...")
     
     
-    # run_debug.cmd
+    # run.cmd - Smart launcher with automatic fallback
+    run_smart_content = """@echo off
+setlocal
+cd /d "%~dp0"
+
+echo ============================================
+echo Standard of Iron - Smart Launcher
+echo ============================================
+echo.
+
+REM Try OpenGL first
+echo [1/2] Attempting OpenGL rendering...
+"%~dp0{app_name}.exe" 2>&1
+set EXIT_CODE=%ERRORLEVEL%
+
+REM Check for crash (access violation = -1073741819)
+if %EXIT_CODE% EQU -1073741819 (
+    echo.
+    echo [CRASH] OpenGL rendering failed!
+    echo [INFO] This is common on VMs or older hardware
+    echo.
+    echo [2/2] Retrying with software rendering...
+    echo.
+    set QT_QUICK_BACKEND=software
+    set QT_OPENGL=software
+    "%~dp0{app_name}.exe"
+    exit /b %ERRORLEVEL%
+)
+
+if %EXIT_CODE% NEQ 0 (
+    echo.
+    echo [ERROR] Application exited with code: %EXIT_CODE%
+    echo [HINT] Try running run_debug.cmd for detailed logs
+    pause
+    exit /b %EXIT_CODE%
+)
+
+exit /b 0
+""".format(app_name=app_name)
+    
+    run_smart_path = app_dir / "run.cmd"
+    run_smart_path.write_text(run_smart_content, encoding='ascii')
+    
+    # run_debug.cmd - Desktop OpenGL with verbose logging
     run_debug_content = """@echo off
     run_debug_content = """@echo off
 setlocal
 setlocal
 cd /d "%~dp0"
 cd /d "%~dp0"
 set QT_DEBUG_PLUGINS=1
 set QT_DEBUG_PLUGINS=1
-set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true
+set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true
+set QT_OPENGL=desktop
 set QT_QPA_PLATFORM=windows
 set QT_QPA_PLATFORM=windows
+echo Starting with Desktop OpenGL...
+echo Logging to runlog.txt
 "%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
 "%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
 echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"
 echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"
+type "%~dp0runlog.txt"
 pause
 pause
 """.format(app_name=app_name)
 """.format(app_name=app_name)
     
     
     run_debug_path = app_dir / "run_debug.cmd"
     run_debug_path = app_dir / "run_debug.cmd"
     run_debug_path.write_text(run_debug_content, encoding='ascii')
     run_debug_path.write_text(run_debug_content, encoding='ascii')
     
     
-    # run_debug_softwaregl.cmd
+    # run_debug_softwaregl.cmd - Software OpenGL fallback
     run_debug_softwaregl_content = """@echo off
     run_debug_softwaregl_content = """@echo off
 setlocal
 setlocal
 cd /d "%~dp0"
 cd /d "%~dp0"
 set QT_DEBUG_PLUGINS=1
 set QT_DEBUG_PLUGINS=1
-set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.quick.*=true
+set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.quick.*=true;qt.rhi.*=true
 set QT_OPENGL=software
 set QT_OPENGL=software
+set QT_QUICK_BACKEND=software
 set QT_QPA_PLATFORM=windows
 set QT_QPA_PLATFORM=windows
-"%~dp0{app_name}.exe" 1> "%~dp0runlog.txt" 2>&1
-echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog.txt"
+set QMLSCENE_DEVICE=softwarecontext
+echo Starting with Qt Quick Software Renderer (no OpenGL)...
+echo This is the safest mode for VMs and old hardware
+echo Logging to runlog_software.txt
+"%~dp0{app_name}.exe" 1> "%~dp0runlog_software.txt" 2>&1
+echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_software.txt"
+type "%~dp0runlog_software.txt"
 pause
 pause
 """.format(app_name=app_name)
 """.format(app_name=app_name)
     
     
     run_debug_softwaregl_path = app_dir / "run_debug_softwaregl.cmd"
     run_debug_softwaregl_path = app_dir / "run_debug_softwaregl.cmd"
     run_debug_softwaregl_path.write_text(run_debug_softwaregl_content, encoding='ascii')
     run_debug_softwaregl_path.write_text(run_debug_softwaregl_content, encoding='ascii')
     
     
-    success(f"Diagnostic scripts written: run_debug.cmd, run_debug_softwaregl.cmd")
+    # run_debug_angle.cmd - ANGLE (OpenGL ES via D3D)
+    run_debug_angle_content = """@echo off
+setlocal
+cd /d "%~dp0"
+set QT_DEBUG_PLUGINS=1
+set QT_LOGGING_RULES=qt.*=true;qt.qml=true;qqml.*=true;qt.rhi.*=true
+set QT_OPENGL=angle
+set QT_ANGLE_PLATFORM=d3d11
+set QT_QPA_PLATFORM=windows
+echo Starting with ANGLE (OpenGL ES via D3D11)...
+echo Logging to runlog_angle.txt
+"%~dp0{app_name}.exe" 1> "%~dp0runlog_angle.txt" 2>&1
+echo ExitCode: %ERRORLEVEL%>> "%~dp0runlog_angle.txt"
+type "%~dp0runlog_angle.txt"
+pause
+""".format(app_name=app_name)
+    
+    run_debug_angle_path = app_dir / "run_debug_angle.cmd"
+    run_debug_angle_path.write_text(run_debug_angle_content, encoding='ascii')
+
+    success(f"Diagnostic scripts written: run.cmd (smart), run_debug.cmd, run_debug_softwaregl.cmd, run_debug_angle.cmd")
+
 
 
 def copy_gl_angle_fallbacks(app_dir: Path, qt_path: Path) -> None:
 def copy_gl_angle_fallbacks(app_dir: Path, qt_path: Path) -> None:
     """Copy GL/ANGLE fallback DLLs for graphics compatibility."""
     """Copy GL/ANGLE fallback DLLs for graphics compatibility."""
@@ -457,6 +994,8 @@ def copy_assets(build_dir: Path) -> None:
 def create_package(build_dir: Path, build_type: str) -> Path:
 def create_package(build_dir: Path, build_type: str) -> Path:
     """Create distributable package."""
     """Create distributable package."""
     info("Creating distributable package...")
     info("Creating distributable package...")
+    warning("This may take several minutes with no progress indicator...")
+    print("  Compressing files (CPU-intensive, please wait)...")
     
     
     app_dir = build_dir / "bin"
     app_dir = build_dir / "bin"
     package_name = f"standard_of_iron-win-x64-{build_type}.zip"
     package_name = f"standard_of_iron-win-x64-{build_type}.zip"
@@ -465,17 +1004,24 @@ def create_package(build_dir: Path, build_type: str) -> Path:
     if package_path.exists():
     if package_path.exists():
         package_path.unlink()
         package_path.unlink()
     
     
+    import time
+    start_time = time.time()
+    
     shutil.make_archive(
     shutil.make_archive(
         package_path.stem,
         package_path.stem,
         'zip',
         'zip',
         app_dir
         app_dir
     )
     )
     
     
-    success(f"Package created: {package_path}")
+    elapsed = time.time() - start_time
+    success(f"Package created: {package_path} (took {elapsed:.1f}s)")
     return package_path
     return package_path
 
 
 def main() -> int:
 def main() -> int:
     """Main entry point."""
     """Main entry point."""
+    import time
+    start_time = time.time()
+    
     parser = argparse.ArgumentParser(
     parser = argparse.ArgumentParser(
         description="Build Standard-of-Iron on Windows",
         description="Build Standard-of-Iron on Windows",
         formatter_class=argparse.RawDescriptionHelpFormatter,
         formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -486,6 +1032,11 @@ def main() -> int:
         action='store_true',
         action='store_true',
         help='Skip dependency checks'
         help='Skip dependency checks'
     )
     )
+    parser.add_argument(
+        '--no-auto-install',
+        action='store_true',
+        help='Do NOT auto-install missing dependencies (show manual instructions instead)'
+    )
     parser.add_argument(
     parser.add_argument(
         '--build-type',
         '--build-type',
         choices=['Debug', 'Release', 'RelWithDebInfo'],
         choices=['Debug', 'Release', 'RelWithDebInfo'],
@@ -523,8 +1074,24 @@ def main() -> int:
     # Check dependencies unless skipped
     # Check dependencies unless skipped
     if not args.skip_checks and not args.deploy_only:
     if not args.skip_checks and not args.deploy_only:
         deps_ok, qt_path = check_dependencies()
         deps_ok, qt_path = check_dependencies()
+        
         if not deps_ok:
         if not deps_ok:
-            return 1
+            # Auto-install by default unless --no-auto-install
+            if not args.no_auto_install:
+                info("Attempting auto-install of missing dependencies...")
+                print()
+                if auto_install_dependencies():
+                    warning("Dependencies installed - please restart your terminal and run this script again")
+                    return 1
+                else:
+                    print()
+                    print_installation_guide()
+                    return 1
+            else:
+                info("Auto-install disabled (--no-auto-install flag)")
+                print()
+                print_installation_guide()
+                return 1
     else:
     else:
         # Try to find Qt even if checks are skipped
         # Try to find Qt even if checks are skipped
         _, qt_path, _ = check_qt()
         _, qt_path, _ = check_qt()
@@ -568,12 +1135,16 @@ def main() -> int:
     if not args.no_package:
     if not args.no_package:
         package_path = create_package(build_dir, args.build_type)
         package_path = create_package(build_dir, args.build_type)
         print()
         print()
-        info(f"Build complete! Package available at: {package_path}")
-        info(f"Executable location: {build_dir / 'bin' / 'standard_of_iron.exe'}")
+        elapsed = time.time() - start_time
+        success(f"Build complete! (total time: {elapsed:.1f}s)")
+        info(f"Package: {package_path}")
+        info(f"Executable: {build_dir / 'bin' / 'standard_of_iron.exe'}")
     else:
     else:
         print()
         print()
-        info(f"Build complete!")
-        info(f"Executable location: {build_dir / 'bin' / 'standard_of_iron.exe'}")
+        elapsed = time.time() - start_time
+        success(f"Build complete! (total time: {elapsed:.1f}s)")
+        info(f"Executable: {build_dir / 'bin' / 'standard_of_iron.exe'}")
+        info("Run with debug scripts: run_debug.cmd, run_debug_softwaregl.cmd, run_debug_angle.cmd")
     
     
     return 0
     return 0
 
 

+ 49 - 4
ui/gl_view.cpp

@@ -3,6 +3,7 @@
 
 
 #include <QOpenGLFramebufferObject>
 #include <QOpenGLFramebufferObject>
 #include <QOpenGLFramebufferObjectFormat>
 #include <QOpenGLFramebufferObjectFormat>
+#include <QOpenGLContext>
 #include <QQuickWindow>
 #include <QQuickWindow>
 #include <qobject.h>
 #include <qobject.h>
 #include <qopenglframebufferobject.h>
 #include <qopenglframebufferobject.h>
@@ -11,9 +12,27 @@
 #include <qtmetamacros.h>
 #include <qtmetamacros.h>
 #include <utility>
 #include <utility>
 
 
-GLView::GLView() { setMirrorVertically(true); }
+GLView::GLView() { 
+  setMirrorVertically(true);
+  
+  // Check if OpenGL is available
+  QOpenGLContext* ctx = QOpenGLContext::currentContext();
+  if (!ctx) {
+    qWarning() << "GLView: No OpenGL context available";
+    qWarning() << "GLView: 3D rendering will not work in software mode";
+    qWarning() << "GLView: Try running without QT_QUICK_BACKEND=software for full functionality";
+  }
+}
 
 
 auto GLView::createRenderer() const -> QQuickFramebufferObject::Renderer * {
 auto GLView::createRenderer() const -> QQuickFramebufferObject::Renderer * {
+  // Check if we have a valid OpenGL context
+  QOpenGLContext* ctx = QOpenGLContext::currentContext();
+  if (!ctx || !ctx->isValid()) {
+    qCritical() << "GLView::createRenderer() - No valid OpenGL context";
+    qCritical() << "Running in software rendering mode - 3D view not available";
+    return nullptr;
+  }
+  
   return new GLRenderer(m_engine);
   return new GLRenderer(m_engine);
 }
 }
 
 
@@ -33,17 +52,43 @@ GLView::GLRenderer::GLRenderer(QPointer<GameEngine> engine)
 
 
 void GLView::GLRenderer::render() {
 void GLView::GLRenderer::render() {
   if (m_engine == nullptr) {
   if (m_engine == nullptr) {
+    qWarning() << "GLRenderer::render() - engine is null";
+    return;
+  }
+  
+  // Double-check OpenGL context is still valid
+  QOpenGLContext* ctx = QOpenGLContext::currentContext();
+  if (!ctx || !ctx->isValid()) {
+    qCritical() << "GLRenderer::render() - OpenGL context lost";
     return;
     return;
   }
   }
-  m_engine->ensureInitialized();
-  m_engine->update(1.0F / 60.0F);
-  m_engine->render(m_size.width(), m_size.height());
+  
+  try {
+    m_engine->ensureInitialized();
+    m_engine->update(1.0F / 60.0F);
+    m_engine->render(m_size.width(), m_size.height());
+  } catch (const std::exception &e) {
+    qCritical() << "GLRenderer::render() exception:" << e.what();
+    return;
+  } catch (...) {
+    qCritical() << "GLRenderer::render() unknown exception";
+    return;
+  }
+  
   update();
   update();
 }
 }
 
 
 auto GLView::GLRenderer::createFramebufferObject(const QSize &size)
 auto GLView::GLRenderer::createFramebufferObject(const QSize &size)
     -> QOpenGLFramebufferObject * {
     -> QOpenGLFramebufferObject * {
   m_size = size;
   m_size = size;
+  
+  // Verify OpenGL context before creating FBO
+  QOpenGLContext* ctx = QOpenGLContext::currentContext();
+  if (!ctx || !ctx->isValid()) {
+    qCritical() << "GLRenderer::createFramebufferObject() - No valid OpenGL context";
+    return nullptr;
+  }
+  
   QOpenGLFramebufferObjectFormat fmt;
   QOpenGLFramebufferObjectFormat fmt;
   fmt.setAttachment(QOpenGLFramebufferObject::Depth);
   fmt.setAttachment(QOpenGLFramebufferObject::Depth);
   fmt.setSamples(0);
   fmt.setSamples(0);