SDL

Simple DirectMedia Layer (SDL) is an API for cross-platform low-level access to graphics, audio, and input hardware. The library is written in C, but bindings for various programming languages are available. Two major versions of SDL exist:

SDL 1.2
The original SDL library, created by Sam Lantinga in 1998, that provides video, audio, joystick, and CD-ROM access, as well as a threading sub-system. The 2D renderer is software-based only. An OpenGL context is available.
SDL 2.0
This version adds a hardware-accelerated renderer for 2D graphics to the library, among other improvements. The API is not backwards-compatible to SDL 1.2.

For both, SDL 1.2 and 2.0, additional libraries are available that extend the core functionality: SDL_image adds support for multiple image formats, SDL_mixer for sound mixing, or SDL_ttf for TrueType font rendering.

The SDL 77 library is a simple layer around SDL 1.2 that allows 2D graphics rendering, image loading, and audio output from FORTRAN 77. The fortran-sdl2 library includes modern ISO C binding interfaces to SDL 2.0 and OpenGL 1.3 for 2D and 3D game programming in Fortran 2008.

SDL 1.2

The SDL 77 library is compatible to all platforms that have at least a FORTRAN 77 compiler available and are supported by SDL 1.2. Common Fortran/C calling conventions are used for mixed-language programming, which are followed by most Fortran compilers. The packages SDL 1.2, SDL_image 1.2, and SDL_mixer 1.2 have to be present. On FreeBSD, install them with:

# pkg install audio/sdl_mixer devel/sdl12 graphics/sdl_image

On Linux, additional development headers are required as well. Just clone the Git repository and execute the included Makefile:

$ git clone https://github.com/interkosmos/sdl77
$ cd sdl77/
$ make

This outputs the static library libSDL77.a. Link your Fortran application against libSDL77.a -lSDL -lSDL_image -lSDL_mixer. If preferred, the SDL 77 library may be built without dependencies to SDL_image and/or SDL_mixer.

Fortran rectangles
Fig. 1: SDL 1.2 software rendering from Fortran

Software Renderer

We can call the SDL 77 library procedures either from FORTRAN 77 or Fortran ≥ 90. The external subroutines should be imported with the external statement first, functions with their return type. Character strings passed to SDL 77 procedures must be null-terminated, using achar(0) or c_null_char from the intrinsic module iso_c_binding.

The following example in Fortran 2018 fills rectangles at random position and in random colour on screen (fig. 1). Press the escape key to quit the program.

