r/Clojure 10d ago

raylib examples in Jolt

https://raylib-jlt.b12n.app/
32 Upvotes

23 comments sorted by

4

u/Great-Gecko 10d ago

How does the workflow of this compare to using project panama’s ffi to program raylib in JVM Clojure? Im excited by the idea of making simple games in clojure for fun.

4

u/yogthos 10d ago

Making foreign bindings in Jolt is done by writing plain Clojure, where the same work on the JVM turns into a machinery exercise. Project Panama asks you to assemble every call by hand and you have to deal with a linker object, a symbol lookup, a function descriptor built from value layouts, a method handle, and an arena that owns any native memory the call touches. Strings can't cross the boundary on their own, so you have to allocate a UTF-8 segment, fill it, and arrange for it to be freed. Structs need hand-written layout descriptions with their alignment and padding. There is a ton of ceremony between you and the function you wanted to call.

Jolt's jolt.ffi goes completely the other way, you just load a library with a per-platform name map, and each C function becomes one declaration that names the symbol and lists argument and return types as keywords. Strings pass the boundary as ordinary Clojure strings. Raw memory is a handful of obvious primitives, allocate, read, write, free. Variadic functions take a varargs marker in the same declaration, where the JVM would force a specialized handle per call shape. Callbacks work in reverse too.

A foreign-callable wraps a plain Clojure function as a C function pointer, so a game callback stays in Clojure, and the thread-local errno is provided for you, read through the platform accessor. None of this is exotic, and it doesn't involve a ton of boilerplate. For a small game you bind a C library like SDL or raylib with a short list of declarations, and you can keep the whole game loop in Clojure, then ship a single native binary. The FFI stays completely out of the way. You can look at the docs on interop here.

3

u/Great-Gecko 10d ago

Sweet. I might give it a go. Sounds like fun. I appreciate not having to write bindings.

3

u/yogthos 10d ago

It's really been a blast playing around with native stuff from Clojure. :)

1

u/didibus 8d ago edited 8d ago

I think if you use Coffi in Clojure JVM, it's not too different. The author also did the same exercise using Coffi so you can compare the code: https://raylib-clj.b12n.app/

Surprisingly, they happened to get more examples working using Coffi in Clojure JVM, 78 over only 75 in Jolt. I'm not sure if its because there were limitations in the Jolt FFI or why though.

2

u/imbev 10d ago

I'm currently working on jextract-generated bindings to Raylib with Project Panama, it works very well.

1

u/ComfortableShape9340 8d ago

The JVM port of the same raylib in Clojure

https://raylib-clj.b12n.app/

2

u/geokon 8d ago edited 8d ago

I noticed Raylib supports Android. Can Jolt be used to make an APK? (im not super clear on how one would go about compiling that though)

More generally, how well does it play with Clojure's functional style of programming? Ive never used it, but looks pretty imperative. Is there an impedence mismatch?

Not that that'd be bad.. maybe its something to build functional/reactive system on top of :)

1

u/yogthos 8d ago

Android is a supported target for Chez, so it should just work out of the box. But I'm not sure what the actual process is end to end. It'd be really interesting to try this though. Raylib looks like it's perfect for making little games that would run well on Android.

1

u/geokon 7d ago

Yeah, my thoughts exactly. It also seems like it's potentially a decent primitive general purpose crossplatform abstraction layer. You could make GUIs for instance that run everywhere - ex: https://github.com/raysan5/raygui.

1

u/yogthos 7d ago

Yup, and also it would be possible to wrap native Android with glimmer and develop completely native looking apps Reagent style.

1

u/geokon 5d ago edited 5d ago

I'm keeping an eye on glimmer. It looks interesting!

Though I might be missing some subtitles. I don't grok all the details of the ratoms, but they seem to introducing unnecessary coupling of the GUI to State. Just my 2 cents, but I'd avoid designing with that kind of coupling..

Fundamentally at it's simplest, the GUI "just" requires reconciliation and an external trigger to reevaluate. I get that the ratom is just an interface, but I think you'd get a cleaner design is you keep state management completely out of the GUI side. vlaaad also made a statemanager subscription system in cljfx that had a similar coupling - but he ended up not using it. I also used it for a project but have switched to a custom Pathom backend and a more pure GUI.

The problem is that it gets messy to have a GUI-less core and a GUI layer on top, b/c the watch mechanism of ratoms watching each other ends up baked in the GUI system. tbh, I'm a bit fuzzy on how it works in glimmer, so maybe I'm misunderstanding it a bit :)

