Search code examples
multithreadingocamldelaysleepwait

Thread delay and keyboard events in OCaml


Here is a simple game loop in OCaml. The state is displayed, input is received, and the state is advanced. The number of frames per second is contrained to 40 by delaying the thread for 0.025 seconds each loop.

main.ml:

let rec main (* state *) frame_time =
  (* Display state here. *)
  Input.get_input ();
  (* Advance state by one frame here. *)
  (* If less than 25ms have passed, delay until they have. *)
  if((Sys.time ()) < (frame_time +. 0.025)) then
    Thread.delay ((frame_time +. 0.025) -. (Sys.time ()));
  main (* next_state *) (Sys.time ())
;;

let init =
  Graphics.open_graph " 800x500";
  let start_time = (Sys.time ()) in
  main (* start_state *) start_time
;;

For this example, the get_input function simply prints keystrokes to the window.

input.ml:

let get_input () =
  let s = Graphics.wait_next_event 
    [Graphics.Key_pressed] in
  if s.Graphics.keypressed then
    Graphics.draw_char s.Graphics.key
;;

Makefile for easy testing:

main: input.cmo main.cmo
    ocamlfind ocamlc -o $@ unix.cma -thread threads.cma graphics.cma $^ 
main.cmo: main.ml
    ocamlfind ocamlc -c $< -thread
input.cmo: input.ml
    ocamlfind ocamlc -c $<

This works for the most part, but when keys are pressed very quickly, the program crashes with this error:

Fatal error: exception Unix.Unix_error(2, "select", "")

I believe it has something to do with Thread.delay. What is causing this issue, and what is the best way to achieve a constant FPS?


Solution

  • I'm not exactly sure what's going on (it depends on the implementation of Thread.delay, which I don't know). However, error 2 is Unix.EAGAIN, which represents a temporary shortage of kernel resources. As the name says, you should probably just try to do your Thread.delay again. If I use try ... with to catch the Unix.Unix_error exception I see no other errors except EAGAIN being delivered. If I just print a message and continue, the program seems to work. At least, it continues to echo characters to the window and doesn't crash. I'm working in OS X 10.7 (Lion). It might work differently for you.

    Edit

    Another possible problem with this code is that Sys.time() returns processor time, which only increases while the process is doing real computation. It doesn't increase while the process is waiting for input. This means that the delay is always invoked, even if you wait a long time between keypresses (it was confusing me for a while). It might be better to use Unix.gettimeofday (), which returns wall clock time.

    Edit 2

    After some more research and testing, I believe that the Unix.EAGAIN error is telling you that the full delay was interrupted by some event. In your case the interrupting event is the arrival of a character (I believe). So if you want to wait the full time, you should replace the single call to Thread.delay() with a loop.

    This gives you something like the following for your main code:

    let rec main (* state *) frame_time =
      (* Display state here. *)
      Input.get_input ();
      (* Advance state by one frame here. *)
      (* If less than 25ms have passed, delay until they have. *)
      let rec delay () =
        let duration = frame_time +. 0.025 -. Unix.gettimeofday () in
        if duration > 0.0 then
          try
            Thread.delay duration
          with Unix.Unix_error (Unix.EAGAIN, _, _) -> delay ()
      in
        delay ();
      main (* next_state *) (Unix.gettimeofday  ())
    ;;
    
    let init =
      Graphics.open_graph " 800x500";
      let start_time = (Unix.gettimeofday  ()) in
      main (* start_state *) start_time
    ;;
    

    (If you use Unix.select to do your delaying, you can remove the dependence on threads. But you might need them anyway for other reasons. The code would look the same except the error is EINTR rather than EAGAIN.)