Skip to content

Getting Started

Aidan Cline edited this page Jan 8, 2023 · 3 revisions

Step by step

Adding and importing GLFW

Your Package.swift file should look something like this:

import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .executable(name: "My Package", targets: ["MyTarget"])
    ],
    dependencies: [
        .package(url: "https://github.com/thepotatoking55/SwiftGLFW.git", .upToNextMajor(from: "4.1.0"))
    ],
    targets: [
        .executableTarget(
            name: "MyTarget",
            dependencies: [
                .product(name: "GLFW", package: "SwiftGLFW")
            ]
        )
    ]
)

From there, you can import GLFW into your project.

import GLFW

Initializing and terminating GLFW

Before you can use most GLFW functions, the library must be initialized. On unsuccessful initialization, an error is thrown.

do {
    try GLFWSession.intialize()
} catch {
    // Initialization failed; error of type GLFWError is thrown
}

When you are done using GLFW, typically just before the application exits, you need to terminate GLFW.

GLFWSession.terminate()

This destroys any remaining windows and releases any other resources allocated by GLFW. After this call, you must initialize GLFW again before using any GLFW functions that require it.

Creating a window and context

The window and its OpenGL context are created with a single call to GLFWWindow.init.

do {
    let window = try GLFWWindow(width: 640, height: 480, title: "My Title")
} catch {
    // Window or OpenGL context creation failed
}

This creates a 640 by 480 windowed mode window with an OpenGL context. If window or OpenGL context creation fails, an error will be thrown. While window creation rarely fails, context creation depends on properly installed drivers and may fail even on machines with the necessary hardware.

By default, the OpenGL context GLFW creates may have any version. You can require a minimum OpenGL version by setting the contextVersion hint before creation. If the required minimum version is not supported on the machine, context (and window) creation fails.

do {
    GLFWWindow.hints.contextVersion = (2,0)
    let window = try GLFWWindow(width: 640, height: 480, title: "My Title")
} catch {
    // Window or context creation failed
}

The window will automatically be destroyed when no longer referenced, so keep it in a variable until it is no longer needed.

Making the OpenGL context current

Before you can use the OpenGL API, you must have a current OpenGL context.

window.context.makeCurrent()

The context will remain current until you make another context current or until the window owning the current context is destroyed.

Checking the window close flag

Each window has a flag indicating whether the window should be closed.

When the user attempts to close the window, either by pressing the close widget in the title bar or using a key combination like Alt+F4, this flag is set to true. Note that the window isn't actually closed, so you are expected to monitor this flag and either destroy the window or give some kind of feedback to the user.

while (!window.shouldClose) {
    // Keep running
}

You can be notified when the user is attempting to close the window by setting a close callback with window.shouldCloseHandler. The callback will be called immediately after the close flag has been set.

You can also set it yourself with window.close(). This can be useful if you want to interpret other kinds of input as closing the window, like for example pressing the Escape key.

Receiving input events

Each window has a large number of callbacks that can be set to receive all the various kinds of events. To receive key press and release events, create a key callback function.

window.keyInputHandler = { (window: GLFWWindow, key: Keyboard.Key, scancode: Int, state: ButtonState, modifiers: Keyboard.Modifier) -> Void in
    if key == .escape && state == .pressed {
        window.close()
    }
}

In order for event callbacks to be called when events occur, you need to process events as described below.

Rendering with OpenGL

Once you have a current OpenGL context, you can use OpenGL normally. For example, the framebuffer size needs to be retrieved for glViewport.

let size: Size = window.framebufferSize
glViewport(0, 0, GLint(size.width), GLint(size.height))

You can also set a framebuffer size callback to window.framebufferSizeChangeHandler and be notified when the size changes.

Reading the timer

To create smooth animation, a time source is needed. GLFW provides a timer that returns the number of seconds since initialization. The time source used is the most accurate on each platform and generally has micro- or nanosecond resolution.

let time: Double = GLFWSession.currentTime

Swapping buffers

GLFW windows by default use double buffering. That means that each window has two rendering buffers; a front buffer and a back buffer. The front buffer is the one being displayed and the back buffer the one you render to.

When the entire frame has been rendered, the buffers need to be swapped with one another, so the back buffer becomes the front buffer and vice versa.

window.swapBuffers()

The swap interval indicates how many frames to wait until swapping the buffers, commonly known as vsync. By default, the swap interval is zero, meaning buffer swapping will occur immediately. On fast machines, many of those frames will never be seen, as the screen is still only updated typically 60-75 times per second, so this wastes a lot of CPU and GPU cycles.

Also, because the buffers will be swapped in the middle the screen update, leading to screen tearing.

