interkosmos/fortran-sdl2

Quitting with ESC key

angelog0 opened this issue · 18 comments

I tried to write a function to quit an SDL2 Fortran app with ESC key beside the usual quitting,

function quit() result(r)
    use sdl2_consts, only: SDL_QUIT_TYPE, SDL_KEY_DOWN, SDL_SCANCODE_ESCAPE
    use sdl2_types, only: sdl_event
    use sdl2, only: sdl_poll_event

    logical :: r

    type(sdl_event) :: event

    r = .false.

    if (sdl_poll_event(event) > 0) then

        select case (event%type)
        case (SDL_QUIT_TYPE)
           r = .true.
        case (SDL_KEY_DOWN)
           print *, event%key%key_sym%sym
           if (event%key%key_sym%sym == SDL_SCANCODE_ESCAPE) r = .true.
        end select
     end if
  end function quit

program main
[...]

  do while (.not.quit())
    [...]
    ! Render to window.
        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

but it does not work for ESC key: the PRINT statement prints always 0 (zero) regardless of the character pressed, so quit() is .true. only when one clicks the upper right X button.

I suspect a bug here...

Just for completeness, I have attached a tar ball (test_case.tar.gz) with a test case in C and rewritten in Fortran. The C code behaves as expected printing (on exiting) the codes of keys pressed and exiting with ESC. The Fortran one prints always 0 for the codes and does not exit if not closing the window.

In comment there is the command line used for the build. The compiler is GCC 8.2.0

I wonder if a library as SDL2 can be completely interfaced in Fortran by means of ISO C BINDING. SDL2 contains C unions which seem excluded from ISO C BINDING.

In this page Interoperability of derived types, under point 4, and (here) one can read:

No Fortran type is interoperable with a C union type, struct type that contains a bit field, or struct type that contains a flexible array member.

Maybe, for confirmation, we need to ask Fortran people...

BTW, just found this discussion.

The following code lets you quit the game with the Escape key:

do while (.not. done)
    rc = sdl_poll_event(event)

    if (rc > 0) then
        select case (event%type)
            case (SDL_QUIT_TYPE)
                done = .true.
            case (SDL_KEY_DOWN)
                keys = sdl_get_keyboard_state()
                ! Quit if player hits Escape:
                if (keys(SDL_SCANCODE_ESCAPE) == 1) then
                    done = .true.
                end if
        end select
    end if
end do

Yes, the Fortran standard does not define any unions. But you can use transfer() to do simple type casting. See sdl_transfer_event() in sdl2.f90.

@interkosmos wrote:

But you can use transfer() to do simple type casting. See sdl_transfer_event()

Hmm... if sdl_poll_event already calls sdl_transfer_event(),

select case (event%type)

                ! SDL_KeyboardEvent
                case (SDL_KEY_DOWN : SDL_KEY_UP)
                    event%key = transfer(event, event%key)

WHY then

write(*,'(A,i0)') 'KEY = ', event%key%key_sym%sym

prints always 0?

I tried also

event%key%key_sym%sym = transfer(event, event%key%key_sym%sym)
           write(*,'(A,i0)') 'KEY = ', event%key%key_sym%sym
           if (event%key%key_sym%sym == ESC) quit = 1

but does not work, prints always 768..

@interkosmos wrote:

The following code lets you quit the game with the Escape key:
keys = sdl_get_keyboard_state()

Who is keys? How it is/must be declared?

Just for completeness, compiling with the -Wall option your code, prints these warnings:

gfortran -Wall -std=f2008 `sdl2-config --cflags` ../f03sdl2-master/sdl2.f90 draw_test.f90 -o draw_test `sdl2-config --libs`
../f03sdl2-master/sdl2.f90:1469:47:

         function sdl_get_render_target(renderer) bind(c, name='SDL_GetRenderTarget')
                                               1
Warning: Unused dummy argument 'renderer' at (1) [-Wunused-dummy-argument]
../f03sdl2-master/sdl2.f90:1469:8:

         function sdl_get_render_target(renderer) bind(c, name='SDL_GetRenderTarget')
        1
Warning: Return value of function 'sdl_get_render_target' at (1) not set [-Wreturn-type]
draw_test.f90:181:18:

            keys = sdl_get_keyboard_state()
                  1