! rect.f90
program rect
    implicit none (type, external)
    ! Import SDL 77 procedures.
    external :: gclose, gcur, gdelay, gevent, gflush, gopen
    integer  :: gkey

    ! SDL 1.2 events and key codes.
    integer, parameter :: EVENT_QUIT = 12
    integer, parameter :: KEY_ESC    = 27

    ! Minimum and maximum rectangle sizes.
    integer, parameter :: MAX_W = 200
    integer, parameter :: MAX_H = 200
    integer, parameter :: MIN_W = 10
    integer, parameter :: MIN_H = 10

    ! Frame delay in msec. and number of rectangles per frame.
    integer, parameter :: DELAY  = 20
    integer, parameter :: NRECTS = 5

    ! Window size.
    integer, parameter :: WIN_WIDTH  = 640
    integer, parameter :: WIN_HEIGHT = 480

    integer :: event, i, stat
    logical :: done = .false.

    ! Seed PRNG, open SDL 1.2 window and disable cursor.
    call random_seed()
    call gopen(WIN_WIDTH, WIN_HEIGHT, 'SDL 77' // achar(0), stat)
    if (stat /= 0) stop 'Error: failed to open SDL 1.2 window'
    call gcur(0)

    ! Main loop.
    do while (.not. done)
        ! Event loop.
        do
            call gevent(event, stat)
            if (event == EVENT_QUIT) done = .true.
            if (stat == 0) exit
        end do

        ! Check for keyboard input.
        if (gkey(KEY_ESC) == 1) done = .true.

        ! Draw random rectangles.
        do i = 1, NRECTS
            call draw_rect()
        end do

        ! Copy screen layer to screen.
        call gflush()
        call gdelay(delay)
    end do

    ! Clean up and quit.
    call gclose()
contains
    subroutine draw_rect()
        external :: gcolor, gfillr

        integer :: x, y, w, h
        integer :: r, g, b
        real    :: p(7)

        call random_number(p)

        w = MIN_W + nint(p(1) * (MAX_W - MIN_W))
        h = MIN_H + nint(p(2) * (MAX_H - MIN_H))
        x = nint(p(3) * (WIN_WIDTH - w))
        y = nint(p(4) * (WIN_HEIGHT - h))

        r = nint(p(5) * 255)
        g = nint(p(6) * 255)
        b = nint(p(7) * 255)

        call gcolor(r, g, b)
        call gfillr(x, y, w, h)
    end subroutine draw_rect
end program rect

Compile and link the example program with GNU Fortran by running:

$ gfortran12 -o rect rect.f90 libSDL77.a -lSDL -lSDL_image -lSDL_mixer

The command-line tool sdl-config(1) returns the platform-specific compiler flags and linker arguments:

$ gfortran12 `sdl-config --cflags` -o rect rect.f90 libSDL77.a `sdl-config --libs` -lSDL_image -lSDL_mixer
$ ./rect

SDL 2.0

The SDL 2.0 library requires different packages than SDL 1.2. Both can be installed at the same time. Packages of SDL 2.0 are available for all modern Unix-like operating systems. On FreeBSD, run:

# pkg install audio/sdl2_mixer devel/sdl20 graphics/sdl2_image graphics/sdl2_ttf

On Linux, additional development headers have to be installed again. Clone the fortran-sdl2 repository, and then build the library libsdl2.a or libfortran-sdl2.a:

$ git clone https://github.com/interkosmos/fortran-sdl2
$ cd fortran-sdl2/
$ make

We can override the default compiler (GNU Fortran) by adding parameter FC=<compiler>. Instead of GNU or BSD make, you may want to run xmake:

$ xmake

There is also rudimentary support for the Fortran Package Manager:

$ fpm build --profile=release

In this case, only a single library libfortran-sdl2.a is created, without examples.

Hardware Renderer

Calling SDL 2.0 from Fortran does not differ much from the way it is done in C. The Fortran interfaces stay close to the bound C routines. We first have to initialise SDL, create a new window, and, if hardware acceleration is desired, create a renderer. Alternatively, we still have access to the legacy software blitter known from SDL 1.2.

Fortran voxel space
Fig. 2: Software-based voxel space engine in Fortran, using SDL 2.0

The following program draws a coloured rectangle to screen. Additional demo applications are part of the fortran-sdl2 library (fig. 2). In the main loop of the example program, we first poll for I/O events which may have occured, followed by actual rendering. Before terminating the application, we have to free all allocated memory. As SDL 2.0 uses unsigned integers to store colours internally, all RGB values have to be converted using the utility function uint8().

! demo.f90
program main
    use, intrinsic :: iso_fortran_env, only: stderr => error_unit, stdout => output_unit
    use, intrinsic :: iso_c_binding, only: c_associated, c_null_char, c_ptr
    use :: sdl2
    implicit none

    integer, parameter :: SCREEN_WIDTH  = 640
    integer, parameter :: SCREEN_HEIGHT = 480

    type(c_ptr)     :: window
    type(c_ptr)     :: renderer
    type(sdl_event) :: event
    type(sdl_rect)  :: r
    integer         :: rc

    ! Initialise SDL.
    if (sdl_init(SDL_INIT_VIDEO) < 0) then
        write (stderr, *) 'SDL Error: ', sdl_get_error()
        stop
    end if

    ! Create the SDL window.
    window = sdl_create_window(title = 'Fortran SDL 2.0' // c_null_char, &
                                   x = SDL_WINDOWPOS_UNDEFINED, &
                                   y = SDL_WINDOWPOS_UNDEFINED, &
                                   w = SCREEN_WIDTH, &
                                   h = SCREEN_HEIGHT, &
                               flags = SDL_WINDOW_SHOWN)

    if (.not. c_associated(window)) then
        write (stderr, *) 'SDL Error: ', sdl_get_error()
        stop
    end if

    ! Position and size of the rectangle.
    r = sdl_rect(50, 50, 150, 150)

    ! Create the renderer.
    renderer = sdl_create_renderer(window, -1, 0)

    ! Event loop.
    do
        ! Poll events.
        do while (sdl_poll_event(event) > 0)
            select case (event%type)
                case (SDL_QUITEVENT)
                    exit
            end select
        end do

        ! Fill screen black.
        rc = sdl_set_render_draw_color(renderer, uint8(0), uint8(0), uint8(0), uint8(SDL_ALPHA_OPAQUE))
        rc = sdl_render_clear(renderer)

        ! Fill the rectangle.
        rc = sdl_set_render_draw_color(renderer, uint8(255), uint8(255), uint8(0), uint8(SDL_ALPHA_OPAQUE))
        rc = sdl_render_fill_rect(renderer, r)

        ! Render to screen and wait 20 ms.
        call sdl_render_present(renderer)
        call sdl_delay(20)
    end do

    ! Quit gracefully.
    call sdl_destroy_renderer(renderer)
    call sdl_destroy_window(window)
    call sdl_quit()
end program main

To compile, link, and run the example program, execute:

$ gfortran12 `sdl2-config --cflags` -o demo demo.f90 libsdl2.a `sdl2-config --libs`
$ ./demo

The utility program sdl2-config(1) will return the correct compiler and linker flags with respect to the currently used platform.

Fortran 3D
Fig. 3: Basic 3D rendering in Fortran with SDL 2.0 and OpenGL

OpenGL

The SDL 2.0 library offers an OpenGL rendering context that can be accessed through the Fortran interface bindings (fig. 3), as an alternative to the slightly dated GLUT. At the current stage, the bindings support only OpenGL immediate mode and a few selected GLU routines. The following example program renders a coloured triangle in orthographic projection (fig. 4):

! gl.f90
program main
    use, intrinsic :: iso_c_binding,   only: c_associated, c_null_char, c_ptr
    use, intrinsic :: iso_fortran_env, only: real64, stderr => error_unit, stdout => output_unit
    use :: sdl2
    implicit none

    integer, parameter :: SCREEN_WIDTH  = 300
    integer, parameter :: SCREEN_HEIGHT = 300

    type(c_ptr)     :: context
    type(c_ptr)     :: window
    type(sdl_event) :: event
    integer         :: rc

    ! Initialise SDL.
    if (sdl_init(SDL_INIT_EVERYTHING) < 0) then
        write (stderr, '(2a)') 'SDL Error: ', sdl_get_error()
        stop
    end if

    ! Enable multisampling for anti-aliasing.
    rc = sdl_gl_set_attribute(SDL_GL_MULTISAMPLEBUFFERS, 1)
    rc = sdl_gl_set_attribute(SDL_GL_MULTISAMPLESAMPLES, 8)

    ! Create the SDL window.
    window = sdl_create_window(title = 'Fortran SDL 2.0' // c_null_char, &
                                   x = SDL_WINDOWPOS_UNDEFINED, &
                                   y = SDL_WINDOWPOS_UNDEFINED, &
                                   w = SCREEN_WIDTH, &
                                   h = SCREEN_HEIGHT, &
                               flags = ior(SDL_WINDOW_OPENGL, SDL_WINDOW_SHOWN))

    if (.not. c_associated(window)) then
        write (stderr, '(2a)') 'SDL Error: ', sdl_get_error()
        stop
    end if

    ! Create OpenGL context.
    context = sdl_gl_create_context(window)
    call opengl_init(SCREEN_WIDTH, SCREEN_HEIGHT)

    ! Main loop.
    loop: do
        ! Event handling.
        if (sdl_poll_event(event) > 0) then
            select case (event%type)
                case (SDL_QUITEVENT)
                    exit loop
            end select
        end if

        ! Render scene.
        call opengl_display()
    end do loop

    ! Quit gracefully.
    call sdl_gl_delete_context(context)
    call sdl_destroy_window(window)
    call sdl_quit()
contains
    subroutine opengl_display()
        !! The rendering routine, called every frame.

        call glclear(ior(GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT)) ! Clear the screen and depth buffer.
        call glloadidentity()                                       ! Reset current model view matrix.

        ! Render a coloured triangle.
        call glbegin(gl_triangles)
            call glcolor3f(1.0, 0.0, 0.0) ! red
            call glvertex2f(-0.8, -0.8)

            call glcolor3f(0.0, 1.0, 0.0) ! green
            call glvertex2f(0.8, -0.8)

            call glcolor3f(0.0, 0.0, 1.0) ! blue
            call glvertex2f(0.0, 0.9)
        call glend()

        ! Flush to screen.
        call sdl_gl_swap_window(window)
    end subroutine opengl_display

    subroutine opengl_init(screen_width, screen_height)
        !! Initialises OpenGL.
        integer, intent(in) :: screen_width, screen_height
        real(kind=real64)   :: aspect

        ! Set projection matrix.
        call glmatrixmode(GL_PROJECTION)
        ! Set aspect ratio of 2D orthographic projection.
        aspect = real(screen_width, kind=real64) / real(screen_height, kind=real64)
        call glortho(-aspect, aspect, -1.0_real64, 1.0_real64, -1.0_real64, 1.0_real64)

        ! Set model view matrix.
        call glmatrixmode(GL_MODELVIEW)
        call glenable(GL_DEPTH_TEST)
        call glclearcolor(0.0, 0.0, 0.0, 1.0)
    end subroutine opengl_init
end program main

Additionally to SDL 2.0, the program has to be linked against -lGL -lGLU on Linux and Unix, -framework OpenGL on macOS, or -lopengl32 -lglu32 on Microsoft Windows:

$ gfortran12 `sdl2-config --cflags` -o gl gl.f90 libfortran-sdl2.a `sdl2-config --libs` -lGL -lGLU
$ ./gl

See the fortran-sdl2 source code repository for more OpenGL examples.

Fortran OpenGL triangle
Fig. 4: The triangle rendered with SDL 2.0 and OpenGL in Fortran

Fortran Libraries

References