// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2002-2006 Marcus Geelnard // SPDX-FileCopyrightText: 2006-2019 Camilla Löwy // SPDX-FileCopyrightText: 2022 The Ebitengine Authors //go:build darwin || windows package goglfw import ( "fmt" "math" "regexp" "strconv" "strings" "unsafe" "github.com/ebitengine/purego" ) func checkValidContextConfig(ctxconfig *ctxconfig) error { if ctxconfig.share != nil { if ctxconfig.client == NoAPI || ctxconfig.share.context.client == NoAPI { return NoWindowContext } } if ctxconfig.source != NativeContextAPI && ctxconfig.source != EGLContextAPI && ctxconfig.source != OSMesaContextAPI { return fmt.Errorf("goglfw: invalid context creation API 0x%08X: %w", ctxconfig.source, InvalidEnum) } if ctxconfig.client != NoAPI && ctxconfig.client != OpenGLAPI && ctxconfig.client != OpenGLESAPI { return fmt.Errorf("goglfw: invalid client API 0x%08X: %w", ctxconfig.client, InvalidEnum) } if ctxconfig.client == OpenGLAPI { if (ctxconfig.major < 1 || ctxconfig.minor < 0) || (ctxconfig.major == 1 && ctxconfig.minor > 5) || (ctxconfig.major == 2 && ctxconfig.minor > 1) || (ctxconfig.major == 3 && ctxconfig.minor > 3) { // OpenGL 1.0 is the smallest valid version // OpenGL 1.x series ended with version 1.5 // OpenGL 2.x series ended with version 2.1 // OpenGL 3.x series ended with version 3.3 // For now, let everything else through return fmt.Errorf("goglfw: invalid OpenGL version %d.%d: %w", ctxconfig.major, ctxconfig.minor, InvalidValue) } if ctxconfig.profile != 0 { if ctxconfig.profile != OpenGLCoreProfile && ctxconfig.profile != OpenGLCompatProfile { return fmt.Errorf("goglfw: invalid OpenGL profile 0x%08X: %w", ctxconfig.profile, InvalidEnum) } if ctxconfig.major <= 2 || (ctxconfig.major == 3 && ctxconfig.minor < 2) { // Desktop OpenGL context profiles are only defined for version 3.2 // and above return fmt.Errorf("goglfw: context profiles are only defined for OpenGL version 3.2 and above: %w", InvalidValue) } } if ctxconfig.forward && ctxconfig.major <= 2 { // Forward-compatible contexts are only defined for OpenGL version 3.0 and above return fmt.Errorf("goglfw: forward-compatibility is only defined for OpenGL version 3.0 and above: %w", InvalidValue) } } else if ctxconfig.client == OpenGLESAPI { if ctxconfig.major < 1 || ctxconfig.minor < 0 || (ctxconfig.major == 1 && ctxconfig.minor > 1) || (ctxconfig.major == 2 && ctxconfig.minor > 0) { // OpenGL ES 1.0 is the smallest valid version // OpenGL ES 1.x series ended with version 1.1 // OpenGL ES 2.x series ended with version 2.0 // For now, let everything else through return fmt.Errorf("goglfw: invalid OpenGL ES version %d.%d: %w", ctxconfig.major, ctxconfig.minor, InvalidValue) } } if ctxconfig.robustness != 0 { if ctxconfig.robustness != NoResetNotification && ctxconfig.robustness != LoseContextOnReset { return fmt.Errorf("goglfw: invalid context robustness mode 0x%08X: %w", ctxconfig.robustness, InvalidEnum) } } if ctxconfig.release != 0 { if ctxconfig.release != ReleaseBehaviorNone && ctxconfig.release != ReleaseBehaviorFlush { return fmt.Errorf("goglfw: invalid context release behavior 0x%08X: %w", ctxconfig.release, InvalidEnum) } } return nil } func chooseFBConfig(desired *fbconfig, alternatives []*fbconfig) *fbconfig { leastMissing := math.MaxInt32 leastColorDiff := math.MaxInt32 leastExtraDiff := math.MaxInt32 var closest *fbconfig for _, current := range alternatives { if desired.stereo && !current.stereo { // Stereo is a hard constraint continue } // Count number of missing buffers missing := 0 if desired.alphaBits > 0 && current.alphaBits == 0 { missing++ } if desired.depthBits > 0 && current.depthBits == 0 { missing++ } if desired.stencilBits > 0 && current.stencilBits == 0 { missing++ } if desired.auxBuffers > 0 && current.auxBuffers < desired.auxBuffers { missing += desired.auxBuffers - current.auxBuffers } if desired.samples > 0 && current.samples == 0 { // Technically, several multisampling buffers could be // involved, but that's a lower level implementation detail and // not important to us here, so we count them as one missing++ } if desired.transparent != current.transparent { missing++ } // These polynomials make many small channel size differences matter // less than one large channel size difference // Calculate color channel size difference value colorDiff := 0 if desired.redBits != DontCare { colorDiff += (desired.redBits - current.redBits) * (desired.redBits - current.redBits) } if desired.greenBits != DontCare { colorDiff += (desired.greenBits - current.greenBits) * (desired.greenBits - current.greenBits) } if desired.blueBits != DontCare { colorDiff += (desired.blueBits - current.blueBits) * (desired.blueBits - current.blueBits) } // Calculate non-color channel size difference value extraDiff := 0 if desired.alphaBits != DontCare { extraDiff += (desired.alphaBits - current.alphaBits) * (desired.alphaBits - current.alphaBits) } if desired.depthBits != DontCare { extraDiff += (desired.depthBits - current.depthBits) * (desired.depthBits - current.depthBits) } if desired.stencilBits != DontCare { extraDiff += (desired.stencilBits - current.stencilBits) * (desired.stencilBits - current.stencilBits) } if desired.accumRedBits != DontCare { extraDiff += (desired.accumRedBits - current.accumRedBits) * (desired.accumRedBits - current.accumRedBits) } if desired.accumGreenBits != DontCare { extraDiff += (desired.accumGreenBits - current.accumGreenBits) * (desired.accumGreenBits - current.accumGreenBits) } if desired.accumBlueBits != DontCare { extraDiff += (desired.accumBlueBits - current.accumBlueBits) * (desired.accumBlueBits - current.accumBlueBits) } if desired.accumAlphaBits != DontCare { extraDiff += (desired.accumAlphaBits - current.accumAlphaBits) * (desired.accumAlphaBits - current.accumAlphaBits) } if desired.samples != DontCare { extraDiff += (desired.samples - current.samples) * (desired.samples - current.samples) } if desired.sRGB && !current.sRGB { extraDiff++ } // Figure out if the current one is better than the best one found so far // Least number of missing buffers is the most important heuristic, // then color buffer size match and lastly size match for other buffers if missing < leastMissing { closest = current } else if missing == leastMissing { if (colorDiff < leastColorDiff) || (colorDiff == leastColorDiff && extraDiff < leastExtraDiff) { closest = current } } if current == closest { leastMissing = missing leastColorDiff = colorDiff leastExtraDiff = extraDiff } } return closest } func (w *Window) refreshContextAttribs(ctxconfig *ctxconfig) (ferr error) { const ( GL_COLOR_BUFFER_BIT = 0x00004000 GL_CONTEXT_COMPATIBILITY_PROFILE_BIT = 0x00000002 GL_CONTEXT_CORE_PROFILE_BIT = 0x00000001 GL_CONTEXT_FLAG_DEBUG_BIT = 0x00000002 GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT = 0x00000001 GL_CONTEXT_FLAG_NO_ERROR_BIT_KHR = 0x00000008 GL_CONTEXT_FLAGS = 0x821E GL_CONTEXT_PROFILE_MASK = 0x9126 GL_CONTEXT_RELEASE_BEHAVIOR = 0x82FB GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH = 0x82FC GL_LOSE_CONTEXT_ON_RESET_ARB = 0x8252 GL_NO_RESET_NOTIFICATION_ARB = 0x8261 GL_NONE = 0 GL_RESET_NOTIFICATION_STRATEGY_ARB = 0x8256 GL_VERSION = 0x1F02 ) w.context.source = ctxconfig.source w.context.client = OpenGLAPI p, err := _glfw.contextSlot.get() if err != nil { return err } previous := (*Window)(unsafe.Pointer(p)) defer func() { err := previous.MakeContextCurrent() if ferr == nil { ferr = err } }() if err := w.MakeContextCurrent(); err != nil { return err } getIntegerv := w.context.getProcAddress("glGetIntegerv") getString := w.context.getProcAddress("glGetString") if getIntegerv == 0 || getString == 0 { return fmt.Errorf("goglfw: entry point retrieval is broken: %w", PlatformError) } r, _, _ := purego.SyscallN(getString, GL_VERSION) version := bytePtrToString((*byte)(unsafe.Pointer(r))) if version == "" { if ctxconfig.client == OpenGLAPI { return fmt.Errorf("goglfw: OpenGL version string retrieval is broken: %w", PlatformError) } else { return fmt.Errorf("goglfw: OpenGL ES version string retrieval is broken: %w", PlatformError) } } for _, prefix := range []string{ "OpenGL ES-CM ", "OpenGL ES-CL ", "OpenGL ES "} { if strings.HasPrefix(version, prefix) { version = version[len(prefix):] w.context.client = OpenGLESAPI break } } m := regexp.MustCompile(`^(\d+)(\.(\d+)(\.(\d+))?)?`).FindStringSubmatch(version) if m == nil { if w.context.client == OpenGLAPI { return fmt.Errorf("goglfw: no version found in OpenGL version string: %w", PlatformError) } else { return fmt.Errorf("goglfw: no version found in OpenGL ES version string: %w", PlatformError) } } w.context.major, _ = strconv.Atoi(m[1]) w.context.minor, _ = strconv.Atoi(m[3]) w.context.revision, _ = strconv.Atoi(m[5]) if w.context.major < ctxconfig.major || (w.context.major == ctxconfig.major && w.context.minor < ctxconfig.minor) { // The desired OpenGL version is greater than the actual version // This only happens if the machine lacks {GLX|WGL}_ARB_create_context // /and/ the user has requested an OpenGL version greater than 1.0 // For API consistency, we emulate the behavior of the // {GLX|WGL}_ARB_create_context extension and fail here if w.context.client == OpenGLAPI { return fmt.Errorf("goglfw: requested OpenGL version %d.%d, got version %d.%d: %w", ctxconfig.major, ctxconfig.minor, w.context.major, w.context.minor, VersionUnavailable) } else { return fmt.Errorf("goglfw: requested OpenGL ES version %d.%d, got version %d.%d: %w", ctxconfig.major, ctxconfig.minor, w.context.major, w.context.minor, VersionUnavailable) } } if w.context.major >= 3 { // OpenGL 3.0+ uses a different function for extension string retrieval // We cache it here instead of in glfwExtensionSupported mostly to alert // users as early as possible that their build may be broken glGetStringi := w.context.getProcAddress("glGetStringi") if glGetStringi == 0 { return fmt.Errorf("goglfw: entry point retrieval is broken: %w", PlatformError) } } if w.context.client == OpenGLAPI { // Read back context flags (OpenGL 3.0 and above) if w.context.major >= 3 { var flags int32 _, _, _ = purego.SyscallN(getIntegerv, GL_CONTEXT_FLAGS, uintptr(unsafe.Pointer(&flags))) if flags&GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT != 0 { w.context.forward = true } if flags&GL_CONTEXT_FLAG_DEBUG_BIT != 0 { w.context.debug = true } else { ok, err := ExtensionSupported("GL_ARB_debug_output") if err != nil { return err } if ok && ctxconfig.debug { // HACK: This is a workaround for older drivers (pre KHR_debug) // not setting the debug bit in the context flags for // debug contexts w.context.debug = true } } if flags&GL_CONTEXT_FLAG_NO_ERROR_BIT_KHR != 0 { w.context.noerror = true } } // Read back OpenGL context profile (OpenGL 3.2 and above) if w.context.major >= 4 || (w.context.major == 3 && w.context.minor >= 2) { var mask int32 _, _, _ = purego.SyscallN(getIntegerv, GL_CONTEXT_PROFILE_MASK, uintptr(unsafe.Pointer(&mask))) if mask&GL_CONTEXT_COMPATIBILITY_PROFILE_BIT != 0 { w.context.profile = OpenGLCompatProfile } else if mask&GL_CONTEXT_CORE_PROFILE_BIT != 0 { w.context.profile = OpenGLCoreProfile } else { ok, err := ExtensionSupported("GL_ARB_compatibility") if err != nil { return err } if ok { // HACK: This is a workaround for the compatibility profile bit // not being set in the context flags if an OpenGL 3.2+ // context was created without having requested a specific // version w.context.profile = OpenGLCompatProfile } } } // Read back robustness strategy ok, err := ExtensionSupported("GL_ARB_robustness") if err != nil { return err } if ok { // NOTE: We avoid using the context flags for detection, as they are // only present from 3.0 while the extension applies from 1.1 var strategy int32 _, _, _ = purego.SyscallN(getIntegerv, GL_RESET_NOTIFICATION_STRATEGY_ARB, uintptr(unsafe.Pointer(&strategy))) if strategy == GL_LOSE_CONTEXT_ON_RESET_ARB { w.context.robustness = LoseContextOnReset } else if strategy == GL_NO_RESET_NOTIFICATION_ARB { w.context.robustness = NoResetNotification } } } else { // Read back robustness strategy ok, err := ExtensionSupported("GL_EXT_robustness") if err != nil { return err } if ok { // NOTE: The values of these constants match those of the OpenGL ARB // one, so we can reuse them here var strategy int32 _, _, _ = purego.SyscallN(getIntegerv, GL_RESET_NOTIFICATION_STRATEGY_ARB, uintptr(unsafe.Pointer(&strategy))) if strategy == GL_LOSE_CONTEXT_ON_RESET_ARB { w.context.robustness = LoseContextOnReset } else if strategy == GL_NO_RESET_NOTIFICATION_ARB { w.context.robustness = NoResetNotification } } } ok, err := ExtensionSupported("GL_KHR_context_flush_control") if err != nil { return err } if ok { var behavior int32 _, _, _ = purego.SyscallN(getIntegerv, GL_CONTEXT_RELEASE_BEHAVIOR, uintptr(unsafe.Pointer(&behavior))) if behavior == GL_NONE { w.context.release = ReleaseBehaviorNone } else if behavior == GL_CONTEXT_RELEASE_BEHAVIOR_FLUSH { w.context.release = ReleaseBehaviorFlush } } // Clearing the front buffer to black to avoid garbage pixels left over from // previous uses of our bit of VRAM glClear := w.context.getProcAddress("glClear") _, _, _ = purego.SyscallN(glClear, GL_COLOR_BUFFER_BIT) if w.doublebuffer { if err := w.context.swapBuffers(w); err != nil { return err } } return nil } func (w *Window) MakeContextCurrent() error { if !_glfw.initialized { return NotInitialized } ptr, err := _glfw.contextSlot.get() if err != nil { return err } previous := (*Window)(unsafe.Pointer(ptr)) if w != nil && w.context.client == NoAPI { return fmt.Errorf("goglfw: cannot make current with a window that has no OpenGL or OpenGL ES context: %w", NoWindowContext) } if previous != nil { if w == nil || w.context.source != previous.context.source { if err := previous.context.makeCurrent(nil); err != nil { return err } } } if w != nil { if err := w.context.makeCurrent(w); err != nil { return err } } return nil } func GetCurrentContext() (*Window, error) { if !_glfw.initialized { return nil, NotInitialized } ptr, err := _glfw.contextSlot.get() if err != nil { return nil, err } return (*Window)(unsafe.Pointer(ptr)), nil } func (w *Window) SwapBuffers() error { if !_glfw.initialized { return NotInitialized } if w.context.client == NoAPI { return fmt.Errorf("goglfw: cannot swap buffers of a window that has no OpenGL or OpenGL ES context: %w", NoWindowContext) } if err := w.context.swapBuffers(w); err != nil { return err } return nil } func SwapInterval(interval int) error { if !_glfw.initialized { return NotInitialized } ptr, err := _glfw.contextSlot.get() if err != nil { return err } window := (*Window)(unsafe.Pointer(ptr)) if window == nil { return fmt.Errorf("goglfw: cannot set swap interval without a current OpenGL or OpenGL ES context %w", NoCurrentContext) } if err := window.context.swapInterval(interval); err != nil { return err } return nil } func ExtensionSupported(extension string) (bool, error) { const ( GL_EXTENSIONS = 0x1F03 GL_NUM_EXTENSIONS = 0x821D ) if !_glfw.initialized { return false, NotInitialized } ptr, err := _glfw.contextSlot.get() if err != nil { return false, err } window := (*Window)(unsafe.Pointer(ptr)) if window == nil { return false, fmt.Errorf("goglfw: cannot query extension without a current OpenGL or OpenGL ES context %w", NoCurrentContext) } if window.context.major >= 3 { // Check if extension is in the modern OpenGL extensions string list glGetIntegerv := window.context.getProcAddress("glGetIntegerv") var count int32 _, _, _ = purego.SyscallN(glGetIntegerv, GL_NUM_EXTENSIONS, uintptr(unsafe.Pointer(&count))) glGetStringi := window.context.getProcAddress("glGetStringi") for i := 0; i < int(count); i++ { r, _, _ := purego.SyscallN(glGetStringi, GL_EXTENSIONS, uintptr(i)) if r == 0 { return false, fmt.Errorf("goglfw: extension string retrieval is broken: %w", PlatformError) } en := bytePtrToString((*byte)(unsafe.Pointer(r))) if en == extension { return true, nil } } } else { // Check if extension is in the old style OpenGL extensions string glGetString := window.context.getProcAddress("glGetString") r, _, _ := purego.SyscallN(glGetString, GL_EXTENSIONS) if r == 0 { return false, fmt.Errorf("goglfw: extension string retrieval is broken: %w", PlatformError) } extensions := bytePtrToString((*byte)(unsafe.Pointer(r))) for _, str := range strings.Split(extensions, " ") { if str == extension { return true, nil } } } // Check if extension is in the platform-specific string return window.context.extensionSupported(extension), nil } // bytePtrToString takes a pointer to a sequence of text and returns the corresponding string. // If the pointer is nil, it returns the empty string. It assumes that the text sequence is // terminated at a zero byte; if the zero byte is not present, the program may crash. // It is copied from golang.org/x/sys/windows/syscall.go for use on macOS, Linux and Windows func bytePtrToString(p *byte) string { if p == nil { return "" } if *p == 0 { return "" } // Find NUL terminator. n := 0 for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ { ptr = unsafe.Add(ptr, 1) } // unsafe.String(p, n) is available as of Go 1.20. return string(unsafe.Slice(p, n)) }