Warning: POINTER-valued function appears on right-hand side of assignment at (1) [-Wsurprising]

I think sdl_get_render_target should go in the interface section not in contains section of the module:

$ diff -Naur programming/f03sdl2-master/sdl2.f90~ programming/f03sdl2-master/sdl2.f90
--- programming/f03sdl2-master/sdl2.f90~        2018-09-08 16:19:29.000000000 +0200
+++ programming/f03sdl2-master/sdl2.f90 2018-09-10 01:57:51.391318300 +0200
@@ -977,6 +977,14 @@
             integer(kind=c_int) :: sdl_get_system_ram
         end function sdl_get_system_ram

+        ! SDL_Texture *SDL_GetRenderTarget(SDL_Renderer *renderer)
+        function sdl_get_render_target(renderer) bind(c, name='SDL_GetRenderTarget')
+            use, intrinsic :: iso_c_binding
+            implicit none
+            type(c_ptr), intent(in), value :: renderer
+            type(c_ptr)                    :: sdl_get_render_target
+        end function sdl_get_render_target
+
         ! Uint32 SDL_GetTicks(void)
         function sdl_get_ticks() bind(c, name='SDL_GetTicks')
             use, intrinsic :: iso_c_binding
@@ -1465,14 +1473,6 @@
             call c_f_pointer(surface%format, sdl_get_pixel_format)
         end function

-        ! SDL_Texture *SDL_GetRenderTarget(SDL_Renderer *renderer)
-        function sdl_get_render_target(renderer) bind(c, name='SDL_GetRenderTarget')
-            use, intrinsic :: iso_c_binding
-            implicit none
-            type(c_ptr), intent(in), value :: renderer
-            type(c_ptr)                    :: sdl_get_render_target
-        end function sdl_get_render_target
-
         ! SDL_Surface *SDL_GetWindowSurface(SDL_Window *window)
         function sdl_get_window_surface(window)
             !! Calls `sdl_get_window_surface_()` and converts the returned

OK, I think the code you propose for quitting should be

integer(C_INT8_T), pointer :: keys(:) => null()

[...]
do while (.not. done)
    rc = sdl_poll_event(event)

    if (rc > 0) then
        select case (event%type)
            case (SDL_QUIT_TYPE)
                done = .true.
            case (SDL_KEY_DOWN)
                keys => sdl_get_keyboard_state()  ! SEE HERE
                ! Quit if player hits Escape:
                if (keys(SDL_SCANCODE_ESCAPE) == 1) then
                    done = .true.
                end if
        end select
    end if
end do

Notice,

keys => sdl_get_keyboard_state()

an association not an assignement:

keys = sdl_get_keyboard_state()

This change suppress also the warning:

POINTER-valued function appears on right-hand side of assignment

Sorry for the incomplete example. The complete one:

integer(kind=8), allocatable :: keys(:)
integer                      :: rc

do while (.not. is_done)
    rc = sdl_poll_event(event)

    if (rc > 0) then
        select case (event%type)
            case (SDL_QUIT_TYPE)
                is_done = .true.
            case (SDL_KEY_DOWN)
                keys = sdl_get_keyboard_state()

                ! Quit if player hits Escape:
                if (is_key(keys, SDL_SCANCODE_ESCAPE)) &
                    is_done = .true.
        end select
    end if
end do

The issue with sdl_get_render_target() was solved already, but not commited. Thank you anyway.

I’m still looking for the reason why event%key%key_sym%sym doesn’t contain the corrent value.

@interkosmos, last you code still gives warnings when compiled with -Wall flag:

gfortran -Wall -std=f2008 `sdl2-config --cflags` ../f03sdl2-master/sdl2.f90 draw_test.f90 -o draw_test `sdl2-config --libs`
draw_test.f90:182:18:

            keys = sdl_get_keyboard_state()
                  1
Warning: POINTER-valued function appears on right-hand side of assignment at (1) [-Wsurprising]
draw_test.f90:182:0:

            keys = sdl_get_keyboard_state()

Warning: 'keys.offset' may be used uninitialized in this function [-Wmaybe-uninitialized]
draw_test.f90:182:0: Warning: 'keys.dim[0].lbound' may be used uninitialized in this function [-Wmaybe-uninitialized]
draw_test.f90:182:0: Warning: 'keys.dim[0].ubound' may be used uninitialized in this function [-Wmaybe-uninitialized]
draw_test.f90:182:0:

            keys = sdl_get_keyboard_state()

