mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
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:
20 files changed
+604
-10
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. -----------------------
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
@@ -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