From 0b4d11bca795a0432708eacc987f596a73c53aea Mon Sep 17 00:00:00 2001 From: Joris Pinkse Date: Wed, 12 Aug 2026 00:29:22 -0400 Subject: [PATCH 1/4] Fall back to the NULL platform when no display is available GLFW.jl calls glfwInit() from __init__, so on a machine with no display server -- headless CI, cluster nodes, batch jobs -- merely loading the package (or any package that depends on it) throws an InitError, and it cannot even be precompiled there. Downstream packages get no chance to handle the situation themselves. GLFW 3.4's NULL platform exists for exactly this case and initializes without a display. When the default platform fails and the user has not requested a platform explicitly, retry with PLATFORM_NULL and warn: the package loads, windows cannot be created or shown, and downstream code can detect the state with GLFW.GetPlatform() == GLFW.PLATFORM_NULL. An explicit JULIA_GLFW_PLATFORM is honored as stated: if the requested platform cannot be initialized, loading fails as before rather than being second-guessed. Behavior with a working display is unchanged. Fixes #169. Addresses the load-time layer of #200 and #253. Co-Authored-By: Claude Opus 5 --- Project.toml | 2 +- README.md | 2 ++ src/GLFW.jl | 24 ++++++++++++++++++++++-- test/runtests.jl | 18 ++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 86fcf26..3867486 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ author = ["Jay Petacat ", "Simon Danisch "] name = "GLFW" uuid = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98" -version = "3.4.6" +version = "3.4.7" [deps] GLFW_jll = "0656b61e-2033-5cc2-a64a-77c0f6c09b89" diff --git a/README.md b/README.md index 5f0ac10..d08356b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Read the [GLFW documentation][docs] for detailed instructions on how to use the * Clipboard (`glfwGetClipboard`, `glfwSetClipboard`) and time (`glfwGetTime`, `glfwSetTime`) functions have been omitted because Julia's standard library already supports similar functionality. * `glfwInit` and `glfwTerminate` are called automatically using the `__init__` and `atexit` functions. While it is okay to still call them explicitly, it is redundant and not required. +* If no display is available (headless CI, cluster or batch nodes), loading the package warns and falls back to GLFW's NULL platform instead of throwing, so packages that depend on GLFW still load. Windows cannot be created or shown in that state; check for it with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. +* The `JULIA_GLFW_PLATFORM` environment variable (`"win32"`, `"cocoa"`, `"wayland"`, `"x11"`, `"null"`, or `"any"`) forces a specific platform. An explicitly requested platform is honored as stated: if it cannot be initialized, loading the package fails rather than falling back. * Thread restrictions are explicitly checked and throw errors when they are not respected. This can be turned off by setting `GLFW.ENABLE_THREAD_ASSERTIONS[] = false` or by setting an environment variable of the same name. [docs]: https://www.glfw.org/docs/latest/ diff --git a/src/GLFW.jl b/src/GLFW.jl index fd0251c..636a2e6 100644 --- a/src/GLFW.jl +++ b/src/GLFW.jl @@ -56,10 +56,30 @@ function __init__() Init() catch err push!(errors, err) - finally - SetErrorCallback(throw) end + # Without a display server (headless CI, cluster nodes, batch jobs) the + # default platform fails and loading the package would throw. Fall back + # to the NULL platform so the package still loads; windows cannot be + # created there, and `GetPlatform() == PLATFORM_NULL` lets downstream + # code detect that it is headless. An explicitly requested platform is + # honored as stated: if JULIA_GLFW_PLATFORM is set and does not work, + # loading fails rather than being second-guessed. + if !is_initialized() && !haskey(ENV, "JULIA_GLFW_PLATFORM") && PlatformSupported(PLATFORM_NULL) + try + Init(platform = PLATFORM_NULL) + @warn "GLFW could not connect to a display and fell back to its NULL platform: " * + "the package is usable, but windows cannot be created or shown. Detect this " * + "with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. To make a missing display " * + "fatal again, set the JULIA_GLFW_PLATFORM environment variable to the " * + "required platform (e.g. \"x11\")." + catch err + push!(errors, err) + end + end + + SetErrorCallback(throw) + if is_initialized() atexit(Terminate) for err in errors diff --git a/test/runtests.jl b/test/runtests.jl index a2909f3..8d18e86 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,6 +15,24 @@ println(GLFW.GetVersionString()) @test GLFW.RELEASE == false @test false == GLFW.RELEASE +if Sys.islinux() + # Loading with no display must not throw: __init__ falls back to the + # NULL platform (#169, #200, #253). Checked in a subprocess so this + # process's initialized GLFW is not disturbed. + base = `$(Base.julia_cmd()) --project=$(Base.active_project()) --startup-file=no` + env = copy(ENV) + delete!(env, "DISPLAY") + delete!(env, "WAYLAND_DISPLAY") + delete!(env, "JULIA_GLFW_PLATFORM") + check = "using GLFW; print(GLFW.is_initialized() && GLFW.GetPlatform() == GLFW.PLATFORM_NULL)" + @test read(setenv(`$base -e $check`, env), String) == "true" + + # An explicitly requested platform that cannot work must still fail. + env["JULIA_GLFW_PLATFORM"] = "x11" + failing = setenv(`$base -e "using GLFW"`, env) + @test !success(pipeline(failing, stdout = devnull, stderr = devnull)) +end + if !haskey(ENV, "CI") # AppVeyor and Travis CI don't support OpenGL include("windowclose.jl") end From 8ebbdc322a9ac1933798332a15fc7929c586acfe Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 25 Aug 2026 19:27:51 +0200 Subject: [PATCH 2/4] clean up init implementation --- README.md | 4 +-- src/GLFW.jl | 63 ++++++++++++++++++++------------------- src/glfw3.jl | 66 ++++++++++++++++++++++++++--------------- test/runtests.jl | 76 ++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 142 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index d08356b..efe635f 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ Read the [GLFW documentation][docs] for detailed instructions on how to use the * Clipboard (`glfwGetClipboard`, `glfwSetClipboard`) and time (`glfwGetTime`, `glfwSetTime`) functions have been omitted because Julia's standard library already supports similar functionality. * `glfwInit` and `glfwTerminate` are called automatically using the `__init__` and `atexit` functions. While it is okay to still call them explicitly, it is redundant and not required. -* If no display is available (headless CI, cluster or batch nodes), loading the package warns and falls back to GLFW's NULL platform instead of throwing, so packages that depend on GLFW still load. Windows cannot be created or shown in that state; check for it with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. -* The `JULIA_GLFW_PLATFORM` environment variable (`"win32"`, `"cocoa"`, `"wayland"`, `"x11"`, `"null"`, or `"any"`) forces a specific platform. An explicitly requested platform is honored as stated: if it cannot be initialized, loading the package fails rather than falling back. +* Loading the package does not throw when the default platform cannot be initialized. It first lets GLFW pick any platform that is actually available (so a machine with Wayland but no X11 still gets a usable window system), and only if that fails too does it warn and fall back to GLFW's NULL platform, so packages that depend on GLFW still load. Windows cannot be created or shown on the NULL platform; check for it with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. +* The `JULIA_GLFW_PLATFORM` environment variable (`"win32"`, `"cocoa"`, `"wayland"`, `"x11"`, `"null"`, or `"any"`) forces a specific platform. A named platform is honored as stated: if it cannot be initialized, loading the package fails rather than falling back — this is how you make a missing display fatal. `"any"` and unrecognized values do not name a platform, so they still fall back as above; an unrecognized value additionally warns. * Thread restrictions are explicitly checked and throw errors when they are not respected. This can be turned off by setting `GLFW.ENABLE_THREAD_ASSERTIONS[] = false` or by setting an environment variable of the same name. [docs]: https://www.glfw.org/docs/latest/ diff --git a/src/GLFW.jl b/src/GLFW.jl index 636a2e6..6ddb309 100644 --- a/src/GLFW.jl +++ b/src/GLFW.jl @@ -47,47 +47,50 @@ include("glfw3.jl") include("vulkan.jl") include("monitor_properties.jl") +# Platforms `__init__` tries, in order. A platform named by JULIA_GLFW_PLATFORM +# is used alone, so failing to initialize it is fatal. Otherwise ANY_PLATFORM +# catches a working platform the default misses (e.g. Wayland without X11), and +# PLATFORM_NULL initializes without a display but cannot create windows. +function InitCandidates() + requested = RequestedPlatform() + # ANY_PLATFORM asks GLFW to choose rather than naming a platform, so it + # still gets the fallbacks below. GLFW never chooses PLATFORM_NULL itself. + names_a_platform = !isnothing(requested) && requested !== ANY_PLATFORM + names_a_platform && return Platform[requested] + + candidates = isnothing(requested) ? Platform[OS_DEFAULT_PLATFORM, ANY_PLATFORM] : Platform[ANY_PLATFORM] + PlatformSupported(PLATFORM_NULL) && push!(candidates, PLATFORM_NULL) + return unique!(candidates) +end + function __init__() # Save errors that occur during initialization errors = Vector{Exception}() SetErrorCallback(err -> push!(errors, err)) + candidates = InitCandidates() try - Init() - catch err - push!(errors, err) - end - - # Without a display server (headless CI, cluster nodes, batch jobs) the - # default platform fails and loading the package would throw. Fall back - # to the NULL platform so the package still loads; windows cannot be - # created there, and `GetPlatform() == PLATFORM_NULL` lets downstream - # code detect that it is headless. An explicitly requested platform is - # honored as stated: if JULIA_GLFW_PLATFORM is set and does not work, - # loading fails rather than being second-guessed. - if !is_initialized() && !haskey(ENV, "JULIA_GLFW_PLATFORM") && PlatformSupported(PLATFORM_NULL) - try - Init(platform = PLATFORM_NULL) - @warn "GLFW could not connect to a display and fell back to its NULL platform: " * - "the package is usable, but windows cannot be created or shown. Detect this " * - "with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. To make a missing display " * - "fatal again, set the JULIA_GLFW_PLATFORM environment variable to the " * - "required platform (e.g. \"x11\")." - catch err - push!(errors, err) + for platform in candidates + Init(platform = platform) && break end + finally + SetErrorCallback(throw) end - SetErrorCallback(throw) + is_initialized() || throw(errors) # Throw fatal errors - if is_initialized() - atexit(Terminate) - for err in errors - @warn err # Warn about any non-fatal errors that may have occurred during initialization - end - else - throw(errors) # Throw fatal errors + atexit(Terminate) + for err in errors + @warn err # Warn about any non-fatal errors that may have occurred during initialization + end + if GetPlatform() === PLATFORM_NULL && candidates != [PLATFORM_NULL] + @warn "GLFW found no usable windowing platform and fell back to its NULL platform: the " * + "package is usable, but windows cannot be created or shown. Detect this with " * + "`GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. To make this fatal instead, set the " * + "JULIA_GLFW_PLATFORM environment variable to the platform you require " * + "(e.g. \"x11\" or \"wayland\")." end + return end end diff --git a/src/glfw3.jl b/src/glfw3.jl index 69af1b9..5629866 100644 --- a/src/glfw3.jl +++ b/src/glfw3.jl @@ -496,31 +496,48 @@ is_initialized() = INITIALIZED[] # Initialization and version information InitHint(hint, value) = @require_main_thread ccall((:glfwInitHint, libglfw), Cvoid, (Cint, Cint), hint, value) -function Init(; platform::Platform = @static Sys.islinux() ? PLATFORM_X11 : ANY_PLATFORM) - require_main_thread() - if haskey(ENV, "JULIA_GLFW_PLATFORM") - platform = HandlePlatformSelection() - end - InitHint(PLATFORM, platform) - INITIALIZED[] = Bool(ccall((:glfwInit, libglfw), Cint, ())) || error("glfwInit failed") -end - -function HandlePlatformSelection()::Platform - platform::String = ENV["JULIA_GLFW_PLATFORM"] - - if platform == "win32" - return PLATFORM_WIN32 - elseif platform == "cocoa" - return PLATFORM_COCOA - elseif platform == "wayland" - return PLATFORM_WAYLAND - elseif platform == "x11" - return PLATFORM_X11 - elseif platform == "null" - return PLATFORM_NULL - else +const PLATFORM_BY_NAME = Dict{String, Platform}( + "win32" => PLATFORM_WIN32, + "cocoa" => PLATFORM_COCOA, + "wayland" => PLATFORM_WAYLAND, + "x11" => PLATFORM_X11, + "null" => PLATFORM_NULL, + "any" => ANY_PLATFORM, +) + +# Used when neither the caller nor JULIA_GLFW_PLATFORM names a platform. The +# Wayland backend is not preferred by default, see #238. +const OS_DEFAULT_PLATFORM = @static Sys.islinux() ? PLATFORM_X11 : ANY_PLATFORM + +""" + RequestedPlatform() + +The platform named by the `JULIA_GLFW_PLATFORM` environment variable, or +`nothing` if it is unset. An unrecognized name warns and yields `ANY_PLATFORM`. +""" +function RequestedPlatform()::Union{Nothing, Platform} + name = get(ENV, "JULIA_GLFW_PLATFORM", nothing) + isnothing(name) && return nothing + platform = get(PLATFORM_BY_NAME, name, nothing) + if isnothing(platform) + @warn "Ignoring unrecognized JULIA_GLFW_PLATFORM=$(repr(name)), letting GLFW choose a " * + "platform. Valid values are $(join(map(repr, sort!(collect(keys(PLATFORM_BY_NAME)))), ", "))." return ANY_PLATFORM end + return platform +end + +""" + Init(; platform = something(RequestedPlatform(), OS_DEFAULT_PLATFORM)) + +Initialize GLFW on `platform` and return whether it succeeded. What went wrong +is reported to the error callback, which throws by default. +""" +function Init(; platform::Platform = something(RequestedPlatform(), OS_DEFAULT_PLATFORM)) + require_main_thread() + InitHint(PLATFORM, platform) + INITIALIZED[] = Bool(ccall((:glfwInit, libglfw), Cint, ())) + return INITIALIZED[] end function Terminate() @@ -598,6 +615,9 @@ const _window_callbacks = Dict{Window, Ref{Vector{Callback}}}() function CreateWindow(width::Integer, height::Integer, title::AbstractString, monitor::Monitor=Monitor(C_NULL), share::Window=Window(C_NULL)) require_main_thread() window = ccall((:glfwCreateWindow, libglfw), Window, (Cint, Cint, Cstring, Monitor, Window), width, height, title, monitor, share) + # Details go to the error callback, which need not throw. Without this the + # null handle reaches glfwSetWindowUserPointer below and segfaults. + window.handle == C_NULL && error("glfwCreateWindow failed") callbacks = Ref{Vector{Callback}}(fill(nothing, _window_callback_num[])) _window_callbacks[window] = callbacks ccall((:glfwSetWindowUserPointer, libglfw), Cvoid, (Window, Ref{Vector{Callback}}), window, callbacks) # any thread valid diff --git a/test/runtests.jl b/test/runtests.jl index 8d18e86..634c6cf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,21 +15,73 @@ println(GLFW.GetVersionString()) @test GLFW.RELEASE == false @test false == GLFW.RELEASE +@testset "platform selection" begin + @test GLFW.PLATFORM_BY_NAME["x11"] == GLFW.PLATFORM_X11 + @test GLFW.PLATFORM_BY_NAME["null"] == GLFW.PLATFORM_NULL + + withenv("JULIA_GLFW_PLATFORM" => nothing) do + @test GLFW.RequestedPlatform() === nothing + end + withenv("JULIA_GLFW_PLATFORM" => "wayland") do + @test GLFW.RequestedPlatform() == GLFW.PLATFORM_WAYLAND + # A named platform is used alone, so failing to init it stays fatal. + @test GLFW.InitCandidates() == [GLFW.PLATFORM_WAYLAND] + end + # An unrecognized name is not a request for a specific platform. + withenv("JULIA_GLFW_PLATFORM" => "not-a-platform") do + @test (@test_logs (:warn,) match_mode = :any GLFW.RequestedPlatform()) == GLFW.ANY_PLATFORM + end + + # The NULL platform is the last resort: a platform that can actually open + # windows must be tried first, otherwise a machine where only some + # platforms work (e.g. Wayland but not X11) silently loses windowing. + candidates = withenv(GLFW.InitCandidates, "JULIA_GLFW_PLATFORM" => nothing) + @test first(candidates) == GLFW.OS_DEFAULT_PLATFORM + @test GLFW.ANY_PLATFORM in candidates + @test allunique(candidates) + if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) + @test last(candidates) == GLFW.PLATFORM_NULL + @test findfirst(==(GLFW.ANY_PLATFORM), candidates) < length(candidates) + # An explicit "any" must still get the NULL fallback appended. + any_candidates = withenv(GLFW.InitCandidates, "JULIA_GLFW_PLATFORM" => "any") + @test any_candidates == [GLFW.ANY_PLATFORM, GLFW.PLATFORM_NULL] + end +end + if Sys.islinux() - # Loading with no display must not throw: __init__ falls back to the - # NULL platform (#169, #200, #253). Checked in a subprocess so this - # process's initialized GLFW is not disturbed. - base = `$(Base.julia_cmd()) --project=$(Base.active_project()) --startup-file=no` - env = copy(ENV) - delete!(env, "DISPLAY") - delete!(env, "WAYLAND_DISPLAY") - delete!(env, "JULIA_GLFW_PLATFORM") - check = "using GLFW; print(GLFW.is_initialized() && GLFW.GetPlatform() == GLFW.PLATFORM_NULL)" - @test read(setenv(`$base -e $check`, env), String) == "true" + # Loading with no display at all must not throw: __init__ falls back to + # the NULL platform (#169, #200, #253). Checked in subprocesses so this + # process's initialized GLFW is not disturbed. XDG_RUNTIME_DIR is pointed + # at an empty directory as well, otherwise libwayland still finds its + # default socket there and we would not be testing the headless path. + base = `$(Base.julia_cmd()) --project=$(dirname(Base.active_project())) --startup-file=no` + emptydir = mktempdir() + headless = copy(ENV) + delete!(headless, "DISPLAY") + delete!(headless, "WAYLAND_DISPLAY") + delete!(headless, "JULIA_GLFW_PLATFORM") + headless["XDG_RUNTIME_DIR"] = emptydir + + loads_on_null = "using GLFW; print(GLFW.is_initialized() && GLFW.GetPlatform() == GLFW.PLATFORM_NULL)" + if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) + @test read(setenv(`$base -e $loads_on_null`, headless), String) == "true" + + # "any" is not a request for a specific platform, so it must fall back + # too rather than being treated as an explicit demand. + any_env = copy(headless) + any_env["JULIA_GLFW_PLATFORM"] = "any" + @test read(setenv(`$base -e $loads_on_null`, any_env), String) == "true" + + # An unrecognized value is ignored with a warning, not obeyed. + junk_env = copy(headless) + junk_env["JULIA_GLFW_PLATFORM"] = "not-a-platform" + @test read(setenv(`$base -e $loads_on_null`, junk_env), String) == "true" + end # An explicitly requested platform that cannot work must still fail. - env["JULIA_GLFW_PLATFORM"] = "x11" - failing = setenv(`$base -e "using GLFW"`, env) + x11_env = copy(headless) + x11_env["JULIA_GLFW_PLATFORM"] = "x11" + failing = setenv(`$base -e "using GLFW"`, x11_env) @test !success(pipeline(failing, stdout = devnull, stderr = devnull)) end From 85527f0a50333e253000e5405dac11257efa737c Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 29 Oct 2026 13:10:12 +0100 Subject: [PATCH 3/4] Support headless rendering --- Project.toml | 8 +- README.md | 45 ++++++- src/GLFW.jl | 51 +++---- src/callback.jl | 4 + src/glfw3.jl | 299 +++++++++++++++++++++++++++++++++++------ src/vulkan.jl | 34 +++-- test/headless.jl | 72 ++++++++++ test/initialization.jl | 177 ++++++++++++++++++++++++ test/runtests.jl | 20 ++- 9 files changed, 618 insertions(+), 92 deletions(-) create mode 100644 test/headless.jl create mode 100644 test/initialization.jl diff --git a/Project.toml b/Project.toml index 3867486..04a2196 100644 --- a/Project.toml +++ b/Project.toml @@ -1,14 +1,16 @@ author = ["Jay Petacat ", "Simon Danisch "] name = "GLFW" uuid = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98" -version = "3.4.7" +version = "4.0.0" [deps] GLFW_jll = "0656b61e-2033-5cc2-a64a-77c0f6c09b89" +Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" [compat] -GLFW_jll = "3.4" -julia = "1.3.0" +GLFW_jll = "3.5.1" +Libdl = "1" +julia = "1.6" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/README.md b/README.md index efe635f..04aa4d2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [Julia][julia] interface to [GLFW 3][glfw], a multi-platform library for creating windows with [OpenGL][opengl] or OpenGL ES contexts and receiving many kinds of input. GLFW has native support for Windows, OS X and many Unix-like systems using the X Window System, such as Linux and FreeBSD. +The Julia package version follows its own API changes independently of the C library version. Use `GLFW.GetVersion()` to query the C library version; the `GLFW_jll` compatibility entry in `Project.toml` specifies supported library versions. + [julia]: https://julialang.org [glfw]: https://www.glfw.org [opengl]: https://wikipedia.org/wiki/OpenGL @@ -43,13 +45,50 @@ Interface Read the [GLFW documentation][docs] for detailed instructions on how to use the library. The Julia interface is almost identical to the underlying C interface, with a few notable differences: * Clipboard (`glfwGetClipboard`, `glfwSetClipboard`) and time (`glfwGetTime`, `glfwSetTime`) functions have been omitted because Julia's standard library already supports similar functionality. -* `glfwInit` and `glfwTerminate` are called automatically using the `__init__` and `atexit` functions. While it is okay to still call them explicitly, it is redundant and not required. -* Loading the package does not throw when the default platform cannot be initialized. It first lets GLFW pick any platform that is actually available (so a machine with Wayland but no X11 still gets a usable window system), and only if that fails too does it warn and fall back to GLFW's NULL platform, so packages that depend on GLFW still load. Windows cannot be created or shown on the NULL platform; check for it with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. -* The `JULIA_GLFW_PLATFORM` environment variable (`"win32"`, `"cocoa"`, `"wayland"`, `"x11"`, `"null"`, or `"any"`) forces a specific platform. A named platform is honored as stated: if it cannot be initialized, loading the package fails rather than falling back — this is how you make a missing display fatal. `"any"` and unrecognized values do not name a platform, so they still fall back as above; an unrecognized value additionally warns. +* Importing GLFW does not initialize it or connect to a display server. The first operation that needs initialization does it automatically, including window hints, monitor queries, and Vulkan helpers. `GLFW.InitHint`, version queries, `GLFW.PlatformSupported`, and `GLFW.SetErrorCallback` remain usable before initialization. `GLFW.Terminate()` is called at exit and can also be called explicitly; later use initializes again. +* `GLFW.Init()` returns a `Bool`. Initialization failures warn by default and return `false`; implicit initialization throws if `Init()` fails. A custom error callback receives initialization errors and may itself throw. Other GLFW operations retain their default throwing error callback. Repeated `Init()` calls after success preserve the selected platform and window hints. +* Initialization first tries the OS default, then lets GLFW pick any available window system (so a machine with Wayland but no X11 still works), and finally warns and falls back to the NULL platform. Check for it with `GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. NULL has no visible windows. It supports offscreen OpenGL, and may support Vulkan headless surfaces if the driver provides `VK_EXT_headless_surface`. Ordinary offscreen Vulkan does not need a GLFW window or surface. +* On NULL, OpenGL window creation automatically tries EGL and then OSMesa. Explicit `CONTEXT_CREATION_API` hints are honored. `CLIENT_API = NO_API` never tries to create an OpenGL context. Window hints survive automatic context selection; `DefaultWindowHints()` restores automatic selection. Shared windows use the source window's context API. +* The `JULIA_GLFW_PLATFORM` environment variable (`"win32"`, `"cocoa"`, `"wayland"`, `"x11"`, `"null"`, or `"any"`) selects a platform when initialization is first needed. An explicit `GLFW.Init(platform = ...)` takes precedence. Named platforms are tried alone: `Init()` returns `false` on failure instead of falling back. `"null"` enables intentional headless use without display probing or fallback warnings. `"any"` and unrecognized values retain the fallbacks above; an unrecognized value additionally warns. +* `GLFW.GetRequiredInstanceExtensions()` returns an empty `Vector{String}` when Vulkan is available but window surface extensions are unavailable. `GLFW.CreateWindowSurface` throws on a failed `VkResult` even if the error callback does not throw. +* Initialization must happen on the main thread. Operations documented as safe on any thread remain so after initialization; explicitly initialize on the main thread before calling them from another thread. * Thread restrictions are explicitly checked and throw errors when they are not respected. This can be turned off by setting `GLFW.ENABLE_THREAD_ASSERTIONS[] = false` or by setting an environment variable of the same name. [docs]: https://www.glfw.org/docs/latest/ +Headless rendering +------------------ + +With GLFW.jl 4, importing GLFW or a Vulkan renderer does not connect to a display. +For offscreen OpenGL, create a hidden window normally: + +```julia +using GLFW +window = GLFW.Window(visible = false, resolution = (640, 480)) +# Render and read back pixels with OpenGL here. +GLFW.DestroyWindow(window) +``` + +Without a display, EGL is selected automatically and OSMesa is tried if EGL fails. +EGL can use a GPU driver or Mesa software rendering. An OpenGL implementation must +still be installed; GLFW does not supply a graphics driver. If neither EGL nor +OSMesa works, the error includes both failures. `ShowWindow` and the high-level +`Window(visible = true)` constructor report that NULL cannot show a window. + +For deterministic headless workers, set `JULIA_GLFW_PLATFORM=null` before the first +GLFW operation. This skips display probing and fallback warnings. Setting it is +optional: missing displays also fall back automatically. + +Vulkan compute and rendering into images need no GLFW initialization. Only query +`GetRequiredInstanceExtensions()` when creating a GLFW window surface; an empty +list means that surface creation is unavailable, not that offscreen Vulkan is. + +On Linux, a system Mesa EGL vendor may require newer Wayland symbols than an old +environment's bundled Wayland library provides, even for surfaceless rendering. +For example, `wl_fixes_interface` requires a recent Wayland runtime. Update the +graphics dependencies together or use a graphics environment with compatible +versions. `Libdl.dlopen("libEGL_mesa.so.0")` can expose such vendor load errors. + Footnotes --------- diff --git a/src/GLFW.jl b/src/GLFW.jl index 6ddb309..c5ddcb8 100644 --- a/src/GLFW.jl +++ b/src/GLFW.jl @@ -1,6 +1,7 @@ module GLFW using GLFW_jll +using Libdl struct ThreadAssertionError target_thread::Int @@ -35,6 +36,21 @@ macro require_main_thread(code) end) end +# Initializing GLFW is restricted to the main thread, even for functions that +# may otherwise be called from any thread once initialization has completed. +function ensure_initialized() + is_initialized() || Init() || throw(GLFWError(NOT_INITIALIZED, "GLFW initialization failed")) + return +end + +macro require_initialized(code) + esc(quote + require_main_thread() + ensure_initialized() + $code + end) +end + function GetVersion() # any thread major, minor, rev = Ref{Cint}(), Ref{Cint}(), Ref{Cint}() @@ -47,12 +63,11 @@ include("glfw3.jl") include("vulkan.jl") include("monitor_properties.jl") -# Platforms `__init__` tries, in order. A platform named by JULIA_GLFW_PLATFORM -# is used alone, so failing to initialize it is fatal. Otherwise ANY_PLATFORM +# Platforms `Init` tries, in order. An explicitly named platform +# is used alone, so failing to initialize it does not fall back. Otherwise ANY_PLATFORM # catches a working platform the default misses (e.g. Wayland without X11), and -# PLATFORM_NULL initializes without a display but cannot create windows. -function InitCandidates() - requested = RequestedPlatform() +# PLATFORM_NULL initializes without a display and has no visible windows. +function InitCandidates(requested = RequestedPlatform()) # ANY_PLATFORM asks GLFW to choose rather than naming a platform, so it # still gets the fallbacks below. GLFW never chooses PLATFORM_NULL itself. names_a_platform = !isnothing(requested) && requested !== ANY_PLATFORM @@ -64,32 +79,8 @@ function InitCandidates() end function __init__() - # Save errors that occur during initialization - errors = Vector{Exception}() - SetErrorCallback(err -> push!(errors, err)) - - candidates = InitCandidates() - try - for platform in candidates - Init(platform = platform) && break - end - finally - SetErrorCallback(throw) - end - - is_initialized() || throw(errors) # Throw fatal errors - + SetErrorCallback(throw) atexit(Terminate) - for err in errors - @warn err # Warn about any non-fatal errors that may have occurred during initialization - end - if GetPlatform() === PLATFORM_NULL && candidates != [PLATFORM_NULL] - @warn "GLFW found no usable windowing platform and fell back to its NULL platform: the " * - "package is usable, but windows cannot be created or shown. Detect this with " * - "`GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. To make this fatal instead, set the " * - "JULIA_GLFW_PLATFORM environment variable to the platform you require " * - "(e.g. \"x11\" or \"wayland\")." - end return end diff --git a/src/callback.jl b/src/callback.jl index ad9835e..75b1b1b 100644 --- a/src/callback.jl +++ b/src/callback.jl @@ -41,6 +41,8 @@ function callbackcode( # All glfwSetFooCallbacks currently in use are restricted to the main thread. # For reference the current ones are: glfwSetKeyCallback, glfwSetCharCallback, glfwSetCharModsCallback, glfwSetMouseButtonCallback, glfwSetCursorPosCallback, glfwSetCursorEnterCallback, glfwSetScrollCallback, glfwSetDropCallback, glfwSetWindowPosCallback, glfwSetWindowSizeCallback, glfwSetWindowCloseCallback, glfwSetWindowRefreshCallback, glfwSetWindowFocusCallback, glfwSetWindowIconifyCallback, glfwSetWindowMaximizeCallback, glfwSetFramebufferSizeCallback, glfwSetWindowContentScaleCallback, glfwSetJoystickCallback, glfwSetErrorCallback, glfwSetMonitorCallback + init = name == "Error" ? nothing : :(ensure_initialized()) + quote # Callback wrapper that can be passed to `cfunction` $wrapper($(callback_params...)) = ($callback_ref($(callback_args...)); return nothing) @@ -48,6 +50,7 @@ function callbackcode( # Set the callback function function $setter($(setter_param_names...), callback::Function) require_main_thread() + $init old_callback = $callback_ref $callback_ref = callback # Prevent callback function from being garbage-collected cfunptr = @cfunction($wrapper, Cvoid, $callback_param_types) @@ -58,6 +61,7 @@ function callbackcode( # Unset the callback function function $setter($(setter_param_names...), ::Nothing) require_main_thread() + $init ccall(($libsetter, libglfw), Ptr{Cvoid}, ($(setter_param_types...), Ptr{Cvoid}), $(setter_param_names...), C_NULL) old_callback = $callback_ref $callback_ref = nothing # Allow former callback function to be garbage-collected diff --git a/src/glfw3.jl b/src/glfw3.jl index 5629866..fa54ba4 100644 --- a/src/glfw3.jl +++ b/src/glfw3.jl @@ -401,6 +401,9 @@ function Window( error("Monitor needs to be nothing, int, or GLFW.Monitor. Found: $monitor") end + if visible && GetPlatform() == PLATFORM_NULL + error("GLFW's NULL platform has no display. Use `visible = false` for offscreen rendering.") + end window = CreateWindow(resolution..., String(name), Monitor(C_NULL), share) if fullscreen @@ -528,15 +531,50 @@ function RequestedPlatform()::Union{Nothing, Platform} end """ - Init(; platform = something(RequestedPlatform(), OS_DEFAULT_PLATFORM)) + Init(; platform = nothing) -> Bool + +Initialize GLFW and return whether it succeeded. Without an explicit `platform`, +honor `JULIA_GLFW_PLATFORM`, or try the OS default, any available window system, +and finally the NULL platform. A named platform is tried alone. -Initialize GLFW on `platform` and return whether it succeeded. What went wrong -is reported to the error callback, which throws by default. +Initialization errors warn by default rather than throwing. A custom error +callback receives these errors and may itself throw. Other GLFW calls still +throw by default. Calling `Init` again after success returns `true` without +changing platforms or resetting window hints. """ -function Init(; platform::Platform = something(RequestedPlatform(), OS_DEFAULT_PLATFORM)) +function Init(; platform::Union{Nothing, Platform} = nothing) require_main_thread() - InitHint(PLATFORM, platform) - INITIALIZED[] = Bool(ccall((:glfwInit, libglfw), Cint, ())) + is_initialized() && return true + candidates = isnothing(platform) ? InitCandidates() : InitCandidates(platform) + errors = GLFWError[] + callback = SetErrorCallback(err -> push!(errors, err)) + try + for candidate in candidates + InitHint(PLATFORM, candidate) + INITIALIZED[] = Bool(ccall((:glfwInit, libglfw), Cint, ())) + is_initialized() && break + end + if is_initialized() + CONTEXT_CREATION_API_HINT[] = nothing + CLIENT_API_HINT[] = OPENGL_API + end + finally + SetErrorCallback(callback) + end + for err in errors + if callback === throw + @warn err + elseif callback !== nothing + callback(err) + end + end + if is_initialized() && GetPlatform() === PLATFORM_NULL && candidates != [PLATFORM_NULL] + @warn "GLFW found no usable windowing platform and fell back to its NULL platform. " * + "Visible windows are unavailable; offscreen OpenGL and Vulkan remain usable " * + "when supported by the installed drivers. Detect this with " * + "`GLFW.GetPlatform() == GLFW.PLATFORM_NULL`. Set JULIA_GLFW_PLATFORM to a named " * + "platform to require it, or to \"null\" for intentional headless use." + end return INITIALIZED[] end @@ -548,7 +586,10 @@ end GetVersionString() = unsafe_string(ccall((:glfwGetVersionString, libglfw), Cstring, ())) # any thread -GetPlatform() = ccall((:glfwGetPlatform, libglfw), Platform, ()) # any thread +function GetPlatform() + ensure_initialized() + return ccall((:glfwGetPlatform, libglfw), Platform, ()) +end # any thread PlatformSupported(platform::Platform) = ccall((:glfwPlatformSupported, libglfw), Cint, (Platform,), platform) == 1 # any thread # Error handling @@ -557,15 +598,23 @@ PlatformSupported(platform::Platform) = ccall((:glfwPlatformSupported, libglfw), # Monitor handling function GetMonitors() require_main_thread() + ensure_initialized() count = Ref{Cint}() ptr = ccall((:glfwGetMonitors, libglfw), Ptr{Monitor}, (Ref{Cint},), count) unsafe_wrap(Array, ptr, count[]) end -GetPrimaryMonitor() = @require_main_thread ccall((:glfwGetPrimaryMonitor, libglfw), Monitor, ()) +GetPrimaryMonitor() = @require_initialized ccall((:glfwGetPrimaryMonitor, libglfw), Monitor, ()) + +function require_monitor(monitor::Monitor) + monitor.handle == C_NULL && throw(ArgumentError("No monitor is available. Monitor properties require a connected display.")) + return +end function GetMonitorPos(monitor::Monitor) require_main_thread() + ensure_initialized() + require_monitor(monitor) x, y = Ref{Cint}(), Ref{Cint}() ccall((:glfwGetMonitorPos, libglfw), Cvoid, (Monitor, Ref{Cint}, Ref{Cint}), monitor, x, y) (x=x[], y=y[]) @@ -573,6 +622,8 @@ end function GetMonitorPhysicalSize(monitor::Monitor) require_main_thread() + ensure_initialized() + require_monitor(monitor) width, height = Ref{Cint}(), Ref{Cint}() ccall((:glfwGetMonitorPhysicalSize, libglfw), Cvoid, (Monitor, Ref{Cint}, Ref{Cint}), monitor, width, height) (width=width[], height=height[]) @@ -580,6 +631,8 @@ end function GetMonitorContentScale(monitor::Monitor) require_main_thread() + ensure_initialized() + require_monitor(monitor) xscale, yscale = Ref{Cfloat}(), Ref{Cfloat}() ccall((:glfwGetMonitorContentScale, libglfw), Cvoid, (Monitor, Ref{Cfloat}, Ref{Cfloat}), monitor, xscale, yscale) (xscale=xscale[], yscale=yscale[]) @@ -587,6 +640,8 @@ end function GetMonitorName(monitor::Monitor) require_main_thread() + ensure_initialized() + require_monitor(monitor) ptr = ccall((:glfwGetMonitorName, libglfw), Cstring, (Monitor,), monitor) ptr == C_NULL && return "" return unsafe_string(ptr) @@ -596,17 +651,117 @@ end function GetVideoModes(monitor::Monitor) require_main_thread() + ensure_initialized() + require_monitor(monitor) count = Ref{Cint}() ptr = ccall((:glfwGetVideoModes, libglfw), Ptr{VidMode}, (Monitor, Ref{Cint}), monitor, count) unsafe_wrap(Array, ptr, count[]) end -GetVideoMode(monitor::Monitor) = @require_main_thread unsafe_load(ccall((:glfwGetVideoMode, libglfw), Ptr{VidMode}, (Monitor,), monitor)) -SetGamma(monitor::Monitor, gamma::Real) = @require_main_thread ccall((:glfwSetGamma, libglfw), Cvoid, (Monitor, Cfloat), monitor, gamma) +function GetVideoMode(monitor::Monitor) + require_main_thread() + ensure_initialized() + require_monitor(monitor) + ptr = ccall((:glfwGetVideoMode, libglfw), Ptr{VidMode}, (Monitor,), monitor) + ptr == C_NULL && error("glfwGetVideoMode failed") + return unsafe_load(ptr) +end + +function SetGamma(monitor::Monitor, gamma::Real) + require_main_thread() + ensure_initialized() + require_monitor(monitor) + ccall((:glfwSetGamma, libglfw), Cvoid, (Monitor, Cfloat), monitor, gamma) + return +end # Window handling -DefaultWindowHints() = @require_main_thread ccall((:glfwDefaultWindowHints, libglfw), Cvoid, ()) -WindowHint(target::Integer, hint::Integer) = @require_main_thread ccall((:glfwWindowHint, libglfw), Cvoid, (Cint, Cint), target, hint) +# GLFW does not expose getters for window hints. Remember explicit context API +# choices so automatic headless selection never overrides an application's hints. +const CONTEXT_CREATION_API_HINT = Ref{Union{Nothing, Cint}}(nothing) +const CLIENT_API_HINT = Ref{Cint}(OPENGL_API) + +function DefaultWindowHints() + require_main_thread() + ensure_initialized() + ccall((:glfwDefaultWindowHints, libglfw), Cvoid, ()) + CONTEXT_CREATION_API_HINT[] = nothing + CLIENT_API_HINT[] = OPENGL_API + return +end + +function WindowHint(target::Integer, hint::Integer) + require_main_thread() + ensure_initialized() + ccall((:glfwWindowHint, libglfw), Cvoid, (Cint, Cint), target, hint) + if target == CONTEXT_CREATION_API + CONTEXT_CREATION_API_HINT[] = hint + elseif target == CLIENT_API + CLIENT_API_HINT[] = hint + end + return +end + +_create_window(width, height, title, monitor, share) = + ccall((:glfwCreateWindow, libglfw), Window, (Cint, Cint, Cstring, Monitor, Window), width, height, title, monitor, share) + +# GLVND can hide a vendor's dlopen failure behind EGL_BAD_PARAMETER. Surface +# that loader error when Mesa was eligible, without overriding vendor selection +# or changing any libraries in a successful rendering process. +function egl_loader_error() + Sys.islinux() || return nothing + vendors = get(ENV, "__EGL_VENDOR_LIBRARY_FILENAMES", nothing) + !isnothing(vendors) && !occursin("mesa", lowercase(vendors)) && return nothing + try + handle = Libdl.dlopen("libEGL_mesa.so.0", Libdl.RTLD_LAZY | Libdl.RTLD_LOCAL) + Libdl.dlclose(handle) + catch err + err isa ErrorException || rethrow() + return "Mesa EGL vendor loader: " * sprint(showerror, err) * + ". Missing Wayland symbols can mean the system Mesa driver requires a newer Wayland_jll runtime." + end + return nothing +end + +function create_headless_window(width, height, title, monitor, share) + errors = GLFWError[] + apis = share.handle == C_NULL ? (EGL_CONTEXT_API, OSMESA_CONTEXT_API) : (GetWindowAttrib(share, CONTEXT_CREATION_API),) + callback = SetErrorCallback(err -> push!(errors, err)) + window = Window(C_NULL) + try + for api in apis + ccall((:glfwWindowHint, libglfw), Cvoid, (Cint, Cint), CONTEXT_CREATION_API, api) + window = _create_window(width, height, title, monitor, share) + window.handle != C_NULL && break + # Invalid arguments and allocation failures are not missing context + # implementations. Preserve these errors instead of trying another API. + any(err -> err.code in (INVALID_ENUM, INVALID_VALUE, OUT_OF_MEMORY), errors) && break + end + finally + ccall((:glfwWindowHint, libglfw), Cvoid, (Cint, Cint), CONTEXT_CREATION_API, NATIVE_CONTEXT_API) + SetErrorCallback(callback) + end + if window.handle == C_NULL + invalid = findfirst(err -> err.code in (INVALID_ENUM, INVALID_VALUE, OUT_OF_MEMORY), errors) + err = if isnothing(invalid) + attempted = length(apis) == 2 ? "EGL and OSMesa" : (first(apis) == EGL_CONTEXT_API ? "EGL" : "OSMesa") + details = join((e.description for e in errors), "; ") + if any(e -> occursin("EGL display", e.description) || occursin("initialize EGL", e.description), errors) + loader_error = egl_loader_error() + isnothing(loader_error) || (details *= "; " * loader_error) + end + GLFWError(API_UNAVAILABLE, + "Headless OpenGL context creation failed with $attempted. " * + "Install an EGL implementation with OpenGL support (a GPU driver or Mesa), " * + "or libOSMesa. " * details) + else + errors[invalid] + end + callback === nothing || callback(err) + throw(err) # A nonthrowing callback must never allow a null window handle through. + end + return window +end # Pair window handles with a callback function list # to prevent them from being garbage-collected @@ -614,7 +769,12 @@ const _window_callbacks = Dict{Window, Ref{Vector{Callback}}}() function CreateWindow(width::Integer, height::Integer, title::AbstractString, monitor::Monitor=Monitor(C_NULL), share::Window=Window(C_NULL)) require_main_thread() - window = ccall((:glfwCreateWindow, libglfw), Window, (Cint, Cint, Cstring, Monitor, Window), width, height, title, monitor, share) + ensure_initialized() + window = if GetPlatform() == PLATFORM_NULL && CLIENT_API_HINT[] != NO_API && isnothing(CONTEXT_CREATION_API_HINT[]) + create_headless_window(width, height, title, monitor, share) + else + _create_window(width, height, title, monitor, share) + end # Details go to the error callback, which need not throw. Without this the # null handle reaches glfwSetWindowUserPointer below and segfaults. window.handle == C_NULL && error("glfwCreateWindow failed") @@ -628,6 +788,7 @@ callbacks(window) = ccall((:glfwGetWindowUserPointer, libglfw), Ref{Vector{Callb function DestroyWindow(window::Window) require_main_thread() + ensure_initialized() ccall((:glfwDestroyWindow, libglfw), Cvoid, (Window,), window) delete!(_window_callbacks, window) end @@ -636,7 +797,7 @@ end WindowShouldClose(window::Window) = ccall((:glfwWindowShouldClose, libglfw), Cint, (Window,), window) != 0 SetWindowShouldClose(window::Window, value::Bool) = ccall((:glfwSetWindowShouldClose, libglfw), Cvoid, (Window, Cint), window, value) -SetWindowTitle(window::Window, title::AbstractString) = @require_main_thread ccall((:glfwSetWindowTitle, libglfw), Cvoid, (Window, Cstring), window, title) +SetWindowTitle(window::Window, title::AbstractString) = @require_initialized ccall((:glfwSetWindowTitle, libglfw), Cvoid, (Window, Cstring), window, title) """ GLFW.SetWindowIcon(window::Window, image::Matrix{NTuple{4, UInt8}}) @@ -666,6 +827,7 @@ function SetWindowIcon(window::Window, images::Vector{<:AbstractMatrix{NTuple{4, return end require_main_thread() + ensure_initialized() ccall((:glfwSetWindowIcon, libglfw), Cvoid, (Window, Cint, Ref{GLFWImage}), window, length(images), images) end @@ -675,17 +837,20 @@ function SetWindowIcon(window::Window, image::AbstractMatrix{NTuple{4,UInt8}}) return end require_main_thread() + ensure_initialized() ccall((:glfwSetWindowIcon, libglfw), Cvoid, (Window, Cint, Ref{GLFWImage}), window, 1, image) end function GetWin32Window(window::Window) require_main_thread() + ensure_initialized() HWND = ccall((:glfwGetWin32Window, libglfw), Ptr{Cvoid}, (Window,), window) return HWND end function GetWindowPos(window::Window) require_main_thread() + ensure_initialized() x, y = Ref{Cint}(), Ref{Cint}() ccall((:glfwGetWindowPos, libglfw), Cvoid, (Window, Ref{Cint}, Ref{Cint}), window, x, y) (x=x[], y=y[]) @@ -693,11 +858,13 @@ end function SetWindowPos(window::Window, x::Integer, y::Integer) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowPos, libglfw), Cvoid, (Window, Cint, Cint), window, x, y) end function GetWindowSize(window::Window) require_main_thread() + ensure_initialized() width, height = Ref{Cint}(), Ref{Cint}() ccall((:glfwGetWindowSize, libglfw), Cvoid, (Window, Ref{Cint}, Ref{Cint}), window, width, height) (width=width[], height=height[]) @@ -705,11 +872,13 @@ end function SetWindowSizeLimits(window, minwidth, minheight, maxwidth, maxheight) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowSizeLimits, libglfw), Cvoid, (Window, Cint, Cint, Cint, Cint), window, minwidth, minheight, maxwidth, maxheight) end function SetWindowAspectRatio(window, numer, denom) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowAspectRatio, libglfw), Cvoid, (Window, Cint, Cint), window, numer, denom) end @@ -717,11 +886,13 @@ SetWindowAspectRatio(window, r::Rational) = SetWindowAspectRatio(window, r.num, function SetWindowSize(window::Window, width::Integer, height::Integer) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowSize, libglfw), Cvoid, (Window, Cint, Cint), window, width, height) end function GetFramebufferSize(window::Window) require_main_thread() + ensure_initialized() width, height = Ref{Cint}(), Ref{Cint}() ccall((:glfwGetFramebufferSize, libglfw), Cvoid, (Window, Ref{Cint}, Ref{Cint}), window, width, height) (width=width[], height=height[]) @@ -729,6 +900,7 @@ end function GetWindowFrameSize(window::Window) require_main_thread() + ensure_initialized() left, top, right, bottom = Ref{Cint}(), Ref{Cint}(), Ref{Cint}(), Ref{Cint}() ccall((:glfwGetWindowFrameSize, libglfw), Cvoid, (Window, Ref{Cint}, Ref{Cint}, Ref{Cint}, Ref{Cint}), window, left, top, right, bottom) (left=left[], top=top[], right=right[], bottom=bottom[]) @@ -736,6 +908,7 @@ end function GetWindowContentScale(window::Window) require_main_thread() + ensure_initialized() xscale, yscale = Ref{Cfloat}(), Ref{Cfloat}() ccall((:glfwGetWindowContentScale, libglfw), Cvoid, (Window, Ref{Cfloat}, Ref{Cfloat}), window, xscale, yscale) (xscale=xscale[], yscale=yscale[]) @@ -743,39 +916,50 @@ end function GetWindowAttrib(window::Window, attrib::Integer) require_main_thread() + ensure_initialized() ccall((:glfwGetWindowAttrib, libglfw), Cint, (Window, Cint), window, attrib) end function SetWindowAttrib(window::Window, attrib::Integer, value::Integer) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowAttrib, libglfw), Cvoid, (Window, Cint, Cint), window, attrib, value) end function WindowHintString(hint::Integer, value::AbstractString) require_main_thread() + ensure_initialized() ccall((:glfwWindowHintString, libglfw), Cvoid, (Cint, Cstring), hint, value) end -RequestWindowAttention(window::Window) = @require_main_thread ccall((:glfwRequestWindowAttention, libglfw), Cvoid, (Window,), window) +RequestWindowAttention(window::Window) = @require_initialized ccall((:glfwRequestWindowAttention, libglfw), Cvoid, (Window,), window) -GetWindowOpacity(window::Window) = @require_main_thread ccall((:glfwGetWindowOpacity, libglfw), Cfloat, (Window,), window) +GetWindowOpacity(window::Window) = @require_initialized ccall((:glfwGetWindowOpacity, libglfw), Cfloat, (Window,), window) function SetWindowOpacity(window::Window, opacity::AbstractFloat) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowOpacity, libglfw), Cvoid, (Window, Cfloat), window, opacity) end -IconifyWindow(window::Window) = @require_main_thread ccall((:glfwIconifyWindow, libglfw), Cvoid, (Window,), window) -RestoreWindow(window::Window) = @require_main_thread ccall((:glfwRestoreWindow, libglfw), Cvoid, (Window,), window) -MaximizeWindow(window) = @require_main_thread ccall((:glfwMaximizeWindow, libglfw), Cvoid, (Window,), window) -ShowWindow(window::Window) = @require_main_thread ccall((:glfwShowWindow, libglfw), Cvoid, (Window,), window) -HideWindow(window::Window) = @require_main_thread ccall((:glfwHideWindow, libglfw), Cvoid, (Window,), window) -GetWindowMonitor(window::Window) = @require_main_thread ccall((:glfwGetWindowMonitor, libglfw), Monitor, (Window,), window) +IconifyWindow(window::Window) = @require_initialized ccall((:glfwIconifyWindow, libglfw), Cvoid, (Window,), window) +RestoreWindow(window::Window) = @require_initialized ccall((:glfwRestoreWindow, libglfw), Cvoid, (Window,), window) +MaximizeWindow(window) = @require_initialized ccall((:glfwMaximizeWindow, libglfw), Cvoid, (Window,), window) +function ShowWindow(window::Window) + require_main_thread() + ensure_initialized() + GetPlatform() == PLATFORM_NULL && error("GLFW's NULL platform has no display. Render offscreen or initialize an available display platform before showing a window.") + ccall((:glfwShowWindow, libglfw), Cvoid, (Window,), window) + return +end +HideWindow(window::Window) = @require_initialized ccall((:glfwHideWindow, libglfw), Cvoid, (Window,), window) +GetWindowMonitor(window::Window) = @require_initialized ccall((:glfwGetWindowMonitor, libglfw), Monitor, (Window,), window) # TODO: Add SetWindowMonitor variants: # - Monitor with video mode # - Nothing with size and position function SetWindowMonitor(window, monitor, xpos, ypos, width, height, refreshRate) require_main_thread() + ensure_initialized() ccall((:glfwSetWindowMonitor, libglfw), Cvoid, (Window, Monitor, Cint, Cint, Cint, Cint, Cint), window, monitor, xpos, ypos, width, height, refreshRate) end @@ -790,14 +974,18 @@ end @windowcallback WindowContentScale(window::Window, xscale::Cfloat, yscale::Cfloat) @windowcallback WindowMaximize(window::Window, maximized::Cint) -> (window, Bool(maximized)) -PollEvents() = @require_main_thread ccall((:glfwPollEvents, libglfw), Cvoid, ()) -WaitEvents() = @require_main_thread ccall((:glfwWaitEvents, libglfw), Cvoid, ()) -WaitEvents(timeout) = @require_main_thread ccall((:glfwWaitEventsTimeout, libglfw), Cvoid, (Cdouble,), timeout) -PostEmptyEvent() = ccall((:glfwPostEmptyEvent, libglfw), Cvoid, ()) # any thread valid +PollEvents() = @require_initialized ccall((:glfwPollEvents, libglfw), Cvoid, ()) +WaitEvents() = @require_initialized ccall((:glfwWaitEvents, libglfw), Cvoid, ()) +WaitEvents(timeout) = @require_initialized ccall((:glfwWaitEventsTimeout, libglfw), Cvoid, (Cdouble,), timeout) +function PostEmptyEvent() + ensure_initialized() + return ccall((:glfwPostEmptyEvent, libglfw), Cvoid, ()) +end # any thread valid # Input handling function GetInputMode(window::Window, mode::Integer) require_main_thread() + ensure_initialized() value = ccall((:glfwGetInputMode, libglfw), Cint, (Window, Cint), window, mode) if mode in (STICKY_KEYS, STICKY_MOUSE_BUTTONS) value = Bool(value) @@ -807,37 +995,41 @@ end function SetInputMode(window::Window, mode::Integer, value::Integer) require_main_thread() + ensure_initialized() ccall((:glfwSetInputMode, libglfw), Cvoid, (Window, Cint, Cint), window, mode, value) end function GetKeyName(key, scancode=0) require_main_thread() + ensure_initialized() ptr = ccall((:glfwGetKeyName, libglfw), Cstring, (Cint, Cint), key, scancode) if ptr != C_NULL unsafe_string(ptr) end end -GetKey(window::Window, key) = @require_main_thread Bool(ccall((:glfwGetKey, libglfw), Cint, (Window, Cint), window, key)) -GetMouseButton(window::Window, button) = @require_main_thread Bool(ccall((:glfwGetMouseButton, libglfw), Cint, (Window, Cint), window, button)) +GetKey(window::Window, key) = @require_initialized Bool(ccall((:glfwGetKey, libglfw), Cint, (Window, Cint), window, key)) +GetMouseButton(window::Window, button) = @require_initialized Bool(ccall((:glfwGetMouseButton, libglfw), Cint, (Window, Cint), window, button)) function GetCursorPos(window::Window) require_main_thread() + ensure_initialized() x, y = Ref{Cdouble}(), Ref{Cdouble}() ccall((:glfwGetCursorPos, libglfw), Cvoid, (Window, Ref{Cdouble}, Ref{Cdouble}), window, x, y) (x=x[], y=y[]) end -SetCursorPos(window::Window, x::Real, y::Real) = @require_main_thread ccall((:glfwSetCursorPos, libglfw), Cvoid, (Window, Cdouble, Cdouble), window, x, y) +SetCursorPos(window::Window, x::Real, y::Real) = @require_initialized ccall((:glfwSetCursorPos, libglfw), Cvoid, (Window, Cdouble, Cdouble), window, x, y) function CreateCursor(image::AbstractMatrix{NTuple{4,UInt8}}, hotspot) require_main_thread() + ensure_initialized() ccall((:glfwCreateCursor, libglfw), Cursor, (Ref{GLFWImage}, Cint, Cint), image, hotspot[1], hotspot[2]) end -CreateStandardCursor(shape::StandardCursorShape) = @require_main_thread ccall((:glfwCreateStandardCursor, libglfw), Cursor, (Cint,), shape) -DestroyCursor(cursor::Cursor) = @require_main_thread ccall((:glfwDestroyCursor, libglfw), Cvoid, (Cursor,), cursor) -SetCursor(window::Window, cursor::Cursor) = @require_main_thread ccall((:glfwSetCursor, libglfw), Cvoid, (Window, Cursor), window, cursor) +CreateStandardCursor(shape::StandardCursorShape) = @require_initialized ccall((:glfwCreateStandardCursor, libglfw), Cursor, (Cint,), shape) +DestroyCursor(cursor::Cursor) = @require_initialized ccall((:glfwDestroyCursor, libglfw), Cvoid, (Cursor,), cursor) +SetCursor(window::Window, cursor::Cursor) = @require_initialized ccall((:glfwSetCursor, libglfw), Cvoid, (Window, Cursor), window, cursor) SetCursor(window::Window, ::Nothing) = SetCursor(window, Cursor(C_NULL)) @windowcallback Key(window::Window, key::Key, scancode::Cint, action::Action, mods::Cint) @windowcallback Char(window::Window, codepoint::Cuint) -> (window, convert(Char, codepoint)) @@ -847,10 +1039,11 @@ SetCursor(window::Window, ::Nothing) = SetCursor(window, Cursor(C_NULL)) @windowcallback CursorEnter(window::Window, entered::Cint) -> (window, Bool(entered)) @windowcallback Scroll(window::Window, xoffset::Cdouble, yoffset::Cdouble) @windowcallback Drop(window::Window, count::Cint, paths::Ptr{Cstring}) -> (window, map(unsafe_string, unsafe_wrap(Array, paths, count))) -JoystickPresent(joy::Joystick) = @require_main_thread Bool(ccall((:glfwJoystickPresent, libglfw), Cint, (Cint,), joy)) +JoystickPresent(joy::Joystick) = @require_initialized Bool(ccall((:glfwJoystickPresent, libglfw), Cint, (Cint,), joy)) function GetJoystickAxes(joy::Joystick) require_main_thread() + ensure_initialized() count = Ref{Cint}() ptr = ccall((:glfwGetJoystickAxes, libglfw), Ptr{Cfloat}, (Cint, Ref{Cint}), joy, count) if ptr != C_NULL @@ -860,6 +1053,7 @@ end function GetJoystickButtons(joy::Joystick) require_main_thread() + ensure_initialized() count = Ref{Cint}() ptr = ccall((:glfwGetJoystickButtons, libglfw), Ptr{Cuchar}, (Cint, Ref{Cint}), joy, count) if ptr != C_NULL @@ -869,6 +1063,7 @@ end function GetJoystickName(joy::Joystick) require_main_thread() + ensure_initialized() ptr = ccall((:glfwGetJoystickName, libglfw), Cstring, (Cint,), joy) if ptr != C_NULL unsafe_string(ptr) @@ -878,12 +1073,30 @@ end @callback Joystick(joy::Joystick, event::DeviceConfigEvent) # Context handling -MakeContextCurrent(window::Window) = ccall((:glfwMakeContextCurrent, libglfw), Cvoid, (Window,), window) # any thread -GetCurrentContext() = Base.cconvert(Window, ccall((:glfwGetCurrentContext, libglfw), Window, ())) # any thread -SwapBuffers(window::Window) = ccall((:glfwSwapBuffers, libglfw), Cvoid, (Window,), window) # any thread according to GLFW docs -SwapInterval(interval::Integer) = ccall((:glfwSwapInterval, libglfw), Cvoid, (Cint,), interval) # any thread -ExtensionSupported(extension::AbstractString) = Bool(ccall((:glfwExtensionSupported, libglfw), Cint, (Cstring,), extension)) # any thread -GetProcAddress(procname::AbstractString) = ccall((:glfwGetProcAddress, libglfw), Ptr{Cvoid}, (Cstring,), procname) # any thread +function MakeContextCurrent(window::Window) + ensure_initialized() + return ccall((:glfwMakeContextCurrent, libglfw), Cvoid, (Window,), window) +end # any thread +function GetCurrentContext() + ensure_initialized() + return Base.cconvert(Window, ccall((:glfwGetCurrentContext, libglfw), Window, ())) +end # any thread +function SwapBuffers(window::Window) + ensure_initialized() + return ccall((:glfwSwapBuffers, libglfw), Cvoid, (Window,), window) +end # any thread according to GLFW docs +function SwapInterval(interval::Integer) + ensure_initialized() + return ccall((:glfwSwapInterval, libglfw), Cvoid, (Cint,), interval) +end # any thread +function ExtensionSupported(extension::AbstractString) + ensure_initialized() + return Bool(ccall((:glfwExtensionSupported, libglfw), Cint, (Cstring,), extension)) +end # any thread +function GetProcAddress(procname::AbstractString) + ensure_initialized() + return ccall((:glfwGetProcAddress, libglfw), Ptr{Cvoid}, (Cstring,), procname) +end # any thread #came from GLWindow/core.jl """ @@ -897,12 +1110,14 @@ end #Came from GLWindow.jl/screen.jl """ -Takes half the resolution of the primary monitor. +Takes half the resolution of the primary monitor, or uses 800 x 600 when no monitor is available. This should make for sensible defaults! """ function standard_screen_resolution() - w, h = primarymonitorresolution() - (div(w,2), div(h,2)) # half of total resolution seems like a good fit! + monitor = GetPrimaryMonitor() + monitor.handle == C_NULL && return (800, 600) + mode = GetVideoMode(monitor) + return (div(mode.width, 2), div(mode.height, 2)) end diff --git a/src/vulkan.jl b/src/vulkan.jl index f71f4a7..d1264fe 100644 --- a/src/vulkan.jl +++ b/src/vulkan.jl @@ -1,5 +1,5 @@ # define Vulkan types locally, so we don't need to add VulkanCore.jl as dependency -const VkResult = Cuint +const VkResult = Cint const VkInstance = Ptr{Cvoid} const VkSurfaceKHR = Ptr{Cvoid} const VkPhysicalDevice = Ptr{Cvoid} @@ -16,17 +16,26 @@ end """ VulkanSupported() Return whether the Vulkan loader has been found. +This does not guarantee a usable physical device or window surface support. +Check physical devices through your Vulkan binding before constructing a renderer; +`GetRequiredInstanceExtensions` only describes GLFW window surface requirements. """ -VulkanSupported() = Bool(ccall((:glfwVulkanSupported, libglfw), Cint, ())) # any thread +function VulkanSupported() + ensure_initialized() + return Bool(ccall((:glfwVulkanSupported, libglfw), Cint, ())) +end """ GetRequiredInstanceExtensions() -> Vector{String} -Return a vector of names of Vulkan instance extensions. +Return the Vulkan instance extensions required for window surface creation. +Returns an empty vector if Vulkan is available but window surfaces are not +supported (for example, on the NULL platform). Offscreen Vulkan can still be used. """ function GetRequiredInstanceExtensions() count = Ref{Cuint}(0) ptr = GetRequiredInstanceExtensions(count) - return GC.@preserve ptr unsafe_string.(unsafe_wrap(Array, ptr, count[])) + ptr == C_NULL && return String[] + return unsafe_string.(unsafe_wrap(Array, ptr, count[])) end """ @@ -34,6 +43,7 @@ end Return a Cstring pointer that can be directly passed to `VkInstanceCreateInfo`. """ function GetRequiredInstanceExtensions(count::Ref) + ensure_initialized() ptr = ccall((:glfwGetRequiredInstanceExtensions, libglfw), Ptr{Cstring}, (Ref{Cuint},), count) # any thread end @@ -43,7 +53,8 @@ Return the address of the specified Vulkan core or extension function for the sp `funcptr` can be used directly as the first argument of `ccall`: ccall(funcptr, ...). """ function GetInstanceProcAddress(instance, procname) - # any thread + # any thread after initialization + ensure_initialized() ccall((:glfwGetInstanceProcAddress, libglfw), Ptr{Cvoid}, (VkInstance, Cstring), instance, procname) end @@ -52,17 +63,22 @@ end Return whether the specified queue family of the specified physical device supports presentation to the platform GLFW was built for. """ function GetPhysicalDevicePresentationSupport(instance, device, queuefamily) - # any thread + # any thread after initialization + ensure_initialized() Bool(ccall((:glfwGetPhysicalDevicePresentationSupport, libglfw), Cint, (VkInstance, VkPhysicalDevice, Cuint), instance, device, queuefamily)) end """ CreateWindowSurface(instance, window, allocator=C_NULL) -Create a Vulkan surface for the specified window. +Create a Vulkan surface for the specified window. Throws if the returned +`VkResult` is not `VK_SUCCESS`, even when the GLFW error callback does not throw. """ function CreateWindowSurface(instance, window, allocator=C_NULL) - # any thread + instance == C_NULL && throw(ArgumentError("CreateWindowSurface requires a valid Vulkan instance")) + # any thread after initialization + ensure_initialized() surface = Ref{VkSurfaceKHR}(C_NULL) - ccall((:glfwCreateWindowSurface, libglfw), VkResult, (VkInstance, Window, Ptr{VkAllocationCallbacks}, Ref{VkSurfaceKHR}), instance, window, allocator, surface) + result = ccall((:glfwCreateWindowSurface, libglfw), VkResult, (VkInstance, Window, Ptr{VkAllocationCallbacks}, Ref{VkSurfaceKHR}), instance, window, allocator, surface) + result == 0 || error("glfwCreateWindowSurface failed with VkResult $result") return surface[] end diff --git a/test/headless.jl b/test/headless.jl new file mode 100644 index 0000000..7015f71 --- /dev/null +++ b/test/headless.jl @@ -0,0 +1,72 @@ +@testset "automatic headless OpenGL" begin + if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) + withenv("JULIA_GLFW_PLATFORM" => "null") do + GLFW.Terminate() + for query in (GLFW.GetMonitorPos, GLFW.GetMonitorPhysicalSize, GLFW.GetMonitorContentScale, + GLFW.GetMonitorName, GLFW.GetVideoModes, GLFW.GetVideoMode, m -> GLFW.SetGamma(m, 1.0)) + @test_throws ArgumentError query(GLFW.Monitor(C_NULL)) + end + window = try + GLFW.CreateWindow(32, 32, "offscreen") + catch err + err isa GLFW.GLFWError || rethrow() + @test err.code == GLFW.API_UNAVAILABLE + @test occursin("EGL and OSMesa", err.description) + nothing + end + if !isnothing(window) + try + api = GLFW.GetWindowAttrib(window, GLFW.CONTEXT_CREATION_API) + @test api in (GLFW.EGL_CONTEXT_API, GLFW.OSMESA_CONTEXT_API) + GLFW.MakeContextCurrent(window) + clearcolor = GLFW.GetProcAddress("glClearColor") + clear = GLFW.GetProcAddress("glClear") + readpixels = GLFW.GetProcAddress("glReadPixels") + ccall(clearcolor, Cvoid, (Cfloat, Cfloat, Cfloat, Cfloat), 0.25, 0.5, 0.75, 1) + ccall(clear, Cvoid, (Cuint,), 0x4000) + pixel = zeros(UInt8, 4) + ccall(readpixels, Cvoid, (Cint, Cint, Cint, Cint, Cuint, Cuint, Ptr{UInt8}), + 0, 0, 1, 1, 0x1908, 0x1401, pixel) + @test all(abs.(Int.(pixel) .- [64, 128, 191, 255]) .<= 1) + @test_throws ErrorException GLFW.ShowWindow(window) + shared = GLFW.CreateWindow(16, 16, "shared", GLFW.Monitor(C_NULL), window) + try + @test GLFW.GetWindowAttrib(shared, GLFW.CONTEXT_CREATION_API) == api + finally + GLFW.DestroyWindow(shared) + end + # Explicit choices and DefaultWindowHints must not be lost + # while the automatic path temporarily sets the C hint. + GLFW.WindowHint(GLFW.CONTEXT_CREATION_API, api) + explicit = GLFW.CreateWindow(16, 16, "explicit") + try + @test GLFW.GetWindowAttrib(explicit, GLFW.CONTEXT_CREATION_API) == api + finally + GLFW.DestroyWindow(explicit) + end + GLFW.DefaultWindowHints() + @test GLFW.CONTEXT_CREATION_API_HINT[] === nothing + hidden = GLFW.Window(visible = false) + try + @test all(>(0), Tuple(GLFW.GetWindowSize(hidden))) + finally + GLFW.DestroyWindow(hidden) + end + finally + GLFW.DestroyWindow(window) + end + end + @test_throws GLFW.GLFWError GLFW.CreateWindow(0, 32, "invalid") + @test_throws ErrorException GLFW.Window(visible = true, resolution = (32, 32)) + GLFW.DefaultWindowHints() + GLFW.WindowHint(GLFW.CLIENT_API, GLFW.NO_API) + window = GLFW.CreateWindow(16, 16, "no OpenGL dependency") + try + @test GLFW.GetWindowAttrib(window, GLFW.CLIENT_API) == GLFW.NO_API + finally + GLFW.DestroyWindow(window) + GLFW.Terminate() + end + end + end +end diff --git a/test/initialization.jl b/test/initialization.jl new file mode 100644 index 0000000..02ddddf --- /dev/null +++ b/test/initialization.jl @@ -0,0 +1,177 @@ +@testset "lazy initialization" begin + if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) + withenv("JULIA_GLFW_PLATFORM" => "null") do + # These functions must remain usable before initialization. + GLFW.Terminate() + GLFW.InitHint(GLFW.COCOA_MENUBAR, false) + callback = err -> nothing + old = GLFW.SetErrorCallback(callback) + @test GLFW.SetErrorCallback(old) === callback + @test !GLFW.is_initialized() + @test (@test_logs GLFW.Init()) + GLFW.Terminate() + if Threads.nthreads() > 1 + # No C initialization may happen on a secondary thread. Once + # initialized, any-thread queries must still work there. + Threads.@threads for i in 1:Threads.nthreads() + if Threads.threadid() != 1 + @test_throws GLFW.ThreadAssertionError GLFW.GetPlatform() + end + end + @test !GLFW.is_initialized() + @test GLFW.Init() + Threads.@threads for i in 1:Threads.nthreads() + @test GLFW.GetPlatform() == GLFW.PLATFORM_NULL + end + end + + # Every independent first-use entry point must initialize. In + # particular, initialize before hints so glfwInit cannot reset them. + for first_use in ( + GLFW.GetPlatform, GLFW.GetMonitors, GLFW.GetPrimaryMonitor, + GLFW.DefaultWindowHints, GLFW.PollEvents, GLFW.PostEmptyEvent, + GLFW.GetCurrentContext, GLFW.VulkanSupported, + () -> GLFW.WindowHint(GLFW.CLIENT_API, GLFW.NO_API), + () -> GLFW.WindowHintString(GLFW.X11_CLASS_NAME, "test"), + () -> GLFW.SetMonitorCallback(nothing), + () -> GLFW.SetJoystickCallback(nothing), + ) + GLFW.Terminate() + first_use() + @test GLFW.is_initialized() + @test GLFW.GetPlatform() == GLFW.PLATFORM_NULL + end + + GLFW.Terminate() + GLFW.WindowHint(GLFW.CLIENT_API, GLFW.NO_API) + GLFW.WindowHint(GLFW.VISIBLE, false) + @test GLFW.Init(platform = GLFW.OS_DEFAULT_PLATFORM) + @test GLFW.GetPlatform() == GLFW.PLATFORM_NULL + window = GLFW.CreateWindow(32, 32, "headless") + try + @test GLFW.GetWindowAttrib(window, GLFW.CLIENT_API) == GLFW.NO_API + @test GLFW.GetWindowAttrib(window, GLFW.VISIBLE) == 0 + GLFW.SetWindowShouldClose(window, true) + @test GLFW.WindowShouldClose(window) + finally + GLFW.DestroyWindow(window) + end + + # The direct CreateWindow entry point initializes too. Its default + # OpenGL context may fail without OSMesa, but must never segfault if + # the error callback is disabled. + GLFW.Terminate() + old = GLFW.SetErrorCallback(nothing) + try + window = try + GLFW.CreateWindow(32, 32, "OpenGL") + catch err + @test err isa ErrorException + nothing + end + @test GLFW.is_initialized() + window === nothing || GLFW.DestroyWindow(window) + finally + GLFW.SetErrorCallback(old) + GLFW.Terminate() + end + end + end +end + +@testset "initialization failure and callbacks" begin + # A platform not compiled into this library fails on every machine, without + # depending on whether a display server happens to be running. + for platform in (GLFW.PLATFORM_WIN32, GLFW.PLATFORM_COCOA, GLFW.PLATFORM_X11, GLFW.PLATFORM_WAYLAND) + GLFW.PlatformSupported(platform) && continue + GLFW.Terminate() + @test !(@test_logs (:warn,) match_mode = :any GLFW.Init(platform = platform)) + @test !GLFW.is_initialized() + errors = GLFW.GLFWError[] + callback = err -> push!(errors, err) + old = GLFW.SetErrorCallback(callback) + try + @test !GLFW.Init(platform = platform) + @test !isempty(errors) + @test GLFW.SetErrorCallback(callback) === callback + finally + GLFW.SetErrorCallback(old) + end + # A failed explicit attempt must not prevent a later valid one. + if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) + @test GLFW.Init(platform = GLFW.PLATFORM_NULL) + @test_throws GLFW.GLFWError GLFW.WindowHint(0, 0) + end + GLFW.Terminate() + end +end + +# A real instance with no surface extensions deliberately exercises the VkResult +# failure path without passing an invalid Vulkan handle into the driver. +struct TestVkInstanceCreateInfo + sType::Cuint + pNext::Ptr{Cvoid} + flags::Cuint + pApplicationInfo::Ptr{Cvoid} + enabledLayerCount::Cuint + ppEnabledLayerNames::Ptr{Cstring} + enabledExtensionCount::Cuint + ppEnabledExtensionNames::Ptr{Cstring} +end + +@testset "Vulkan without presentation" begin + @test GLFW.VkResult === Cint + @test_throws ArgumentError GLFW.CreateWindowSurface(C_NULL, GLFW.Window(C_NULL)) + if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) + withenv("JULIA_GLFW_PLATFORM" => "null") do + GLFW.Terminate() + if GLFW.VulkanSupported() + GLFW.Terminate() + extensions = GLFW.GetRequiredInstanceExtensions() + count = Ref{Cuint}(42) + ptr = GLFW.GetRequiredInstanceExtensions(count) + @test count[] == length(extensions) + if isempty(extensions) + @test ptr == C_NULL + else + # Newer GLFW can expose VK_EXT_headless_surface on NULL + # when the Vulkan driver supports it. It is not required + # for compute, rasterization or ray tracing offscreen. + @test "VK_KHR_surface" in extensions + @test "VK_EXT_headless_surface" in extensions + end + create = GLFW.GetInstanceProcAddress(C_NULL, "vkCreateInstance") + info = TestVkInstanceCreateInfo(1, C_NULL, 0, C_NULL, 0, C_NULL, 0, C_NULL) + instance = Ref{GLFW.VkInstance}(C_NULL) + result = ccall(create, GLFW.VkResult, + (Ref{TestVkInstanceCreateInfo}, Ptr{Cvoid}, Ref{GLFW.VkInstance}), info, C_NULL, instance) + if result in (-3, -9) # VK_ERROR_INITIALIZATION_FAILED / VK_ERROR_INCOMPATIBLE_DRIVER + @info "Vulkan loader found, but no usable ICD could create an instance" result + @test_skip result == 0 + else + @test result == 0 + end + if result == 0 + destroy = GLFW.GetInstanceProcAddress(instance[], "vkDestroyInstance") + GLFW.WindowHint(GLFW.CLIENT_API, GLFW.NO_API) + window = GLFW.CreateWindow(32, 32, "Vulkan") + old = GLFW.SetErrorCallback(nothing) + try + err = try + GLFW.CreateWindowSurface(instance[], window) + catch err + err + end + @test err isa ErrorException + @test occursin("glfwCreateWindowSurface failed with VkResult -", sprint(showerror, err)) + finally + GLFW.SetErrorCallback(old) + GLFW.DestroyWindow(window) + ccall(destroy, Cvoid, (GLFW.VkInstance, Ptr{Cvoid}), instance[], C_NULL) + end + end + end + GLFW.Terminate() + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 634c6cf..c621c92 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,8 @@ using GLFW, Test +@test !GLFW.is_initialized() println(GLFW.GetVersionString()) +@test !GLFW.is_initialized() # GLFWError uses enum for known error code @test isa(GLFW.GLFWError(GLFW.NOT_INITIALIZED, "").code, GLFW.ErrorCode) @@ -49,7 +51,7 @@ println(GLFW.GetVersionString()) end if Sys.islinux() - # Loading with no display at all must not throw: __init__ falls back to + # Importing does not initialize GLFW; first use falls back to # the NULL platform (#169, #200, #253). Checked in subprocesses so this # process's initialized GLFW is not disturbed. XDG_RUNTIME_DIR is pointed # at an empty directory as well, otherwise libwayland still finds its @@ -62,7 +64,7 @@ if Sys.islinux() delete!(headless, "JULIA_GLFW_PLATFORM") headless["XDG_RUNTIME_DIR"] = emptydir - loads_on_null = "using GLFW; print(GLFW.is_initialized() && GLFW.GetPlatform() == GLFW.PLATFORM_NULL)" + loads_on_null = "using GLFW; print(!GLFW.is_initialized() && GLFW.GetPlatform() == GLFW.PLATFORM_NULL && GLFW.is_initialized())" if GLFW.PlatformSupported(GLFW.PLATFORM_NULL) @test read(setenv(`$base -e $loads_on_null`, headless), String) == "true" @@ -78,13 +80,21 @@ if Sys.islinux() @test read(setenv(`$base -e $loads_on_null`, junk_env), String) == "true" end - # An explicitly requested platform that cannot work must still fail. + # Import succeeds even with an unavailable requested platform. Explicit + # Init returns false; implicit initialization throws before using GLFW. x11_env = copy(headless) x11_env["JULIA_GLFW_PLATFORM"] = "x11" - failing = setenv(`$base -e "using GLFW"`, x11_env) + imports = setenv(`$base -e "using GLFW; print(!GLFW.is_initialized())"`, x11_env) + @test read(imports, String) == "true" + init_fails = setenv(`$base -e "using GLFW; print(!GLFW.Init() && !GLFW.is_initialized())"`, x11_env) + @test read(init_fails, String) == "true" + failing = setenv(`$base -e "using GLFW; GLFW.GetPlatform()"`, x11_env) @test !success(pipeline(failing, stdout = devnull, stderr = devnull)) end -if !haskey(ENV, "CI") # AppVeyor and Travis CI don't support OpenGL +include("initialization.jl") +include("headless.jl") + +if !haskey(ENV, "CI") && GLFW.GetPlatform() != GLFW.PLATFORM_NULL include("windowclose.jl") end From 91c6b6ca3151e4ca786973aca8cc0f53b00a00e2 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 29 Oct 2026 13:15:49 +0100 Subject: [PATCH 4/4] Fix driverless initialization test --- test/initialization.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/initialization.jl b/test/initialization.jl index 02ddddf..f09f8b1 100644 --- a/test/initialization.jl +++ b/test/initialization.jl @@ -58,7 +58,7 @@ end # The direct CreateWindow entry point initializes too. Its default - # OpenGL context may fail without OSMesa, but must never segfault if + # OpenGL context may fail without EGL or OSMesa, but must never segfault if # the error callback is disabled. GLFW.Terminate() old = GLFW.SetErrorCallback(nothing) @@ -66,7 +66,8 @@ window = try GLFW.CreateWindow(32, 32, "OpenGL") catch err - @test err isa ErrorException + @test err isa GLFW.GLFWError + @test err.code == GLFW.API_UNAVAILABLE nothing end @test GLFW.is_initialized()