Merge pull request #1143 from libretro/gl-d3d12-first

fix(windows): bundled Mesa uses Direct3D 12 before falling back to llvmpipe (#1104)
This commit is contained in:
Joe Mattiello authored and GitHub committed 2026-11-01 18:12:27 -05:00
commit a6769bb43e
20 files changed
+604 -10

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. -----------------------
+111
View File
@@ -0,0 +1,111 @@
name: Windows ARM OpenGL qualification
on:
pull_request:
# Any pull request that touches the Windows GL path, whatever its base: the
# qualification must also run for a PR stacked on another PR's branch.
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:
group: windows-gl-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
software-gl:
runs-on: windows-11-arm
timeout-minutes: 45
defaults:
run:
shell: 'msys2 {0}'
steps:
- uses: actions/checkout@v7
- uses: msys2/setup-msys2@v2
with:
msystem: CLANGARM64
update: false
install: >-
zip
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
- name: Configure and build the ARM64 test candidate
run: |
set -eu
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 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 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 Mesa OpenGL' diagnostics/automatic-stderr.txt
# The launcher picks d3d12 only for a hardware D3D12 adapter and llvmpipe
# otherwise; the hosted runner has only the Basic Render Driver (WARP), so
# it shows llvmpipe. Either is the bundled Mesa and not the GDI Generic
# 1.1 driver the native attempt reported.
grep -E 'renderer=(llvmpipe|D3D12)' diagnostics/automatic-stderr.txt
grep -E '\[gl\] (bundled Mesa will render through Direct3D 12|no hardware Direct3D 12 adapter)' 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
export LIBGL_ALWAYS_SOFTWARE=true
set +e
stage/bin/software-opengl/vj_gl_context_integration.exe legacy >diagnostics/legacy-loader.txt 2>&1
legacy_result=$?
set -e
echo "Legacy loader exit: $legacy_result" | tee -a diagnostics/legacy-loader.txt
stage/bin/software-opengl/vj_gl_context_integration.exe >diagnostics/sdl-loader.txt 2>&1
cat diagnostics/sdl-loader.txt
timeout 90 stage/bin/software-opengl/virtualjaguar.exe --data-dir "$PWD/test-data" \
--screenshot "$PWD/diagnostics/arm64-software-gl.png" --screenshot-frame 20 \
>diagnostics/app-stdout.txt 2>diagnostics/app-stderr.txt
test -s diagnostics/arm64-software-gl.png
command rm -f stage/bin/software-opengl/vj_gl_context_integration.exe
cat diagnostics/app-stderr.txt
- name: Package the portable test build
run: |
set -eu
printf '%s\n' 'Open virtualjaguar.exe normally. If its graphics driver cannot initialize, it automatically retries with bundled Mesa OpenGL: hardware rendering through Direct3D 12 when the machine has it, CPU rendering (slower) otherwise. 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
- uses: actions/upload-artifact@v7
if: always()
with:
name: windows-arm64-opengl-test
path: |
out/*
diagnostics/*
if-no-files-found: error
+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: |
+2 -1
View File
@@ -322,6 +322,7 @@ endif()
set(VJ_APP_SOURCES
src/main.cpp
src/app.cpp
src/gl_context.cpp
src/paths.cpp
src/instance.cpp
src/config.cpp
@@ -443,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)
+9
View File
@@ -311,8 +311,17 @@ endfunction()
# fallback. SKIPPED (exit 77) without the library or a display.
if(VJ_APP_BUILD_TESTS)
enable_testing()
add_executable(vj_gl_context_test tests/gl_context_test.cpp)
target_include_directories(vj_gl_context_test PRIVATE src)
add_test(NAME gl_context_unit COMMAND vj_gl_context_test)
add_executable(vj_gl_context_integration tests/gl_context_integration.cpp src/gl_context.cpp)
target_include_directories(vj_gl_context_integration PRIVATE src)
target_link_libraries(vj_gl_context_integration PRIVATE vj_imgui ${VJ_SDL3_TARGET})
add_test(NAME gl_context_integration COMMAND vj_gl_context_integration)
set_tests_properties(gl_context_integration PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77)
add_executable(vj_shader_tests
test/shader_tests.cpp
src/gl_context.cpp
src/shaders.cpp
src/present_util.cpp
src/paths.cpp
+18
View File
@@ -423,3 +423,21 @@ 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 a bundled Mesa OpenGL through
`windows/stage-software-gl.py` (MSYS2 Mesa, Python and objdump required) in
the `software-opengl/` folder (the name predates the Direct3D 12 path below).
The normal executable automatically retries once with the bundled app if
graphics startup fails, which on a stock Windows 10/11 install without a vendor
OpenGL driver (Windows on ARM, VMs, Remote Desktop, the basic display adapter)
means WGL handed us the GDI Generic OpenGL 1.1 driver. The retry preserves game
arguments, working directory and the original config/save root; users do not
need a separate launcher. The launcher picks Mesa's driver: `d3d12` (OpenGL on
the machine's Direct3D 12 driver, hardware rendering, the same layer as
Microsoft's OpenGL Compatibility Pack) when a hardware D3D12 adapter exists,
pinned to that adapter; `llvmpipe` (CPU rendering, slower) otherwise, including
when the only D3D12 adapter is the Microsoft Basic Render Driver (WARP), which
is software too and slower than llvmpipe. A `GALLIUM_DRIVER` already in the
environment wins. The `[gl]` log lines name the choice and the renderer that
came up. Keep the dependency notices and matching architecture alongside the
bundled executable.
+10
View File
@@ -80,3 +80,13 @@ 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 Mesa,
preserving command-line arguments and the original data root. Mesa renders on
the machine's hardware Direct3D 12 adapter when there is one (`d3d12`) and on
the CPU otherwise (`llvmpipe`; the Basic Render Driver does not count as
hardware). 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)
+11 -2
View File
@@ -1,6 +1,7 @@
#include "platform/file_access.h"
#include "app.h"
#include "build_channel.h"
#include "gl_context.h"
#include "devices_ui.h"
#include "fonts_imgui.h"
#include "ui_scale.h"
@@ -435,7 +436,11 @@ bool App::initWindow() {
std::fprintf(stderr, "cannot create an OpenGL 3.3 core context: %s\n", SDL_GetError());
return false;
}
SDL_GL_MakeCurrent(window_, gl_);
if (!SDL_GL_MakeCurrent(window_, gl_)) {
std::fprintf(stderr, "cannot make OpenGL context current: %s\n", SDL_GetError());
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");
@@ -2574,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;
+39
View File
@@ -0,0 +1,39 @@
#include "gl_context.h"
#include <SDL3/SDL.h>
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "imgui_impl_opengl3_loader.h"
namespace vj {
bool initOpenGLLoader() {
auto getString = reinterpret_cast<PFNGLGETSTRINGPROC>(SDL_GL_GetProcAddress("glGetString"));
const char* version = getString ? reinterpret_cast<const char*>(getString(GL_VERSION)) : nullptr;
const char* vendor = getString ? reinterpret_cast<const char*>(getString(GL_VENDOR)) : nullptr;
const char* renderer = getString ? reinterpret_cast<const char*>(getString(GL_RENDERER)) : nullptr;
std::fprintf(stderr, "[gl] version=%s; vendor=%s; renderer=%s\n",
version ? version : "unavailable", vendor ? vendor : "unavailable", renderer ? renderer : "unavailable");
if (!supportsDesktopGL33(version)) {
SDL_SetError("Virtual Jaguar requires desktop OpenGL 3.3 or newer; this driver provides %s. "
"Use a compatible graphics driver or a software OpenGL implementation.", version ? version : "no current OpenGL context");
std::fprintf(stderr, "[gl] %s\n", SDL_GetError());
return false;
}
#ifdef _WIN32
// Use the same library that created the context, including SDL_OPENGL_LIBRARY.
// ImGui's default loader independently opens opengl32.dll, which can be a
// different (legacy system) implementation on Windows ARM / software-GL setups.
const int result = imgl3wInit2([](const char* name) -> GL3WglProc {
return reinterpret_cast<GL3WglProc>(SDL_GL_GetProcAddress(name));
});
#else
const int result = imgl3wInit();
#endif
if (result != 0) {
SDL_SetError("Could not initialize the OpenGL function loader (error %d).", result);
std::fprintf(stderr, "[gl] %s\n", SDL_GetError());
return false;
}
return true;
}
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <cstdio>
namespace vj {
// SDL's WGL backend can return a legacy context even when 3.3 core was requested.
inline bool supportsDesktopGL33(const char* version) {
int major = 0, minor = 0;
return version && std::sscanf(version, "%d.%d", &major, &minor) == 2 &&
(major > 3 || (major == 3 && minor >= 3));
}
// Requires a current SDL context. Reports the actual driver and sets SDL's error on failure.
bool initOpenGLLoader();
}
+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;
}
+162
View File
@@ -0,0 +1,162 @@
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <dxgi1_2.h>
#include <d3d12.h>
#include "software_gl.h"
#include <algorithm>
#include <cstdio>
#include <cwchar>
#include <vector>
namespace vj {
namespace {
// The description of the first hardware adapter that can create a Direct3D 12
// device at feature level 11_0, or empty when the machine has none. The
// "Microsoft Basic Render Driver" (WARP) is a software adapter and is skipped:
// GL on WARP is slower than llvmpipe and the app did not survive it on the
// hosted ARM64 runner. dxgi.dll / d3d12.dll are loaded by hand so the
// executable keeps running on a machine without either (no link dependency).
std::wstring hardwareD3D12Adapter() {
std::wstring found;
HMODULE dxgi = LoadLibraryW(L"dxgi.dll");
HMODULE d3d12 = LoadLibraryW(L"d3d12.dll");
if (!dxgi || !d3d12) {
if (dxgi) FreeLibrary(dxgi);
if (d3d12) FreeLibrary(d3d12);
return found;
}
using CreateFactory1 = HRESULT(WINAPI*)(REFIID, void**);
using CreateDevice = HRESULT(WINAPI*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
auto createFactory = reinterpret_cast<CreateFactory1>(GetProcAddress(dxgi, "CreateDXGIFactory1"));
auto createDevice = reinterpret_cast<CreateDevice>(GetProcAddress(d3d12, "D3D12CreateDevice"));
IDXGIFactory1* factory = nullptr;
if (createFactory && createDevice && SUCCEEDED(createFactory(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(&factory)))) {
IDXGIAdapter1* adapter = nullptr;
for (UINT i = 0; found.empty() && factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) {
DXGI_ADAPTER_DESC1 desc = {};
// DXGI_ADAPTER_FLAG_SOFTWARE is not reliably set on the Basic Render
// Driver (the hosted ARM64 runner enumerates it without the flag), so
// also reject Microsoft's vendor id 0x1414 and the name itself.
if (SUCCEEDED(adapter->GetDesc1(&desc))) {
const bool software = (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) || desc.VendorId == 0x1414 ||
std::wcsstr(desc.Description, L"Basic Render") != nullptr;
if (!software && SUCCEEDED(createDevice(adapter, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), nullptr)))
found = desc.Description;
}
adapter->Release();
adapter = nullptr;
}
factory->Release();
}
FreeLibrary(d3d12);
FreeLibrary(dxgi);
return found;
}
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"VJ_SOFTWARE_OPENGL_RETRY=", 25)) continue;
entries.emplace_back(p);
}
FreeEnvironmentStringsW(block);
entries.push_back(L"SDL_OPENGL_LIBRARY=" + dll);
entries.push_back(L"VJ_SOFTWARE_OPENGL_RETRY=1");
// Which Gallium driver the bundled Mesa uses. It carries d3d12 (OpenGL on
// the Direct3D 12 driver every stock Windows 10/11 install has, the same
// layer as Microsoft's OpenGL Compatibility Pack) and llvmpipe (pure
// software). With a hardware D3D12 adapter present, d3d12 renders on the
// GPU, pinned to that adapter so Mesa cannot drift onto WARP; without one,
// llvmpipe: Mesa left alone takes the Microsoft Basic Render Driver, which
// is software D3D12 under a GL translation, slower than llvmpipe and not
// something the app survived on the hosted ARM64 runner. A GALLIUM_DRIVER
// the user already set wins over this probe.
if (!GetEnvironmentVariableW(L"GALLIUM_DRIVER", nullptr, 0)) {
std::wstring adapter = hardwareD3D12Adapter();
if (!adapter.empty()) {
entries.push_back(L"GALLIUM_DRIVER=d3d12");
if (!GetEnvironmentVariableW(L"MESA_D3D12_DEFAULT_ADAPTER_NAME", nullptr, 0))
entries.push_back(L"MESA_D3D12_DEFAULT_ADAPTER_NAME=" + adapter);
std::fprintf(stderr, "[gl] bundled Mesa will render through Direct3D 12 on %ls\n", adapter.c_str());
} else {
entries.push_back(L"GALLIUM_DRIVER=llvmpipe");
entries.push_back(L"LIBGL_ALWAYS_SOFTWARE=true");
std::fprintf(stderr, "[gl] no hardware Direct3D 12 adapter; bundled Mesa will render in software (llvmpipe)\n");
}
}
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 Mesa OpenGL (Direct3D 12 when available, software otherwise)\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
}
+2 -1
View File
@@ -21,6 +21,7 @@
#include "imgui_impl_opengl3_loader.h"
#include "config.h"
#include "gl_context.h"
#include "paths.h"
#include "present_util.h"
#include "shaders.h"
@@ -480,7 +481,7 @@ static int runGl() {
return 77;
}
SDL_GL_MakeCurrent(win, ctx);
if (imgl3wInit() != 0) {
if (!vj::initOpenGLLoader()) {
std::printf("shader_tests gl: FAIL cannot load GL entry points\n");
return 1;
}
+54
View File
@@ -0,0 +1,54 @@
// Real-context regression: SDL_OPENGL_LIBRARY must also supply the UI's GL entry points.
#include "gl_context.h"
#include <SDL3/SDL.h>
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "imgui_impl_opengl3_loader.h"
#include <cstring>
int main(int argc, char** argv) {
if (!SDL_Init(SDL_INIT_VIDEO)) return 77;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#ifdef __APPLE__
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif
SDL_Window* window = SDL_CreateWindow("GL loader regression", 32, 32, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
SDL_GLContext context = window ? SDL_GL_CreateContext(window) : nullptr;
if (!context) {
std::fprintf(stderr, "SKIP no test GL context: %s\n", SDL_GetError());
if (window) SDL_DestroyWindow(window);
SDL_Quit();
return 77;
}
auto getString = reinterpret_cast<PFNGLGETSTRINGPROC>(SDL_GL_GetProcAddress("glGetString"));
const char* version = getString ? reinterpret_cast<const char*>(getString(GL_VERSION)) : nullptr;
const bool legacy = argc > 1 && !std::strcmp(argv[1], "legacy");
std::printf("SDL context version=%s\n", version ? version : "unavailable");
if (!legacy && !vj::supportsDesktopGL33(version)) {
std::fprintf(stderr, "SKIP driver has no desktop GL 3.3: %s\n", version ? version : "unavailable");
SDL_GL_DestroyContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
return 77;
}
// 'legacy' replays the pre-fix independent OS-library loader for before/after evidence.
bool loaded = legacy ? imgl3wInit() == 0 : vj::initOpenGLLoader();
bool passed = false;
if (loaded && glClearColor && glClear && glReadPixels && glGetString) {
unsigned char pixel[4] = {};
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
passed = pixel[0] > 240 && pixel[1] < 15 && pixel[2] < 15;
std::printf("GL pixel=%u,%u,%u; version=%s\n", pixel[0], pixel[1], pixel[2], glGetString(GL_VERSION));
}
if (loaded) imgl3wShutdown();
SDL_GL_DestroyContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
std::puts(passed ? "PASS GL loader and real framebuffer readback" : "FAIL GL loader or framebuffer readback");
return passed ? 0 : 1;
}
+16
View File
@@ -0,0 +1,16 @@
#include "gl_context.h"
#include <cstdlib>
int main() {
struct Case { const char* version; bool supported; };
const Case cases[] = {{nullptr, false}, {"", false}, {"1.1.0", false}, {"2.1 Mesa", false},
{"3.2", false}, {"3.3 (Core Profile) Mesa", true}, {"4.6.0", true},
{"OpenGL ES 3.3", false}, {"garbage", false}};
for (const auto& c : cases) {
if (vj::supportsDesktopGL33(c.version) != c.supported) {
std::fprintf(stderr, "FAIL GL version gate: %s\n", c.version ? c.version : "null");
return 1;
}
}
std::puts("PASS desktop GL 3.3 gate (including SDL's legacy-context fallback)");
}
+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");
}