so, gpipe
out of the various haskell graphics libraries i've used, gpipe has so far been the best. that being said, the list of haskell graphics libraries is pretty short.
lambdacube was... okay. well, it was okay up until i hit the point where there were two different ways to specify uniforms, neither of which seemed to work, and there was precisely zero documentation. also its bespoke shader language was like haskell up until it wasn't, and also had very minimal documentation. it was kind of a mess.
opengl (not opengl-raw) papers over a few arbitrary parts of the opengl state machine, while also not even exposing other parts of it, so that any non-trivial program would need to use both opengl and opengl-raw and just hope that their raw calls weren't mucking about with anything that opengl was assuming was safely encapsulated.
gpipe, though... the degree of encapsulation it has over actual opengl is pretty amazing. all the shaders are written in plain haskell, in the same file as the rest of your code. gpipe is really good at communicating with the haskell universe, in ways that sometimes border on the magical.
there are some downsides, though. gpipe is very type-safe, but that type safety comes at a cost, and that is some of the most byzantine type signatures i've ever seen in non-theory code.
take for example texture sampling:
gpipe functions are rife with this kind of thing, having two or three type variables that are peppered across the argument types repeatedly and bound to various typeclasses. when they all infer correctly, it's great. when they don't, i had to dig around looking: is it that it can't correctly infer the type (which it frequently can't); or am i actually giving it the wrong kind of data, and in that case what actually is the right kind of data? is it only the wrong kind of data because somewhere else i'm using a different type that causes one of these type variables to be inferred as a non-matching type, and, if so, where is that other value?
the lack of common opengl reference points can get kinda confusing too, especially in conjunction with the above. if i want to turn on the depth buffer, then i gotta be calling `drawWindowColorDepth` instead of just `drawWindowColor`, sure. but that also takes a different type of argument in several ways -- i need a FragmentStream that has a FragDepth value, and i need to provide depth comparison data for the shader pipeline. i also need to have initialized a Window with a DepthRenderable value. or if you want to turn on alpha blending, you need to provide a blending option for your `drawWindow*` calls, sure. you'll also need to have your window initialized with a `Format` type that includes an alpha component, which changes some of the types needed in your shader for certain calls (like clearing the window).
these are things you can learn from checking the type signature, and that's generally how you learn Haskell frameworks, but there's so much type information, generally obfuscated behind chains of typeclasses and type families, and it's in places that are very weird if you're already familiar with how opengl works behind the curtain. none of that is bad as such -- well, the wacky type inference is bad -- but it definitely has caused a fair number of stumbles so far.
for example, their 'hello world' demo never actually directly refers to the shader render data. a
all those types do a lot of work: they wrap up and abstract away the underlying opengl setup, and do things like make it impossible to write the wrong kind of data to a shader, or the wrong color type to a texture, or whatever. that's very nice, and useful, but it also causes some weird issues itself. partly it's that all abstractions are leaky, right, and so there's always the looming threat of, say, my shader render state taking up too many slots and so being too big to be sent to the GPU and causing an error. which is an invisible constraint until it happens, since as far as gpipe is concerned the shader render function is just a normal haskell function, it's just that if you know opengl you know that that's the entry-point into the shader system and that there's a finite amount of stuff you can stuff through it at one time.
all that being said though, this has been so much clearer, easier to use, and full-featured than any of the other haskell rendering setups i've checked out. i installed it uhh eleven days ago, and i already ported over my object rendering code, plus i set up a basic camera, plus i got textures working, UVs being synthesized for procedural models, and basic sprite font code working. some of that's just because i've been getting better as a programmer, but some of that is definitely because gpipe manages the most tedious parts of rendering and wraps up the rest of it in a way that it's difficult to break your rendering state.
lambdacube was... okay. well, it was okay up until i hit the point where there were two different ways to specify uniforms, neither of which seemed to work, and there was precisely zero documentation. also its bespoke shader language was like haskell up until it wasn't, and also had very minimal documentation. it was kind of a mess.
opengl (not opengl-raw) papers over a few arbitrary parts of the opengl state machine, while also not even exposing other parts of it, so that any non-trivial program would need to use both opengl and opengl-raw and just hope that their raw calls weren't mucking about with anything that opengl was assuming was safely encapsulated.
gpipe, though... the degree of encapsulation it has over actual opengl is pretty amazing. all the shaders are written in plain haskell, in the same file as the rest of your code. gpipe is really good at communicating with the haskell universe, in ways that sometimes border on the magical.
opengl made it clear that you were pulling levers on a big opengl machine, and lambdacube had you naming and dispatching uniforms according to the opengl model. in gpipe uniforms just kind of... vanish. a lot of opengl concepts vanish, and are replaced with very haskelly functions and types that behave in the way you'd expect haskell functions and types to work.there are some downsides, though. gpipe is very type-safe, but that type safety comes at a cost, and that is some of the most byzantine type signatures i've ever seen in non-theory code.
take for example texture sampling:
sample2D returns a ColorSampleable c => ColorSample x c. a ColorSample is actually just a type of Color c (S x (ColorElement c)). a ColorElement is a type family within the ColorSampleable class that has like a dozen instances that seem to map types between RGB* and * (like, RGBWord to Word). but those RGB types are a type family in the Format typeclass, and count components. so Color RGBWord a is 'actually' a V3 Word, with the V3 from the RGB part (vs. RGBA or RG or w/e which would have V4 or V2), and the Word from the ColorElement instance.gpipe functions are rife with this kind of thing, having two or three type variables that are peppered across the argument types repeatedly and bound to various typeclasses. when they all infer correctly, it's great. when they don't, i had to dig around looking: is it that it can't correctly infer the type (which it frequently can't); or am i actually giving it the wrong kind of data, and in that case what actually is the right kind of data? is it only the wrong kind of data because somewhere else i'm using a different type that causes one of these type variables to be inferred as a non-matching type, and, if so, where is that other value?
the lack of common opengl reference points can get kinda confusing too, especially in conjunction with the above. if i want to turn on the depth buffer, then i gotta be calling `drawWindowColorDepth` instead of just `drawWindowColor`, sure. but that also takes a different type of argument in several ways -- i need a FragmentStream that has a FragDepth value, and i need to provide depth comparison data for the shader pipeline. i also need to have initialized a Window with a DepthRenderable value. or if you want to turn on alpha blending, you need to provide a blending option for your `drawWindow*` calls, sure. you'll also need to have your window initialized with a `Format` type that includes an alpha component, which changes some of the types needed in your shader for certain calls (like clearing the window).
these are things you can learn from checking the type signature, and that's generally how you learn Haskell frameworks, but there's so much type information, generally obfuscated behind chains of typeclasses and type families, and it's in places that are very weird if you're already familiar with how opengl works behind the curtain. none of that is bad as such -- well, the wacky type inference is bad -- but it definitely has caused a fair number of stumbles so far.
for example, their 'hello world' demo never actually directly refers to the shader render data. a
Shader os s a has a state of s, but you can't directly look at the state data; only a few functions expose that via taking a function of type s -> {whatever} and using its result. and that's fine but in the demo that was not clear to me at all, because in the demo it's only ever indirectly mentioned with functions like id or const. and it's not like there's any particularly clever trick there, it's fairly obvious, but there are so many clever tricks going on that it really obfuscates the overall code and data flow since everything has like three type variables attached and they interact with each other in weird ways.all those types do a lot of work: they wrap up and abstract away the underlying opengl setup, and do things like make it impossible to write the wrong kind of data to a shader, or the wrong color type to a texture, or whatever. that's very nice, and useful, but it also causes some weird issues itself. partly it's that all abstractions are leaky, right, and so there's always the looming threat of, say, my shader render state taking up too many slots and so being too big to be sent to the GPU and causing an error. which is an invisible constraint until it happens, since as far as gpipe is concerned the shader render function is just a normal haskell function, it's just that if you know opengl you know that that's the entry-point into the shader system and that there's a finite amount of stuff you can stuff through it at one time.
all that being said though, this has been so much clearer, easier to use, and full-featured than any of the other haskell rendering setups i've checked out. i installed it uhh eleven days ago, and i already ported over my object rendering code, plus i set up a basic camera, plus i got textures working, UVs being synthesized for procedural models, and basic sprite font code working. some of that's just because i've been getting better as a programmer, but some of that is definitely because gpipe manages the most tedious parts of rendering and wraps up the rest of it in a way that it's difficult to break your rendering state.