Warning: 'keys.dim[0].lbound' may be used uninitialized in this function [-Wmaybe-uninitialized]
draw_test.f90:182:0: Warning: 'keys.dim[0].ubound' may be used uninitialized in this function [-Wmaybe-uninitialized]
draw_test.f90:182:0: Warning: 'keys.dim[0].ubound' may be used uninitialized in this function [-Wmaybe-uninitialized]
draw_test.f90:182:0: Warning: 'keys.dim[0].lbound' may be used uninitialized in this function [-Wmaybe-uninitialized]

Maybe I am wrong, but I think my proposal

integer(C_INT8_T), pointer :: keys(:) => null()
[...]
keys => sdl_get_keyboard_state()

is the right... (See this at section 11.9)

Any way, may you elaborate about this

OOps, just seen your answer, thanks.

Yes, I go d’accord with you. Your proposal seems to be better. See #4 (comment), I’m still clueless.

After last change I had to change this code:

integer(C_INT8_T), pointer :: keys(:) => null()
[...]
keys => sdl_get_keyboard_state()

in

integer(C_UINT8_T), pointer :: keys(:) => null()
[...]
! the same
keys => sdl_get_keyboard_state()

The new code build without warnings but does not work, typing ESC does not exit the program.

Are you sure that in `sdl2_aliases' (why another module and not using the existing sdl2_consts?)

integer, parameter :: c_uint8_t            = c_int16_t

is the right thing?

If I remember rightly, Fortran for uint.. types wants int.. types of the same size (int8_t) in this case... but this would break other things, so I think you have to review this carefully...

In other words, uint8_t in C is 1 byte while int16_t is 2 byte.

Just this

integer, parameter :: c_uint8_t            = c_int8_t

fixes the issue, and maybe also event%key%key_sym%sym !!!

Thank you very much for all the hints. They have been committed.

Sorry but there are still some issue. From the SDL2 Wiki:

int SDL_SetRenderDrawColor(SDL_Renderer* renderer,
                           Uint8         r,
                           Uint8         g,
                           Uint8         b,
                           Uint8         a)

but now I find in your code:

! int SDL_SetRenderDrawColor(SDL_Renderer *renderer, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
        function sdl_set_render_draw_color_(renderer, r, g, b, a) bind(c, name='SDL_SetRenderDrawColor')
            use, intrinsic :: iso_c_binding
            use :: sdl2_consts
            implicit none
            type(c_ptr),              intent(in), value :: renderer
            integer(kind=c_uint16_t), intent(in), value :: r
            integer(kind=c_uint16_t), intent(in), value :: g
            integer(kind=c_uint16_t), intent(in), value :: b
            integer(kind=c_uint16_t), intent(in), value :: a
            integer(kind=c_int)                         :: sdl_set_render_draw_color_
        end function sdl_set_render_draw_color_

[...]

! int SDL_SetRenderDrawColor(SDL_Renderer *renderer, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
        function sdl_set_render_draw_color(renderer, r, g, b, a)
            !! Converts integer arguments to c_uint32_t before calling
            !! `sdl_set_render_draw_color_()`.
            use, intrinsic :: iso_c_binding, only: c_ptr
            use :: sdl2_consts
            implicit none
            type(c_ptr), intent(in) :: renderer
            integer,     intent(in) :: r
            integer,     intent(in) :: g
            integer,     intent(in) :: b
            integer,     intent(in) :: a
            integer                 :: sdl_set_render_draw_color

            sdl_set_render_draw_color = sdl_set_render_draw_color_(renderer, &
                                                                   int(r, kind=c_uint16_t), &
                                                                   int(g, kind=c_uint16_t), &
                                                                   int(b, kind=c_uint16_t), &
                                                                   int(a, kind=c_uint16_t))
        end function sdl_set_render_draw_color

which causes

rc = sdl_set_render_draw_color(renderer,int(r,1),int(g,1),int(b,1), &
         1
Error: Type mismatch in argument 'r' at (1); passed INTEGER(1) to INTEGER(4)

If I remember, the previous implementation didn't use the wrapper and maybe it was better. I think the best should be not to use the wrapper

! int SDL_SetRenderDrawColor(SDL_Renderer *renderer, Uint8 r, Uint8 g, Uint8 b, Uint8 a)
        function sdl_set_render_draw_color(renderer, r, g, b, a) bind(c, name='SDL_SetRenderDrawColor')
            use, intrinsic :: iso_c_binding
            use :: sdl2_consts
            implicit none
            type(c_ptr),              intent(in), value :: renderer
            integer(kind=c_uint8_t), intent(in), value :: r
            integer(kind=c_uint8_t), intent(in), value :: g
            integer(kind=c_uint8_t), intent(in), value :: b
            integer(kind=c_uint8_t), intent(in), value :: a
            integer(kind=c_int)                         :: sdl_set_render_draw_color
        end function sdl_set_render_draw_color

I understand you intent (avoid the explicit conversion by the user), but sometime it would be better to reduce the cost to call a function to call a function...

But how do you want to store an integer like 255 in c_int8_t? That will cause an arithmetic overflow.

Without a wrapper function, the programmer always has to do the type conversion manually. Boy, that would be annoying.

@interkosmos wrote:

But how do you want to store an integer like 255 in c_int8_t?

Have you tried? Why it worked in your previous implementation? In my test case I have color defined with (255,0,127), (255,255,255), (0,255,255)... and all colors where right..

BTW, I interfaced (most) Windows itself this way without issue.. I think it is how ISO C BINDING works. Maybe we have to ask Fortran people...

Without a wrapper function, the programmer always has to do the type conversion manually. Boy, that would be annoying.

No No.. It is not annoying if you can save the cost of calling function, subroutine... Instead that code not only has the cost of conversion (int(..,kind=...)) but also one more function call. Any way do at your best.

And... hem hem with last your commit there is this failure:

gfortran -std=f2008 -Wall `sdl2-config --cflags` -o music examples/music/music.f90 sdl2.o sdl2_mixer.o sdl2_ttf.o `sdl2-config --libs` -lSDL2_mixer -lSDL2_ttf
examples/music/music.f90:62:9:

     rc = mix_open_audio(MIX_DEFAULT_FREQUENCY, &
         1
Error: Type mismatch in argument 'format' at (1); passed INTEGER(4) to INTEGER(2)
make: *** [Makefile:70: music] Error 1

Please see the following example:

program example
    use, intrinsic :: iso_c_binding
    implicit none
    integer(kind=c_int8_t) :: foo = int(127, 1) ! works
    integer(kind=c_int8_t) :: bar = int(255, 1) ! doesn't work

    print *, foo, bar
end program example

IMHO the correct type is therefore c_uint16_t.

Also this work (a solution adopted in 2006 interfacing for the first time BGI aka WinBGIm v. 3):

$ cat print_uint8_t.c
#include <stdio.h>
#include <stdint.h>

void print_uint8_t(uint8_t u)
{
  printf("%u\n",u);

}

$ cat test_c_uint8_t.f90
!
! gcc -c print_uint8_t.c
! gfortran -std=f2008 test_c_uint8_t.f90 -o test_c_uint8_t.out print_uint8_t.o
! ./getch_test.out
!

program test_c_uint8_t
  implicit none

  interface
     subroutine print_u8(u) bind(c, name='print_uint8_t')
       use, intrinsic :: iso_c_binding, only: C_CHAR
       character(C_CHAR), intent(in), value :: u
     end subroutine print_u8
  end interface

  call print_u8(achar(127))
  call print_u8(achar(0))
  call print_u8(achar(255))

end program test_c_uint8_t

What I don't like is that c_int16_t is 2 byte while in the original C routine the data passed is 1 byte... The C routine takes only the firs byte of it, but I wonder if this is the main street to adopt for this kind of data. I have asked to Fortran people.

Anyway now your example builds fine.. thanks.

The first assignment in this chapter is easy: create a program with a main function and a separate subfunction called hello, which when called prints "Hello there!". The subfunction does not take any parameters or return any value, just prints the line. Then, to the main function, add a call to the subfunction and two print commands, the first one before the call which says "Lets call the subfunction:", and one after the subfunction call, a print command which prints "Quitting.". If implemented correctly, the program will print the following: