diff --git a/Project.toml b/Project.toml index 86fcf26..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.6" +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 5f0ac10..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,11 +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. +* 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 fd0251c..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,27 +63,25 @@ include("glfw3.jl") include("vulkan.jl") include("monitor_properties.jl") -function __init__() - # Save errors that occur during initialization - errors = Vector{Exception}() - SetErrorCallback(err -> push!(errors, err)) - - try - Init() - catch err - push!(errors, err) - finally - SetErrorCallback(throw) - end +# 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 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 + names_a_platform && return Platform[requested] - 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 - end + 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__() + SetErrorCallback(throw) + atexit(Terminate) + return end 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 69af1b9..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 @@ -496,31 +499,83 @@ 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) +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 = 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. + +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::Union{Nothing, Platform} = nothing) require_main_thread() - if haskey(ENV, "JULIA_GLFW_PLATFORM") - platform = HandlePlatformSelection() + 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 - 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 - return ANY_PLATFORM + 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 function Terminate() @@ -531,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 @@ -540,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[]) @@ -556,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[]) @@ -563,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[]) @@ -570,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) @@ -579,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 @@ -597,7 +769,15 @@ 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") 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 @@ -608,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 @@ -616,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}}) @@ -646,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 @@ -655,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[]) @@ -673,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[]) @@ -685,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 @@ -697,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[]) @@ -709,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[]) @@ -716,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[]) @@ -723,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 @@ -770,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) @@ -787,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)) @@ -827,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 @@ -840,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 @@ -849,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) @@ -858,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 """ @@ -877,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..f09f8b1 --- /dev/null +++ b/test/initialization.jl @@ -0,0 +1,178 @@ +@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 EGL or 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 GLFW.GLFWError + @test err.code == GLFW.API_UNAVAILABLE + 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 a2909f3..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) @@ -15,6 +17,84 @@ println(GLFW.GetVersionString()) @test GLFW.RELEASE == false @test false == GLFW.RELEASE -if !haskey(ENV, "CI") # AppVeyor and Travis CI don't support OpenGL +@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() + # 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 + # 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 && GLFW.is_initialized())" + 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 + + # 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" + 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 + +include("initialization.jl") +include("headless.jl") + +if !haskey(ENV, "CI") && GLFW.GetPlatform() != GLFW.PLATFORM_NULL include("windowclose.jl") end