Files
nook/posts/20251218-glsl-edsl-p1.md
2025-12-18 22:35:32 +01:00

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