mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
fix(windows): automatically retry graphics startup with bundled software OpenGL
This commit is contained in:
1 parent
1e1fdc55a3
commit
11b9d2103f
14 files changed
+299
-70
No files matched your search
@@ -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. -----------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user