Friday, July 24, 2015

Introducing Luminance, a safer OpenGL API

A few weeks ago, I was writing Haskell lines for a project I had been working on for a very long time. That project was a 3D engine. There are several posts about it on my blog, feel free to check out.

The thing is… Times change. The more it passes, the more I become mature in what I do in the Haskell community. I’m a demoscener, and I need to be productive. Writing a whole 3D engine for such a purpose is a good thing, but I was going round and round in circles, changing the whole architecture every now and then. I couldn’t make my mind and help it. So I decided to stop working on that, and move on.

If you are a Haskell developer, you might already know Edward Kmett. Each talk with him is always interesting and I always end up with new ideas and new knowledge. Sometimes, we talk about graphics, and sometimes, he tells me that writing a 3D engine from scratch and release it to the community is not a very good move.

I’ve been thinking about that, and in the end, I agree with Edward. There’re two reasons making such a project hard and not interesting for a community:

  1. a good “3D engine” is a specialized one – for FPS games, for simulations, for sport games, for animation, etc. If we know what the player will do, we can optimize a lot of stuff, and put less details into not-important part of the visuals. For instance, some games don’t really care about skies, so they can use simple skyboxes with nice textures to bring a nice touch of atmosphere, without destroying performance. In a game like a flight simulator, skyboxes have to be avoided to go with other techniques to provide a correct experience to players. Even though an engine could provide both techniques, apply that problem to almost everything – i.e. space partitionning for instance – and you end up with a nightmare to code ;
  2. an engine can be a very bloated piece of software – because of point 1. It’s very hard to keep an engine up to date regarding technologies, and make every one happy, especially if the engine targets a large audience of people – i.e. hackage.

Point 2 might be strange to you, but that’s often the case. Building a flexible 3D engine is a very hard and non-trivial task. Because of point 1, you utterly need to restrict things in order to get the required level of performance or design. There are people out there – especially in the demoscene world – who can build up 3D engines quickly. But keep in mind those engines are limited to demoscene applications, and enhancing them to support something else is not a trivial task. In the end, you might end up with a lot of bloated code you’ll eventually zap later on to build something different for another purpose – eh, demoscene is about going dirty, right?! ;)

Basics

So… Let’s go back to the basics. In order to include everyone, we need to provide something that everyone can download, install, learn and use. Something like OpenGL. For Haskell, I highly recommend using gl. It’s built against the gl.xml file – released by Khronos. If you need sound, you can use the complementary library I wrote, using the same name convention, al.

The problem with that is the fact that OpenGL is a low-level API. Especially for new comers or people who need to get things done quickly. The part that bothers – wait, no, annoys – me the most is the fact that OpenGL is a very old library which was designed two decades ago. And we suffer from that. A lot.

OpenGL is a stateful graphics library. That means it maintains a state, a context, in order to work properly. Maintaining a context or state is a legit need, don’t get it twisted. However, if the design of the API doesn’t fit such a way of dealing with the state, we come accross a lot of problems. Is there one programmer who hasn’t experienced black screens yet? I don’t think so.

The OpenGL’s API exposes a lot of functions that perform side-effects. Because OpenGL is weakly typed – almost all objects you can create in OpenGL share the same GL(u)int type, which is very wrong – you might end up doing nasty things. Worse, it uses an internal binding system to select the objects you want to operate on. For instance, if you want to upload data to a texture object, you need to bind the texture before calling the texture upload function. If you don’t, well, that’s bad for you. There’s no way to verify code safety at compile-time.

You’re not convinced yet? OpenGL doesn’t tell you directly how to change things on the GPU side. For instance, do you think you have to bind your vertex buffer before performing a render, or is it sufficient to bind the vertex array object only? All those questions don’t have direct answers, and you’ll need to dig in several wikis and forums to get your answers – the answer to that question is “Just bind the VAO, pal.”

What can we do about it?

Several attempts to enhance that safety have come up. The first thing we have to do is to wrap all OpenGL object types into proper types. For instance, we need several types for Texture and Framebuffer.

