fix(windows): automatically retry graphics startup with bundled software OpenGL

This commit is contained in:
Joseph Mattiello authored and GitHub committed 2026-11-01 18:09:04 -05:00
1 parent 1e1fdc55a3
commit 11b9d2103f
14 files changed
+299 -70

No files matched your search

+7 -1
View File
@@ -349,6 +349,8 @@ jobs:
mingw-w64-x86_64-gcc
mingw-w64-x86_64-cmake
mingw-w64-x86_64-ninja
mingw-w64-x86_64-mesa
mingw-w64-x86_64-python
cmake_extra: ''
- arch: arm64
os: windows-11-arm
@@ -358,6 +360,8 @@ jobs:
mingw-w64-clang-aarch64-clang
mingw-w64-clang-aarch64-cmake
mingw-w64-clang-aarch64-ninja
mingw-w64-clang-aarch64-mesa
mingw-w64-clang-aarch64-python
cmake_extra: '-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++'
defaults:
run:
@@ -419,7 +423,9 @@ jobs:
if [ "$ARCH" = arm64 ]; then grep -qiE 'aarch64|arm64' machine.txt; else grep -qiE 'x86-64|i386:x86-64' machine.txt; fi
- name: Install tree
run: cmake --install build --prefix "$PWD/stage"
run: |
cmake --install build --prefix "$PWD/stage"
python app/packaging/windows/stage-software-gl.py stage
# ---- SignPath (stub): runs only with the `sign` input and SIGNPATH_API_TOKEN; the SignPath
# Foundation application is not approved yet, so this is dormant. -----------------------
+21 -58
View File
@@ -4,9 +4,15 @@ on:
branches: [develop]
paths:
- 'app/src/gl_context.*'
- 'app/src/software_gl.h'
- 'app/src/platform/software_gl_windows.cpp'
- 'app/src/main.cpp'
- 'app/tests/software_gl_test.cpp'
- 'app/packaging/windows/stage-software-gl.py'
- 'app/src/app.cpp'
- 'app/tests/gl_context*'
- 'app/cmake/VJShaders.cmake'
- 'app/cmake/VJPlatformTests.cmake'
- '.github/workflows/windows-gl-test.yml'
workflow_dispatch:
concurrency:
@@ -40,66 +46,31 @@ jobs:
cmake -S app -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DVJ_APP_WIN32_GUI=ON -DVJ_APP_WERROR=ON -DVJ_BUILD_CHANNEL=nightly
cmake --build build --target virtualjaguar vj_gl_context_test vj_gl_context_integration -j4
cmake --build build --target virtualjaguar vj_gl_context_test vj_gl_context_integration vj_software_gl_test -j4
build/vj_gl_context_test.exe
build/vj_software_gl_test.exe
cmake --install build --prefix "$PWD/stage"
- name: Stage a separate software OpenGL runtime and its notices
run: |
set -eu
mkdir -p stage/bin/software-opengl diagnostics
python - <<'PY'
import pathlib, re, shutil, subprocess
def windows_path(path):
return pathlib.Path(subprocess.check_output(['cygpath', '-m', str(path)], text=True).strip())
source = windows_path('/clangarm64/bin')
target = pathlib.Path('stage/bin/software-opengl')
pending = ['opengl32.dll']
copied = set()
owners = set()
while pending:
name = pending.pop()
if name.lower() in copied:
continue
path = source / name
if not path.exists():
continue # system DLL, not an MSYS2 dependency
copied.add(name.lower())
shutil.copy2(path, target / path.name)
imports = subprocess.check_output(['llvm-objdump', '-p', str(path)], text=True)
pending.extend(re.findall(r'DLL Name:\s*(\S+)', imports))
unix_path = subprocess.check_output(['cygpath', '-u', str(path)], text=True).strip()
owners.add(subprocess.check_output(['pacman', '-Qqo', unix_path], text=True).strip())
assert (target / 'libgallium_wgl.dll').exists(), 'Mesa WGL driver missing'
notices = target / 'licenses'
for owner in sorted(owners):
for line in subprocess.check_output(['pacman', '-Ql', owner], text=True).splitlines():
unix_path = line.split(' ', 1)[1]
if '/share/licenses/' not in unix_path or unix_path.endswith('/'):
continue
path = windows_path(unix_path)
if path.is_file():
dest = notices / owner / path.name
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, dest)
(target / 'PACKAGES.txt').write_text(subprocess.check_output(['pacman', '-Q', *sorted(owners)], text=True))
# WGL/GDI must find the Mesa frontend by its standard name next to the
# executable. Keep a separate full app copy so normal graphics stays clean.
for path in list(pathlib.Path('stage/bin').iterdir()):
if path == target:
continue
if path.is_dir():
shutil.copytree(path, target / path.name, dirs_exist_ok=True)
else:
shutil.copy2(path, target / path.name)
shutil.copy2('build/vj_gl_context_integration.exe', target)
(target / 'portable.txt').write_text('Keep software-test settings beside this executable.\n')
PY
mkdir -p diagnostics
python app/packaging/windows/stage-software-gl.py stage
command cp -f build/vj_gl_context_integration.exe stage/bin/software-opengl/
- name: Replay the old loader and qualify real ARM64 software GL rendering
run: |
set -eu
set +e
build/vj_gl_context_integration.exe legacy >diagnostics/native-driver.txt 2>&1
set -e
timeout 90 stage/bin/virtualjaguar.exe --data-dir "$PWD/automatic fallback data" \
--screenshot "$PWD/diagnostics/automatic-fallback.png" --screenshot-frame 20 \
>diagnostics/automatic-stdout.txt 2>diagnostics/automatic-stderr.txt
test -s diagnostics/automatic-fallback.png
grep -q 'automatically retrying with bundled software OpenGL' diagnostics/automatic-stderr.txt
grep -q 'renderer=llvmpipe' diagnostics/automatic-stderr.txt
test -f 'automatic fallback data/config.ini'
test ! -e 'automatic fallback data/saves/instance-2'
export PATH="$PWD/stage/bin/software-opengl:$PATH"
export SDL_OPENGL_LIBRARY="$(cygpath -w "$PWD/stage/bin/software-opengl/opengl32.dll")"
export GALLIUM_DRIVER=llvmpipe
@@ -120,15 +91,7 @@ jobs:
- name: Package the portable test build
run: |
set -eu
python - <<'PY'
import pathlib
root = pathlib.Path('stage/bin')
normal = '@echo off\ncd /d "%~dp0"\nvirtualjaguar.exe >test-stdout.txt 2>test-stderr.txt\necho Exit code: %errorlevel%>>test-stderr.txt\nnotepad test-stderr.txt\n'
software = '@echo off\nsetlocal\ncd /d "%~dp0software-opengl"\nset "PATH=%~dp0software-opengl;%PATH%"\nset "SDL_OPENGL_LIBRARY=%~dp0software-opengl\\opengl32.dll"\nset "GALLIUM_DRIVER=llvmpipe"\nset "LIBGL_ALWAYS_SOFTWARE=true"\nvirtualjaguar.exe >"%~dp0software-stdout.txt" 2>"%~dp0software-stderr.txt"\necho Exit code: %errorlevel%>>"%~dp0software-stderr.txt"\nnotepad "%~dp0software-stderr.txt"\n'
(root / 'Test-Normal-Graphics.cmd').write_bytes(normal.replace('\n','\r\n').encode())
(root / 'Test-Software-Graphics.cmd').write_bytes(software.replace('\n','\r\n').encode())
(root / 'TEST-BUILD.txt').write_text('ARM64 diagnostic candidate. Try Test-Normal-Graphics.cmd first, then Test-Software-Graphics.cmd. Close the app to open its log. Software mode uses CPU rendering and changes no system settings. Not a Store package.\n')
PY
printf '%s\n' 'Open virtualjaguar.exe normally. If its graphics driver cannot initialize, it automatically retries with bundled software OpenGL. CPU rendering can be slower. No driver installation or special launcher is needed.' >stage/bin/TEST-BUILD.txt
bash app/packaging/windows/package.sh stage gl-test out --arch arm64
printf '%s\n' "$GITHUB_SHA" >out/SOURCE_COMMIT.txt
sha256sum out/*.zip >out/SHA256SUMS.txt
+11 -3
View File
@@ -10,7 +10,14 @@ on:
- 'app/packaging/icons/**'
- 'app/packaging/check-payload.sh'
- 'test/roms/yarc.j64'
- 'app/packaging/windows/msix/**'
- 'app/packaging/windows/**'
- 'app/src/software_gl.h'
- 'app/src/gl_context.*'
- 'app/src/app.h'
- 'app/src/platform/software_gl_windows.cpp'
- 'app/src/main.cpp'
- 'app/src/app.cpp'
- 'app/CMakeLists.txt'
- 'app/src/platform/platform_windows.cpp'
- '.github/workflows/windows-store.yml'
workflow_dispatch:
@@ -40,12 +47,12 @@ jobs:
- arch: x64
os: windows-latest
msystem: MINGW64
packages: mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
packages: mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja mingw-w64-x86_64-mesa mingw-w64-x86_64-python
cmake_extra: ''
- arch: arm64
os: windows-11-arm
msystem: CLANGARM64
packages: mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja
packages: mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja mingw-w64-clang-aarch64-mesa mingw-w64-clang-aarch64-python
cmake_extra: '-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++'
steps:
- uses: actions/checkout@v7
@@ -75,6 +82,7 @@ jobs:
cmake --install build-store --prefix "$PWD/stage-store"
stage-store/bin/virtualjaguar.exe --headless --frames 300 --expect-hash f4c30ea3 test/roms/yarc.j64
bash app/packaging/check-payload.sh build-store stage-store/bin virtualjaguar.exe
python app/packaging/windows/stage-software-gl.py stage-store
- name: Pack and validate Store MSIX with the Windows SDK
shell: pwsh
run: |
+1 -1
View File
@@ -444,7 +444,7 @@ if(APPLE)
set_source_files_properties(src/platform/platform_macos.mm PROPERTIES
COMPILE_OPTIONS "-fobjc-arc;-Wno-deprecated-declarations")
elseif(WIN32)
list(APPEND VJ_APP_SOURCES src/platform/platform_windows.cpp)
list(APPEND VJ_APP_SOURCES src/platform/platform_windows.cpp src/platform/software_gl_windows.cpp)
else()
list(APPEND VJ_APP_SOURCES src/platform/platform_other.cpp)
endif()
+9
View File
@@ -44,3 +44,12 @@ target_include_directories(vj_window_chrome_test PRIVATE src)
target_link_libraries(vj_window_chrome_test PRIVATE vj_imgui)
add_test(NAME window_chrome_unit COMMAND vj_window_chrome_test)
set_tests_properties(window_chrome_unit PROPERTIES TIMEOUT 60 FAIL_REGULAR_EXPRESSION "FAIL ")
add_executable(vj_software_gl_test tests/software_gl_test.cpp)
target_include_directories(vj_software_gl_test PRIVATE src)
if(WIN32)
target_sources(vj_software_gl_test PRIVATE src/platform/software_gl_windows.cpp)
target_link_libraries(vj_software_gl_test PRIVATE shell32)
endif()
add_test(NAME software_gl_unit COMMAND vj_software_gl_test)
set_tests_properties(software_gl_unit PROPERTIES TIMEOUT 60)
+8
View File
@@ -423,3 +423,11 @@ PNG, `.ico` and `.icns` files.
Gatekeeper first-run on a freshly downloaded signed zip, SmartScreen on the installer, Intel Mac behavior
(CI may not run the x86_64 slice), Wayland/X11/PipeWire sound on real Linux desktops, and the AppImage on
distributions older than Ubuntu 22.04.
Windows packages also stage Mesa software OpenGL through
`windows/stage-software-gl.py` (MSYS2 Mesa, Python and objdump required).
The normal executable automatically retries once with the bundled software app
if graphics startup fails. The retry preserves game arguments, working directory
and the original config/save root; users do not need a separate launcher.
Software rendering uses the CPU and can be slower. Keep its dependency notices
and matching architecture alongside the software executable.
+7
View File
@@ -80,3 +80,10 @@ upload multiple different builds under an already published version.
References: [manual packaging](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion),
[version rules](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/package-version-numbering),
[signing](https://learn.microsoft.com/en-us/windows/msix/package/signing-package-overview).
Windows packages include a `software-opengl/` app copy and Mesa dependency notices.
Startup tries the normal driver first. If window/context/renderer initialization
fails, it releases the first instance and retries once with the bundled CPU
renderer, preserving command-line arguments and the original data root. Only the
child's environment selects Mesa; system drivers and settings are unchanged.
Qualify both the normal and fallback launch paths for each Store architecture.
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
# Stage the MSYS2 Mesa dependency closure and matching app copy for automatic fallback.
import os, pathlib, re, shutil, subprocess, sys
def windows_path(path):
return pathlib.Path(subprocess.check_output(['cygpath', '-m', str(path)], text=True).strip())
source = windows_path(os.environ['MINGW_PREFIX'] + '/bin')
bindir = pathlib.Path(sys.argv[1]) / 'bin'
target = bindir / 'software-opengl'
if target.exists():
raise SystemExit('Choose a fresh install prefix: software OpenGL is already staged')
target.mkdir()
objdump = shutil.which('llvm-objdump') or shutil.which('objdump')
if not objdump:
raise SystemExit('MSYS2 objdump is required to stage the software renderer')
pending = ['opengl32.dll']
copied = set()
owners = set()
while pending:
name = pending.pop()
if name.lower() in copied:
continue
path = source / name
if not path.exists():
continue # system DLL, not an MSYS2 dependency
copied.add(name.lower())
shutil.copy2(path, target / path.name)
imports = subprocess.check_output([objdump, '-p', str(path)], text=True)
pending.extend(re.findall(r'DLL Name:\s*(\S+)', imports))
unix_path = subprocess.check_output(['cygpath', '-u', str(path)], text=True).strip()
owners.add(subprocess.check_output(['pacman', '-Qqo', unix_path], text=True).strip())
assert (target / 'libgallium_wgl.dll').exists(), 'Mesa WGL driver missing'
notices = target / 'licenses'
for owner in sorted(owners):
for line in subprocess.check_output(['pacman', '-Ql', owner], text=True).splitlines():
unix_path = line.split(' ', 1)[1]
if '/share/licenses/' not in unix_path or unix_path.endswith('/'):
continue
path = windows_path(unix_path)
if path.is_file():
dest = notices / owner / path.name
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, dest)
(target / 'PACKAGES.txt').write_text(subprocess.check_output(['pacman', '-Q', *sorted(owners)], text=True))
# WGL/GDI must find the Mesa frontend by its standard name next to the
# executable. Keep a separate full app copy so normal graphics stays clean.
for path in list(bindir.iterdir()):
if path == target:
continue
if path.is_dir():
shutil.copytree(path, target / path.name, dirs_exist_ok=True)
else:
shutil.copy2(path, target / path.name)
+6 -5
View File
@@ -440,10 +440,7 @@ bool App::initWindow() {
std::fprintf(stderr, "cannot make OpenGL context current: %s\n", SDL_GetError());
return false;
}
if (!initOpenGLLoader()) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Virtual Jaguar graphics initialization", SDL_GetError(), window_);
return false;
}
if (!initOpenGLLoader()) return false;
swapGuard_.contextCreated(platform::displayAsleep(window_));
if (swapGuard_.deadLink())
std::fprintf(stderr, "[gl] the display is asleep: vsync is off for this run (a swap would wait for a refresh that never comes)\n");
@@ -2582,7 +2579,11 @@ int App::runInteractive(const CliArgs& a) {
lowDpi_ = a.lowDpi || a.pixelDensity > 1; // --pixel-density builds its own density on a 1x window
simDensity_ = a.pixelDensity > 1 ? std::min(4u, a.pixelDensity) : 0;
g_simDensity = simDensity_ ? (float)simDensity_ : 1.0f;
if (!initWindow() || !initImGui()) return 1;
if (!initWindow() || !initImGui()) {
graphicsStartupError = SDL_GetError();
if (graphicsStartupError.empty()) graphicsStartupError = "Could not initialize the graphics renderer.";
return 1;
}
desktopInit();
if (!a.font.empty()) {
if (!fonts::applySpec(a.font, fontSettings)) std::fprintf(stderr, "--font: cannot read '%s'\n", a.font.c_str());
+1
View File
@@ -164,6 +164,7 @@ public:
int run(const CliArgs& args); // returns the process exit code
// ---- services (public: the UI drives them) -------------------------------
std::string graphicsStartupError; // nonempty only when window/context/UI initialization fails
Paths paths;
Config cfg;
CoreHost& host;
+14 -2
View File
@@ -6,6 +6,7 @@
#include <cstring>
#include "app.h"
#include "software_gl.h"
#include "platform/commands.h"
#include "platform/platform.h"
@@ -327,6 +328,17 @@ int main(int argc, char** argv) {
return 0;
}
if (a.dumpNativeMenu) return vj::platform::dumpNativeMenu();
vj::App app;
return app.run(a);
int result = 1;
std::string graphicsError, dataDir;
{
vj::App app;
result = app.run(a);
graphicsError = app.graphicsStartupError;
dataDir = app.paths.root;
} // Release workers, streams and the instance lock before the software retry.
if (!graphicsError.empty()) {
if (vj::retrySoftwareOpenGL(argc, argv, dataDir, result)) return result;
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Virtual Jaguar graphics initialization", graphicsError.c_str(), nullptr);
}
return result;
}
+99
View File
@@ -0,0 +1,99 @@
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include "software_gl.h"
#include <algorithm>
#include <cstdio>
#include <cwchar>
#include <vector>
namespace vj {
namespace {
std::wstring wide(const std::string& text) {
if (text.empty()) return {};
int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), (int)text.size(), nullptr, 0);
if (!size) return {};
std::wstring result((size_t)size, L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), (int)text.size(), result.data(), size);
return result;
}
bool fileExists(const std::wstring& path) {
DWORD attr = GetFileAttributesW(path.c_str());
return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
}
}
bool retrySoftwareOpenGL(int argc, char** argv, const std::string& dataDir, int& exitCode) {
if (GetEnvironmentVariableW(L"VJ_SOFTWARE_OPENGL_RETRY", nullptr, 0)) return false;
wchar_t path[32768];
DWORD size = GetModuleFileNameW(nullptr, path, (DWORD)(sizeof path / sizeof path[0]));
if (!size || size >= sizeof path / sizeof path[0]) return false;
std::wstring exe(path, size);
size_t slash = exe.find_last_of(L"\\/");
if (slash == std::wstring::npos) return false;
std::wstring folder = exe.substr(0, slash + 1) + L"software-opengl\\";
std::wstring child = folder + L"virtualjaguar.exe";
std::wstring dll = folder + L"opengl32.dll";
if (!fileExists(child) || !fileExists(dll) || !fileExists(folder + L"libgallium_wgl.dll")) {
std::fprintf(stderr, "[gl] bundled software OpenGL is unavailable\n");
return false;
}
std::wstring command = quoteSoftwareGLArgument(child);
for (int i = 1; i < argc; ++i) command += L" " + quoteSoftwareGLArgument(wide(argv[i]));
// The executable lives beside Mesa, but config/saves and the instance number
// must keep using the original app's data root (including portable installs).
command += L" --data-dir " + quoteSoftwareGLArgument(wide(dataDir));
std::vector<std::wstring> entries;
wchar_t* block = GetEnvironmentStringsW();
if (!block) return false;
for (const wchar_t* p = block; *p; p += std::wcslen(p) + 1) {
if (!_wcsnicmp(p, L"SDL_OPENGL_LIBRARY=", 19) || !_wcsnicmp(p, L"GALLIUM_DRIVER=", 15) ||
!_wcsnicmp(p, L"LIBGL_ALWAYS_SOFTWARE=", 22) || !_wcsnicmp(p, L"VJ_SOFTWARE_OPENGL_RETRY=", 25)) continue;
entries.emplace_back(p);
}
FreeEnvironmentStringsW(block);
entries.push_back(L"SDL_OPENGL_LIBRARY=" + dll);
entries.push_back(L"GALLIUM_DRIVER=llvmpipe");
entries.push_back(L"LIBGL_ALWAYS_SOFTWARE=true");
entries.push_back(L"VJ_SOFTWARE_OPENGL_RETRY=1");
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; });
std::vector<wchar_t> environment;
for (const auto& entry : entries) environment.insert(environment.end(), entry.c_str(), entry.c_str() + entry.size() + 1);
environment.push_back(L'\0');
STARTUPINFOW startup = {};
startup.cb = sizeof startup;
// Preserve redirected diagnostics without inheriting an instance lock.
HANDLE copies[3] = {};
const DWORD ids[] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE};
for (int i = 0; i < 3; ++i) {
HANDLE h = GetStdHandle(ids[i]);
if (h && h != INVALID_HANDLE_VALUE)
DuplicateHandle(GetCurrentProcess(), h, GetCurrentProcess(), &copies[i], 0, TRUE, DUPLICATE_SAME_ACCESS);
}
if (copies[1] || copies[2]) {
startup.dwFlags = STARTF_USESTDHANDLES;
startup.hStdInput = copies[0]; startup.hStdOutput = copies[1]; startup.hStdError = copies[2];
}
PROCESS_INFORMATION process = {};
std::fprintf(stderr, "[gl] automatically retrying with bundled software OpenGL\n");
std::fflush(nullptr);
BOOL created = CreateProcessW(child.c_str(), command.data(), nullptr, nullptr, TRUE,
CREATE_UNICODE_ENVIRONMENT, environment.data(), nullptr, &startup, &process);
DWORD error = GetLastError();
for (HANDLE h : copies) if (h) CloseHandle(h);
if (!created) {
std::fprintf(stderr, "[gl] software renderer could not start (Windows error %lu)\n", (unsigned long)error);
return false;
}
CloseHandle(process.hThread);
DWORD result = 1;
if (WaitForSingleObject(process.hProcess, INFINITE) == WAIT_OBJECT_0) GetExitCodeProcess(process.hProcess, &result);
CloseHandle(process.hProcess);
exitCode = (int)result;
return true;
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <string>
namespace vj {
// Windows command-line quoting, including empty arguments and trailing backslashes.
inline std::wstring quoteSoftwareGLArgument(const std::wstring& arg) {
std::wstring out = L"\"";
size_t slashes = 0;
for (wchar_t c : arg) {
if (c == L'\\') { ++slashes; continue; }
out.append(slashes * (c == L'"' ? 2 : 1), L'\\');
slashes = 0;
if (c == L'"') out += L'\\';
out += c;
}
out.append(slashes * 2, L'\\');
return out + L'"';
}
#ifdef _WIN32
// Called only after the failed App is destroyed and its instance lock is released.
// Runs at most once and returns the child exit status, preserving CLI/wait semantics.
bool retrySoftwareOpenGL(int argc, char** argv, const std::string& dataDir, int& exitCode);
#else
inline bool retrySoftwareOpenGL(int, char**, const std::string&, int&) { return false; }
#endif
}
+36
View File
@@ -0,0 +1,36 @@
#include "software_gl.h"
#include <cstdio>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#endif
int main() {
const std::vector<std::wstring> args = {L"", L"plain", L"path with spaces", L"C:\\", L"C:\\folder\\", L"quoted \"value\"", L"slashes\\\\\"quote", L"Unicode \u2603"};
#ifdef _WIN32
std::wstring command = L"program.exe";
for (const auto& arg : args) command += L" " + vj::quoteSoftwareGLArgument(arg);
int count = 0;
wchar_t** parsed = CommandLineToArgvW(command.c_str(), &count);
bool ok = parsed && count == (int)args.size() + 1;
if (ok) for (size_t i = 0; i < args.size(); ++i) ok = ok && args[i] == parsed[i + 1];
if (parsed) LocalFree(parsed);
if (!ok) { std::puts("FAIL Windows argument roundtrip"); return 1; }
int exitCode = 123;
SetEnvironmentVariableW(L"VJ_SOFTWARE_OPENGL_RETRY", L"1");
if (vj::retrySoftwareOpenGL(0, nullptr, "", exitCode) || exitCode != 123) {
std::puts("FAIL software retry loop guard"); return 1;
}
SetEnvironmentVariableW(L"VJ_SOFTWARE_OPENGL_RETRY", nullptr);
// This unit binary's build directory has no staged software-opengl app.
if (vj::retrySoftwareOpenGL(0, nullptr, "", exitCode) || exitCode != 123) {
std::puts("FAIL missing runtime should not launch a child"); return 1;
}
#else
if (vj::quoteSoftwareGLArgument(L"C:\\") != L"\"C:\\\\\"" || vj::quoteSoftwareGLArgument(L"") != L"\"\"") {
std::puts("FAIL Windows trailing-backslash quoting"); return 1;
}
#endif
std::puts("PASS software graphics arguments and retry guards");
}