Brillo 2.0 - Production ready 2D graphics

I’m incredibly excited to announce Brillo 2.0! :tada:

This is a massive release and the result of our efforts to fix all the issues that make Haskell desktop apps feel like second-class citizens:

  • Make anti-aliasing the default for all primitives
  • Add support for true type fonts
  • Add window refresh callback for live resizing support on macOS
  • Add cursor shape support (pointer, crosshair, …)
  • File system support:
    • Add dialog to pick files or directories
    • Support opening a save file dialog
    • Add support for dropping of files/directories
  • Add SVG export support with comprehensive test suite
    • Add simple canvas editor to demonstrate SVG export
  • Export all fields of BitmapData type
    (allows executing image manipulation operations on the pixel data)
  • Add brillo-export to export several image formats
    • Add support for WebP file format
  • Add support for rendering non-convex polygons
  • Add several example games
    (2048, Chess, Pac-Man, Snake, Tetris, Tic Tac Toe)
  • Remove all smart constructors that only wrap a data constructor

It should also work on all platforms now!
To make sure, please download the pre-built Tetris example and let me know if it works for you!

I’m also building Perspec – a desktop app to correct the perspective of images – with it and will soon release a very capable new version! :raising_hands:

46 Likes

Congrats! Looks like a lot of updates to Gloss (CHANGELOG.md > 2024, 2026).

I confirm that Tetris ran on an arm mac, macos tahoe 26.2. (How-to: download, try to run it once, go to mac’s Settings > Privacy & Security, scroll to the bottom, allow brillo-tetris, run again, confirm dialogs.)

5 Likes

Are shaders on the roadmap?

Looks good! I’m not sure about the “placement” though. Is this a full replacement for gloss? Is gloss somehow less maintained nowadays? In other words, why fork?

1 Like

Yes, gloss is unfortunately less maintained, and depending on it is becoming more and more of a pain. As a maintainer with several libraries depending on gloss I’m very happy about this development.

3 Likes

Now they are! :grin:

Here is a PR for adding support for shaders: Add support for custom shaders by ad-si · Pull Request #31 · ad-si/Brillo · GitHub
Is this how you envisioned it?

5 Likes

That seems like a good simple way to do it - and you really raise the ceiling of the sorts of things you can do visually with brillo thanks to it.

I think a good “test” is to see what it’d look like for brillo to have 2D lighting of sprites with normal maps (e.g. those generate by laigter GitHub - azagaya/laigter: Laigter: automatic normal map generator for sprites!)

2 Likes

I wanted to try out things, remove code, refactor stuff, etc without having to think of legacy users. That’s why I thought proper fork with a different name would be more suitable. The gloss team is more than welcome to adopt/copy any changes they’d like to see in Gloss!

1 Like

Hello, I’m glad to see gloss being given some love!

I’m wondering about the motivation behind this change: to me the philosophy of gloss is that Picture is an abstract datatype. I shouldn’t need to care about how it’s represented and what its constructors are. Exposing these constructors means that clients of the library may start deconstructing pictures, hindering possible future refactorings.

Or did you maybe come across situations in which deconstructing pictures was useful?

1 Like

Deconstructing / pattern matching was already supported as the constructor was already exposed. So they were not even true smart constructors. Given that, I don’t really see a point in wrapping it with a useless polygon = Polygon.

Also, I think graphic programming is one of the few areas where you actually want to know what’s going on under the hood so you don’t accidentally introduce performance bottlenecks.

  • Picture data constructor → Directly handled by Brillo
  • Function → Some code will be executed to create the Picture. You might be able to implement it in another way that better suits your use-case (lower resolution polygon, shader, bitmap, …)
1 Like

This is great!
Quick question before I dive into the code, is there any support for buffering of OpenGL commands?

(Original gloss was getting annoyingly slow lately because of the OpenGL1-style immediate rendering commands.)

Thanks!

No, unfortunately the entire rendering pipeline is still immediate mode. But yeah, making this more efficient would be a great addition for the next release. Any help would be highly appreciated! :blush:

Btw, can we elaborate the constraints and design space there?

I was already trying this with one project and in the end I had to migrate to a custom gl renderer (ugly but works), one thing learned in the process was that even the haskell runtime wrappery may add a (potentially serious) bit of overhead there (see Use `unsafe` FFI imports to remove lot of RTS overhead? · Issue #26 · ekmett/gl · GitHub but not sure how OpenGL backend fares there).

I’m not very good with GL2/3 (I stopped doing graphics sometime when openGL1 was OK and restarted now in vulkan times :smiley: ) but from what I understand there’s no good way to do actual command buffers in GL3, just some (very fat) prepared-command-buffers that the shaders can expand?

Last time I looked into that, it should be relatively easy to introduce opengl display lists (an immediate mode feature! deprecated in 3.1, yay!). Looks like the whole Picture can be compiled into it. Then a new primitive DisplayList can be added to call them (even as a part of other pictures. Not sure if they can be nested though).

Hi all, my first post/comment here. I’m learning Haskell, and for other reasons than graphics, but I have, in an earlier life, written an immediate mode OGL renderer, before converting it to OpenGL 3.2 or so.

The main idea is the number of draw calls must be minimized, and that there is a prioritized order of state updates. Data (vertices, colors, other data that can be used by shaders) stored in Vertex Buffer Objects is the cheapest to deal with, these are streamed across the other state (in the GPU). If you can rewrite stuff to instancing, even better.

Then matrices, textures and at last shaders. Absolutely sort drawables by shader. An effect of this is that it is common to make shaders of maximum code size, such that they cover as many use cases as possible.

The basic premise for instancing is that if there are shapes that can be generated (by geometry/tesselation shaders) given just a few datapoints (e.g. position and orientation, texture coordinates, …), you can put out millions of things in a single draw call. I used this for map elements (lines for example).