Then, we need a way to ensure that we cannot call a function if the context is not setup for. There are a few ways to do that. For instance, indexed monads can be a good start. However, I tried that, and I can tell you it’s way too complicated. You end up with very long types that make things barely unreadable. See this and this for excerpts.

Luminance

In my desperate quest of providing a safer OpenGL’s API, I decided to create a library from scratch called luminance. That library is not really an OpenGL safe wrapper, but it’s very close to being that.

luminance provides the same objects than OpenGL does, but via a safer way to create, access and use them. It’s an effort for providing safe abstractions without destroying performance down and suited for graphics applications. It’s not a 3D engine. It’s a rendering framework. There’s no light, asset managers or that kind of features. It’s just a tiny and simple powerful API.

Example

luminance is still a huge work in progress. However, I can already show an example. The following example opens a window but doesn’t render anything. Instead, it creates a buffer on the GPU and perform several simple operations onto it.

-- Several imports.
import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.Trans.Resource -- from the resourcet package
import Data.Foldable ( traverse_ )
import Graphics.Luminance.Buffer
import Graphics.Luminance.RW
import Graphics.UI.GLFW -- from the GLFW-b package
import Prelude hiding ( init ) -- clash with GLFW-b’s init function

windowW,windowH :: Int
windowW = 800
windowH = 600

windowTitle :: String
windowTitle = "Test"

