[solved] Laziness using SDL

Hello. I am writing an emulator using the SDL (v2) library as a screen. As usual in Haskell, it looks like my screen does not show until it is really required, i.e. the emulator is waiting for a key press (which luckily is done using the SDL as well). If it does not execute a “wait for key” instruction, everything else related to graphics is not executed (the window does not open at all).

Can I force evaluation in a cleaner way than a useless code block on the event loop complex enough so that the compiler cannot tag it as useless?

Coming from the OCaml world, Haskell’s laziness is beautiful but enabling it by default is probably the most painful feature, and I don’t really know how to strictify parts of the execution without writing dirty code. Thanks for your help!

3 Likes

Perhaps without already throwing indictment and risk unproductive flamewars, would it be possible to think along the line of “how to cleanly and elegantly trigger evaluations?”

I would like to learn about that too, but not another meta discussion of lazy by default good or bad…

1 Like

I have only used SDL1 bindings, but this behaviour seems weird. Do you have a minimal reproducible example by any chance?

1 Like

Sorry if my post sounded aggressive, that was not the goal. I was just highlighting the fact that it is a hard point to get familiar with when coming from a strict functional language.

2 Likes

It would take some time to isolate code and make a minimal example, but here is the idea.

The main loop of the emulator looks like this:

void . forever $ do
  -- this can update the graphics memory on a Draw instruction,
  -- and in this case it sets a flag indicating that the screen must be refreshed
  executeCycle cpu memory graphicsMemory
  mustRefreshScreen <- getFlag (flags cpu) DrawFlag
  -- only display the graphics memory on the screen when required
  when mustRefreshScreen $ S.display screen graphicsMemory

and here is the display function:

display :: Screen -> GraphicsMemory -> IO ()
display (Screen _ renderer) graphicsMemory = do
  rendererDrawColor renderer $= backgroundColour
  clear renderer
  forM_ [0..31] $ \y ->
    forM_ [0..63] $ \x -> do
      pixelOn <- GM.testPixel graphicsMemory (toEnum x) (toEnum y)
      rendererDrawColor renderer $= if pixelOn then backgroundColour else foregroundColour
      fillRect renderer (Just $ Rectangle (P $ V2 (toEnum x * 10) (toEnum y * 10)) (V2 10 10))
  present renderer

I know it somehow works because if I reach an instruction waiting for input, everything is displayed correctly in the visible screen. Maybe my code is not idiomatic and I would be happy to learn more.

1 Like

But for example let’s take the minimal main function code in the documentation. I changed the loop waiting for key input to just setting a blue background and doing nothing forever, and my window does not show anymore.

main :: IO ()
main = do
  initializeAll
  window <- createWindow "My SDL Application" defaultWindow
  renderer <- createRenderer window (-1) defaultRenderer
  rendererDrawColor renderer $= V4 0 0 255 255
  clear renderer
  present renderer
  void . forever $ return ()
1 Like
{- cabal:
build-depends: base, text, sdl2
-}

{-# LANGUAGE OverloadedStrings #-}

import SDL
import Data.Text
import Control.Monad (void, forever)

main :: IO ()
main = do
  initializeAll
  window <- createWindow "My SDL Application" defaultWindow
  renderer <- createRenderer window (-1) defaultRenderer
  rendererDrawColor renderer $= V4 0 0 255 255
  clear renderer
  present renderer
  void . forever $ return ()
$ nix-shell -p pkg-config  -p SDL2

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.4.2

$ cabal run --verbose sdl2.hs 

Seems alright to me…

2 Likes

I believe (but could be wrong) that SDL does not “keep your screen alive” for you. So if you resize the window, or maybe partially obscure it, or stuff like that, you need to redraw it. So if you just draw once and then wait forever, it may be visible at first (as @hellwolf observes), but it it is a bit fragile.

In games you usually draw a new frame regularly anyways, so this is not a big issue there. If you really don’t want to do that, I guess you have to find out how to wait for the relevant events that require a redraw.

4 Likes

I have no clue how to debug this. I run the exact same code and nothing shows. :exploding_head:

1 Like

I don’t think this has anything to do with laziness.
Perhaps you’re not using the threaded runtime or something like that.

Laziness is not a problem in general when your data structures are strict.

Also, void . forever $ return () is a busy loop, needlessly consuming a capability (this can break a non-threaded runtime btw). Use something like takeMVar shouldQuit instead.

4 Likes

I think laziness has nothing to do with this. At the end of the day SDL will require execution of the screen.

1 Like

Hello. After some experiments rewriting the code in C and checking various issues in the official SDL GitHub repository and forum, and thanks to this post, I can conclude that the event loop is required on macOS.

Sorry for the noise guys… As a casual Haskell user, I thought it was language-related, because I have already run into issues where if you don’t actually do an IO action, nothing really happens. It looks so similar it was easy to get confused. Maybe we can delete this post, or edit the title and keep it for another beginner who might have the same error?

In any case, thanks for answering so fast on this thread. :+1:

9 Likes

It’s good to build institutional knowledge about stuff (gui apps in particular), so thanks for asking and sharing experience :+1:

6 Likes