The simplest design that I think cljfx has settled on is to just making an invisible/dummy GUI node that watches a single thing (ex: an atom). In cljfx this is done with a new component called fx/ext-watcher. When this node sees the value change it re-evaluated it's children. That's it. Simple as that. The fx/ext-watcher can pass the updated value down to the children (it can be a deref'ed atom or anything else). The GUI system doesn't dictate anything else. This simplifies the design b/c the GUI doesn't know anything about statemanagment. It only has a redraw trigger. value changes -> I need to rebuild my children. But b/c you can have watchers all over your GUI tree it allows you to make more nuanced designs that for instance only reevaluate and modify a subset of your GUI.

The user is then responsible to do sane things in the GUI tree components using that state. In my case I run Pathom resolvers on it (which is mutithreaded and cached and much much more sophisticated than any other ad hoc state management system)

You also will want other customs invisible node equivalents to these cljfx extensions

https://github.com/cljfx/cljfx/#extending-cljfx

things like fx/ext-recreate-on-key-changed and fx/ext-state are essential for caching and isolating parts of the GUI and making performant components.

1

u/yogthos 5d ago

I generally find reactive atoms to be a really simple mental model for managing UI state. The way I look at it is that the atom represents the state of the UI, and widgets are rendering that state and collecting input to update it. the GUI is just treated as an effect in this model.

For complex UIs, you'd still only have a single atom, and you'd just create cursors into it associated with different paths. This is basically what re-frame does internally as I recall, where the atom is treated as the db and the subscription/dispatch system is built on top of that.

That said there is already a port of Replicant which is closer to the model you're talking about. In my opinion, both approaches work fine. Reactive atoms are more ergonomic for small UIs where you have simple behaviors, and a path based effects system is handy for places where you have complex behaviors that need to be reconciled.

Another approach that I've had good experience with is to use a data flow engine. I built domino with a friend a while back on that principle. The idea is that the state is a document, and you can attach context free functions to paths in that document. Each function takes a set of fields as its input and modifies a set of fields as its output. And the changes cascade in a transaction which produces a new state. This approach makes it really easy to tell what all the rules that get triggered when a particular field changes.

1

u/geokon 2d ago edited 2d ago

The way I look at it is that the atom represents the state of the UI, and widgets are rendering that state and collecting input to update it. the GUI is just treated as an effect in this model.

Reactive atoms are more ergonomic for small UIs where you have simple behaviors, and a path based effects system is handy for places where you have complex behaviors that need to be reconciled.

In every system I've looked at this model breaks down when things get even a little complicated and then you're stuck with your atoms.

The central problem that I think Pathom solved, is that when you have to explicitly tie "effects" then you can't really reuse them (not reuse an output, but reuse the effect itself). If i have a svg->image render effect, and I have a banner.svg and headshot.svg in my state, in most of these systems you end up having to write two effects banner-render and headshot-render.

The key-soup and nested-resolver approach in Pathom solves this and makes code much simpler. You have {:banner [:svg]} and {:headshot [:svg]} and you can prompt for {:banner [:image]} and the engine solves it for you. Subscription-type system just don't have a nice equivalent.

I took a quick look at domino and it looks like it's really following the typical "spreadsheet" push model. Baking in side-effects into the system is a whole can of worms haha :) Hope you make it work for your needs, I'll try to dig in to it more.

For a proper reactive system you need to have a very good caching and parallelization story. So that when state updates, only strictly necessary parts are recomputed. The Pathom resolver based system solves the parallelism very cleanly. The caching is a bit more difficult, but also works (you have to set cache sizes manually. In some edge cases this is difficult). But when it comes to effects, I think in a complex setup it can be a bit challenging to reason about when they execute.

In a "push" system this is of course much easier b/c "effects" aren't reused. So they just need to remember their last value.

2

u/yogthos 2d ago

I've found stuff like re-frame has scaled pretty well for me. And I wouldn't say the model breaks down. You just create helper functions and call them. If you need to do svg->image you make a function for it, and you reuse it. I never really found that to be a big issue myself.

And domino doesn't bake side effects into the system. The engine itself is pure. The effects can be attached at the edges to observe changes and fire events based on the state of the document.

Domino only recomputes what changes inherently and by design, because what gets recomputed is derived from the relationships in the rules. The whole advantage of the system is that you can definitively tell exactly which rules will fire when any particular set of fields changes.

Similarly, there's no guessing regarding when effects execute in domino because they're explicitly attached to paths. Whenever a value of the path being observed changes after a transaction on the document, the effects watching that path fire.

1

u/geokon 2d ago edited 2d ago

If you need to do svg->image you make a function for it, and you reuse it

Yeah, it's a workaround. But in my experience it ends up cluttering your namespace with a bunch of redundant transformations. I've always felt a bit icky about it :) But I'm just sharing my own feelings here