main :: IO ()
main = do
  init
  -- Initiate the OpenGL context with GLFW.
  windowHint (WindowHint'Resizable False)
  windowHint (WindowHint'ContextVersionMajor 3)
  windowHint (WindowHint'ContextVersionMinor 3)
  windowHint (WindowHint'OpenGLForwardCompat False)
  windowHint (WindowHint'OpenGLProfile OpenGLProfile'Core)
  window <- createWindow windowW windowH windowTitle Nothing Nothing
  makeContextCurrent window
  -- Run our application, which needs a (MonadIO m,MonadResource m) => m
  -- we traverse_ so that we just terminate if we’ve failed to create the
  -- window.
  traverse_ (runResourceT . app) window
  terminate

-- GPU regions. For this example, we’ll just create two regions. One of floats
-- and the other of ints. We’re using read/write (RW) regions so that we can
-- send values to the GPU and read them back.
data MyRegions = MyRegions {
    floats :: Region RW Float
  , ints   :: Region RW Int
  }

-- Our logic.
app :: (MonadIO m,MonadResource m) => Window -> m ()
app window = do
  -- We create a new buffer on the GPU, getting back regions of typed data
  -- inside of it. For that purpose, we provide a monadic type used to build
  -- regions through the 'newRegion' function.
  region <- createBuffer $
    MyRegions
      <$> newRegion 10
      <*> newRegion 5
  clear (floats region) pi -- clear the floats region with pi
  clear (ints region) 10 -- clear the ints region with 10
  readWhole (floats region) >>= liftIO . print -- print the floats as an array
  readWhole (ints region) >>= liftIO . print -- print the ints as an array
  floats region `writeAt` 7 $ 42 -- write 42 at index=7 in the floats region
  floats region @? 7 >>= traverse_ (liftIO . print) -- safe getter (Maybe)
  floats region @! 7 >>= liftIO . print -- unsafe getter
  readWhole (floats region) >>= liftIO . print -- print the floats as an array

Those read/write regions could also have been made read-only or write-only. For such regions, some functions can’t be called, and trying to do so will make your compiler angry and throw errors at you.

Up to now, the buffers are created persistently and coherently. That might cause issues with OpenGL synchronization, but I’ll wait for benchmarks before changing that part. If benchmarking spots performance bottlenecks, I’ll introduce more buffers and regions to deal with special cases.

luminance doesn’t force you to use a specific windowing library. You can then embed it into any kind of host libraries.

What’s to come?

luminance is very young. At the moment of writing this article, it’s only 26 commits old. I just wanted to present it so that people know it exists will be released as soon as possible. The idea is to provide a library that, if you use it, won’t create black screens because of framebuffers incorrectness or buffers issues. It’ll ease debugging OpenGL applications and prevent from making nasty mistakes.

I’ll keep posting about luminance as I get new features implemented.

As always, keep the vibe, and happy hacking!

Thursday, July 16, 2015

Don’t use Default

Don’t use Default

Background knowledge: typeclasses and laws

In Haskell, we have a lot of typeclasses. Those are very handy and – in general – come with laws. Laws are very important and give hints on how we are supposed to use a typeclass.

For instance, the Semigroup typeclass exposes an operator ((<>)) and has an associativity law. If a, b and c have the same type T, if we know that T is a Semigroup, we have the following law:

a <> b <> c = (a <> b) <> c = a <> (b <> c)

If T is a Monoid, which is a Semigroup with an identity (called mempty), we have a law for monoids:

a <> mempty = mempty <> a = a

Those laws can – have – to be used in our code base to take advantage over the structures, optimize or avoid boilerplate.

The Default typeclass

In some situations, we want a way to express default values. That’s especially true in OO languages, like in the following C++ function signature:

void foo(float i = 1);

In Haskell, we cannot do that, because we have to pass all arguments to a function. The following doesn’t exist:

foo :: Float -> IO ()
foo (i = 1) = -- […]

And there’s more. Even in C++, how do you handle the case when you have several arguments and want only the first one to be defaulted? You cannot.

So, so… Some Haskellers decided to solve that problem with a typeclass. After all, we can define the following typeclass:

class Default a where
  def :: a

We can then implement Default and have a default value for a given type.

instance Default Radians where
  def = Radians $ 2*pi

instance Default Fullscreen where
  def = Fullscreen False

However, there is an issue with that. You cannot use Default without creating newtypes to overload them. Why? Well, consider the following Default instance:

instance Default Float where
  def = -- what should we put here?

Remember that, in Haskell, an instance is defined only once and is automatically imported when you import the module holding it. That means you cannot have the following:

-- in a module A
instance Default Float where
  def = 0

-- in a module B
instance Default Float where
  def = 1

-- in a module C
import A
import B

-- What instances should we use?

Hey, that’s easy. We just have to keep the modules apart, and import the one we want to use!

Yeah, well. No. No.

Orphan instances are wrong. You should read this for further explanations.

That’s why we have to use newtypes everywhere. And that’s boring. Writing code should always have a goal. When we write as much boilerplate code as real code, we can start thinking there’s something wrong. Worse, if we have more boilerplate than real code, well, something is terribly wrong. In our case, we’re introducing a lot of newtypes for only being able to use def at a few spots. Is that even worth it? Of course not.

Default is evil

The Default typeclass is evil. It’s shipped with default instances, like one for [a], which defaults to the empty list – []. It might be clear for you but why would I want to default to the empty list? Why not to [0]? Or a more complex list? I really doubt someone ever uses def :: [a].

Another reason why Default is wrong? There’s absolutely no law. You just have a default value, and that’s all.

bar :: (Default a) => a -> Maybe String

Can you say what the default is for? Of course you cannot. Because there’s no law. The instance has no real meaning. A default value makes sense only for the computation using it. For instance, the empty list makes sense if we glue it to the list concatenation.

We already have a lot of defaults

In base, there’re already several ways to express defaulted values.

The Monoid’s mempty is a way to express a default value regarding its binary operation ((<>)).

The Alternative’s empty provides a similar default, but for first-class types.

The MonadZero’s mzero provides a different default, used to absorb everything. That’s a law of MonadZero:

mzero >>= f = mzero
a >> mzero  = mzero

Those defaults are interesting because we can reason about. If I see mzero in a monadic code, I know that whatever is following will be discarded.

Conclusion

So please. Stop using Default. It doesn’t bring any sense to your codebase. It actually removes some! If you want to provide functions with defaulted arguments, consider partially applied functions.

data-default is a very famous package – look at the downloads! You can now have some hindsight about it before downloading it and ruining your design. ;)

Happy hacking. :)