3.2 KiB
layout, title, description, is_draft
| layout | title | description | is_draft |
|---|---|---|---|
| default.liquid | 2025-12-18 GLSL EDSL in Haskell part 1 | first part of GLSL EDSL development series. We initialize the project and do some planning. | true |
all code for this project is available here. Please contact me if you want an account for contribution or to file issues.
goals of this project
I want to generate GLSL from the type-safe environment of Haskell with an EDSL. Specifically, I want to functionally declare the glsl shader code I use in my game.
At the moment, that's quite a lot. Let's start with a practically bare Haskell/OpenGL project. Let's build our development environment.
mkdir hs-glsl
cd hs-glsl
# get initial dependencies
nix-shell -p cabal-install ghc
cabal init
# all defaults are ok for our needs EXCEPT the following:
# Application directory: I chose 4 then put `src`
# Version: I chose 0.1.0, breaking with PVP in
# favor of Semantic Versioning
# Category: Graphics
# I also entered my contact, username, and homepage
Great, now we have a hs-glsl.cabal, the file that defines our project. Cabal also created a number of other files and directories, namely src/Main.hs and LICENSE. Running the command cabal run will run the project, but it isn't reproducable; how can we do it with Nix? We can create a nix flake as follows:
# flake.nix
{
inputs = {
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
};
outputs = {
nixpkgs, self, ...
}: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.${system} = {
default =
pkgs.haskellPackages.callCabal2nix "hs-glsl" {};
};
devShells.${system} = {
default = pkgs.mkShell {
packages = [
pkgs.cabal-install
];
inputsFrom = [
self.packages.${system}.default
];
};
};
};
}
now nix develop puts us in a shell with all of out dependencies, and nix run should run the program. Let's use GLFW-b to create a window:
# hs-glsl.cabal
...
executable hs-glsl
...
build-depends:
base ^>=4.20.2.0
, GLFW-b <1000
...
-- | src/Main.hs
module Main (main) where
-- | windowing library
import qualified Graphics.UI.GLFW as GLFW
-- | main
main :: IO ()
main = do
-- initialize GLFW
_ <- GLFW.init
GLFW.defaultWindowHints
-- we use GLSL version 3.3 Core here
GLFW.windowHint $ GLFW.WindowHint'ContextVersionMajor 3
GLFW.windowHint $ GLFW.WindowHint'ContextVersionMinor 3
GLFW.windowHint $ GLFW.WindowHint'OpenGLProfile GLFW.OpenGLProfile'Core
-- monitor required to fullscreen at start
monitor <- GLFW.getPrimaryMonitor
-- actually create the window
Just window <- GLFW.createWindow 256 256 "hs-glsl" monitor Nothing
GLFW.makeContextCurrent $ Just window
-- enter loop
loop window
-- | loop that runs every frame
loop :: GLFW.Window -> IO ()
loop window = do
-- swap to next framebuffer
GLFW.swapBuffers window
-- call next frame
loop window
Great, a fullscreen window that never closes! I'll go ahead and add a quick key callback so that pressing esc closes the program.
-- | src/Main.hs