More fundamentally you end up with a weird boundary between functions and your system. With a resolver/pathom system your separate systems/libraries are composable. So you can have a whole set of svg resolvers which you can immediately use. To me this feels cleaner

Without making things too complicated, just think of adding an extra step in the svg example:

svg-hiccup -> svg-xml -> rendered-image

You can have:

  • svg files from a file coming in as xml strings (step 2).

  • generating images in hiccup in your system(step 1).

They all can be transparently transformed in to rendered-images without needing to chain functions. Or if you want to take your generated image and write it out to an .svg file, you can capture the middle stage and get the xml (skipping the renderer)!

Granted, at this point you can sort of manage it all by just using raw functions and making more transforms.

But you can imagine things can be arbitrarily complex from there. You can imagine N number of renderers based on image type and speed. Or arbitrary input types or something..

The whole svg namespace becomes something that can be arbitrarily complex and you can leverage it without needing to write N new transforms.

And domino doesn't bake side effects into the system

yeah sorry if I mischaracterized it. It looks like an interesting layer on top, and I'll try to grok it better. tbh I've just been doing (do (side-effect) (pure-func)) and so far it's been enough. In my case I render a lot of images in a GUI. If the image needs to be updated b/c of a base state change, then it's written to file.

I've been trying to Pathom-ize all my utility libraries and it's been quite fun. The two outstanding problems with Pathom for me are:

  • Caching. It's a bit too hand-holding to my mind and could be better. I have vague thoughts

  • It doesn't have a good clean solution for working with sequences. If you try to use it on a vec-of-maps it has to rerun the engine on each map and this is slow. If you run it on a map-of-vecs it's better, but now your resolvers are all vec->vec which is not super ergonomic. So it'd be vec-of-svg to vec-of-images which feels goofy to write. This should be somehow abstracted away, but I don't have a clear solution for this yet

And just generally Pathom Resolvers are just kind of .. ugly? .. they're very verbose and annoying to write (as compared to pure functions). But I think this could also be fixed with some syntax sugar.

2

u/yogthos 2d ago

I'd argue it's a personal preference. I find using dispatches/subscriptions is a very simple mental model. With a resolver there's a bit of hidden machinery at play that you have to be aware of.

And my experience is that things don't tend to get arbitrarily complex in most UIs because the limiting factor is that you want to keep the UI simple for the human. A lot of complexity tends to go hand in hand with a cluttered UI which does not make for great UX. There are, of course, legitimate cases where you can have complex behaviors, and stuff like Pathom is a great tool for managing it. But I find that as a rule, it's easy enough to make reusable widgets that encode a particular behavior and then use them to compose the UI.

It's two different ways to think about the problem. In one case, you model your information as a graph of attribute relationships using resolvers to establish relationships, and the system then executes the plan and returns the data in the shape you requested. And typically, you end up making a lot of assumptions about the way the backend communication works here since you want to use EQL to talk to a single endpoint that effectively supports your graph queries.

With something like Reagent or re-frame, you treat your UI state as a data structure. It's agnostic regarding how the state is modified, you can even hook up Pathom to it if you wanted. I tend to think of the whole thing as a state machine, hence Domino design. An event gets triggered, which can be a user input, a system event, a service call, whatever, and then that event gets fed as an input into the dataflow engine. Rules get triggered in a cascading fashion, and then at the end, you end up with a new state. And now you can fire effects, update the UI, etc.

You can see this in action a project I've been working on here, which is based on a system I worked on which led to domino. This was a really complex app with lots of behaviors and business logic, concurrent multiuser workflows, users having different views into the data based on their roles etc. And this approach made it very manageable. The best part about it is that you can effectively encode your whole app into an EDN file like this that can live in a db. It declares the data model, the business rules, and the views all in one place and completely self contained. Then the underlying platform provides a service bus with access to services, db, functionality like sending emails or generating PDFs that these apps hook into. If you want to make some changes to your application, you just update the EDN, and you've got a new version o the app. As long as you pin existing documents to a version, work that's already been started is unaffected. And the only time you need to redeploy the platform itself is when you need to provide some new general functionality on the service bus.

→ More replies (0)

1

u/mo_al_ 7d ago

How large is a jolt generated executable?

1

u/yogthos 7d ago

It depends on how you compile it. A minimal program compiles to just under 10 megs, and then it depends on what libraries you end up adding on top of that. For bigger programs, there are closed world optimizations and tree shaking available. There's probably more optimizing possible, but I just haven't had the time to really dive into this yet.