Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
author = ["Jay Petacat <jay@jayschwa.net>", "Simon Danisch <sdanisch@gmail.com>"]
name = "GLFW"
uuid = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98"
version = "3.4.6"
version = "3.4.7"

[deps]
GLFW_jll = "0656b61e-2033-5cc2-a64a-77c0f6c09b89"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* 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/
Expand Down
43 changes: 33 additions & 10 deletions src/GLFW.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +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)
for platform in candidates
Init(platform = platform) && break
end
finally
SetErrorCallback(throw)
end

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
is_initialized() || 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
66 changes: 43 additions & 23 deletions src/glfw3.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,76 @@ 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 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.
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

if !haskey(ENV, "CI") # AppVeyor and Travis CI don't support OpenGL
include("windowclose.jl")
end
Loading