For these reasons, applications will typically want to set the swap interval to one. It can be set to higher values, but this is usually not recommended, because of the input latency it leads to.

window.context.setSwapInterval(1)

This function will fail if the context is not current.

Processing events

GLFW needs to communicate regularly with the window system both in order to receive events and to show that the application hasn't locked up. Event processing must be done regularly while you have visible windows and is normally done each frame after buffer swapping.

There are two methods for processing pending events; polling and waiting. This example will use event polling, which processes only those events that have already been received and then returns immediately.

GLFWSession.pollEvents()

This is the best choice when rendering continually, like most games do. If instead you only need to update your rendering once you have received new input, GLFWSession.waitEvents() is a better choice. It waits until at least one event has been received, putting the thread to sleep in the meantime, and then processes all received events. This saves a great deal of CPU cycles and is useful for, for example, many kinds of editing tools.

Putting it together

Now that you know how to initialize GLFW, create a window and poll for keyboard input, it's possible to create a simple program.

This program creates a 640 by 480 windowed mode window and starts a loop that clears the screen, renders a triangle and processes events until the user either presses Escape or closes the window. It uses the C interface for OpenGL, so your implementation will likely differ.

import OpenGL
import GLFW

struct Vertex {
    var x, y: GLfloat
    var r, g, b: GLfloat
}

let vertices: [Vertex] = [
    Vertex(x: -0.6, y: -0.4, r: 1.0, g: 0.0, b: 0.0),
    Vertex(x:  0.6, y: -0.4, r: 0.0, g: 1.0, b: 0.0),
    Vertex(x:  0.0, y:  0.6, r: 0.0, g: 0.0, b: 1.0),
]

let vertex_shader_text = """
#version 110
uniform mat4 MVP;
attribute vec3 vCol;
attribute vec2 vPos;
varying vec3 color;
void main() {
    gl_Position = MVP * vec4(vPos, 0.0, 1.0);
    color = vCol;
}
"""

let fragment_shader_text = """
#version 110
varying vec3 color;
void main() {
    gl_FragColor = vec4(color, 1.0);
}
"""

struct Main {
    @MainActor func main() {
        do {
            var vertex_buffer, vertex_shader, fragment_shader, program: GLuint
            var mvp_location, vpos_location, vcol_location: GLint
            
            try GLFWSession.initialize()
            
            GLFWWindow.hints.openGLVersion = .v2_0
            let window = try GLFWWindow(width: 640, height: 480, title: "Simple Example")
            
            window.keyInputHandler = { (window, key, scancode, state, mods) in
                if key == .escape && state == .pressed {
                    window.close()
                }
            }

            window.context.makeCurrent()
            
            // NOTE: OpenGL error checks have been omitted for brevity
            
            glGenBuffers(1, &vertex_buffer)
            glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer)
            glBufferData(GL_ARRAY_BUFFER, MemoryLayout<Vertex>.stride * vertices.count, vertices, GL_STATIC_DRAW)
            
            vertex_shader_text.withCString { text in
                vertex_shader = glCreateShader(GL_VERTEX_SHADER)
                glShaderSource(vertex_shader, 1, [text], nil)
                glCompileShader(vertex_shader)
            }
            
            fragment_shader_text.withCString { text in
                fragment_shader = glCreateShader(GL_FRAGMENT_SHADER)
                glShaderSource(fragment_shader, 1, [text], nil)
                glCompileShader(fragment_shader)
            }
            
            program = glCreateProgram()
            glAttachShader(program, vertex_shader)
            glAttachShader(program, fragment_shader)
            glLinkProgram(program)
            
            mvp_location = glGetUniformLocation(program, "MVP")
            vpos_location = glGetAttribLocation(program, "vPos")
            vcol_location = glGetAttribLocation(program, "vCol")
            
            glEnableVertexAttribArray(vpos_location)
            glVertexAttribPointer(vpos_location, 2, GL_FLOAT, GL_FALSE,
                                  GLsizei(MemoryLayout<Vertex>.stride), nil)
            glEnableVertexAttribArray(vcol_location);
            glVertexAttribPointer(vcol_location, 3, GL_FLOAT, GL_FALSE,
                                  GLsizei(MemoryLayout<Vertex>.stride),
                                  UnsafeRawPointer(bitPattern: MemoryLayout<Float>.stride * 2))
            
            while !window.shouldClose {
                glUseProgram(program)
                glDrawArrays(GL_TRIANGLES, 0, 3)
                
                window.swapBuffers()
                GLFWSession.pollInputEvents()
            }
            
            GLFWSession.terminate()
        } catch let error as GLFWError {
            print("Error: \(error.description ?? error.kind.description)")
        } catch {
            print("Error: \(error)")
        }
    }
}