Stream: show and tell

Topic: Terrocotta


view this post on Zulip Romain Lepert (Aug 02 2026 at 00:15):

Hi everyone,

I'd like to share with you Terrocotta: a Roc native UI library.
I have been working on it for the past +1month and I think is in a good enough shape to share.

At its core terrocotta is a port of Clay to pure Roc, rendered using roc-ray. It is as faithful to the original implementation as possible and adds some niceties on top: MVU, event bindings, text style inheritance, etc.

The demo examples currently run at 300-600 fps (terrocotta: roc build --opt=speed, roc-ray: zig build -Doptimize=ReleaseFast) but there is probably quite some room for improvement since Clay advertises 10_000 fps.

I hope that it can help the UI effort to some extent. For those interested, I recommend reading the docs/architecture.md for internal explanations.

I also sometimes dump some Roc experience feedback to #24

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:04):

Wow, looks great!

Screenshot 2026-08-02 at 11.04.16.png

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:04):

I guess I need to make a roc-ray release with an optimized host :sweat_smile:

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:25):

Luke Boswell said:

I guess I need to make a roc-ray release with an optimized host :sweat_smile:

Here's a fresh release https://github.com/lukewilliamboswell/roc-ray/releases/tag/0.8.2

view this post on Zulip blu (Aug 02 2026 at 01:41):

Does this build to both native and web? How might you compare/contrast this with something like iced? Big fan of these "write once, run anywhere" libraries esp. Elm-inspired, and I've been looking for a good roc library for this, but roc-iced seems to be only old roc compiler and unmaintained. If it supports web/wasm, I'll give Terracotta a shot!

view this post on Zulip blu (Aug 02 2026 at 01:43):

Btw the code examples you have are so much easier to understand than the original Clay ones. Elm-architecture + Multiplatform + functional/no runtime panic guarantees should be the future IMO. Good stuff!

view this post on Zulip blu (Aug 02 2026 at 01:46):

@Luke Boswell I looked into this the other day and pretty sure that roc-ray doesn't yet compile to WASM/web, though raylib itself can? Which I assume means it just hasn't been implemented yet. Was wondering if that was on the roadmap or if I'm thinking about that correctly?

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:48):

It used to target web too, but I deleted all that when I migrated to the new compiler just to keep the scope simpler.

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:49):

If anyone is interested in adding that I'd love it ... but it's not something I'm working on or anything

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:56):

Luke Boswell said:

Luke Boswell said:

I guess I need to make a roc-ray release with an optimized host :sweat_smile:

Here's a fresh release https://github.com/lukewilliamboswell/roc-ray/releases/tag/0.8.2

Something went wrong and the host is still the debug version... I'm looking into it

view this post on Zulip Luke Boswell (Aug 02 2026 at 02:24):

Ok here's a better fix ... from 40 up to 900 frames per second

https://github.com/lukewilliamboswell/roc-ray/releases/tag/0.8.3

Screenshot 2026-08-02 at 12.24.13.png

view this post on Zulip blu (Aug 02 2026 at 02:28):

If anyone is interested in adding that I'd love it ... but it's not something I'm working on or anything

I might look into this at some point. Write once, run anywhere UI story in a functional language is very compelling (and if I'm not mistaken one of the reasons Roc was created, feeling the limitations of Elm to be only web, right?). I guess this is sort of the inverse

view this post on Zulip Luke Boswell (Aug 02 2026 at 02:44):

Also -- heres a PR that fixes the ARC bug and leaks in Roc these examples exposed https://github.com/roc-lang/roc/pull/10539

Thank you @Romain Lepert

view this post on Zulip Romain Lepert (Aug 02 2026 at 06:44):

As always you are quick on the draw Luke. Thank you !

view this post on Zulip Romain Lepert (Aug 02 2026 at 06:50):

blu said:

Does this build to both native and web? How might you compare/contrast this with something like iced? Big fan of these "write once, run anywhere" libraries esp. Elm-inspired, and I've been looking for a good roc library for this, but roc-iced seems to be only old roc compiler and unmaintained. If it supports web/wasm, I'll give Terracotta a shot!

It should be possible since the original clay has both a canvas renderer and a html/css renderer. Given the immediate mode nature only the canvas makes real sense but you lose the web accessibility, bot crawling, etc. The html/css renderer might only make sense for static websites maybe.

view this post on Zulip Anton (Aug 03 2026 at 12:49):

feeling the limitations of Elm to be only web

Yes indeed, although Roc was not made with a UI focus in mind as I remember.

view this post on Zulip Richard Feldman (Aug 03 2026 at 14:37):

yeah my feeling at the time was "I already have an Elm-like experience for Web UIs: Elm!" :smile:

that said, I do want something for native GUIs and I also appreciate that there's a lot of interest in having the same language on the server and on the client, including browser-based clients

view this post on Zulip Nicola 'tekNico' Larosa (Aug 05 2026 at 13:32):

Terracotta

Apparently the idea is:

Terracotta + Roc -> TerROCotta

view this post on Zulip blu (Aug 05 2026 at 22:40):

that said, I do want something for native GUIs and I also appreciate that there's a lot of interest in having the same language on the server and on the client, including browser-based clients

Totally fair, and thinking about this more maybe the HTML/JS/CSS trifecta vs. native UI architectures are just too conceptually different to unify cleanly. I guess I was just reading the iced philosophy and started dreaming of UItopia.

view this post on Zulip blu (Aug 05 2026 at 22:41):

And in that UItopia I'd rather write in a Roc-like friendly syntax than Rust :sweat_smile:

view this post on Zulip Romain Lepert (Aug 06 2026 at 15:11):

Contrary to immediate mode Terrocotta, iced.rs is retained mode. Nonetheless when targeting the web it uses WebGPU/WebGL, not HTML+DOM. That means it has the same restrictions as Terrocotta which hypothetically could do Canvas2D or WebGL or WebGPU.

view this post on Zulip Romain Lepert (Aug 06 2026 at 15:21):

By restrictions I mean things like: SEO, browser translation, printing, password managers, autofill. Things of that nature

view this post on Zulip Romain Lepert (Oct 13 2026 at 19:49):

Terrocotta just got its first release 0.1.0.

It is coming with some nice upgrades

and a suite example apps:

view this post on Zulip Aurélien Geron (Oct 13 2026 at 19:56):

Wow, this is really impressive! I particularly like the IDE demo. :star_struck:

view this post on Zulip Romain Lepert (Oct 13 2026 at 20:01):

FPS for the interested:

I will be using these apps as benchmark for potentially performance sensitive changes:

view this post on Zulip Romain Lepert (Oct 13 2026 at 20:06):

Aurélien Geron said:

Wow, this is really impressive! I particularly like the IDE demo. :star_struck:

thank you. I am quite happy with how the proof of concept IDE turned out. Also with the 240 FPS.

view this post on Zulip pixelsprout (Oct 13 2026 at 20:21):

This looks really awesome, I was thinking Clay would be a good fit for a game engine I'm working on, so this certainly gives me confidence in the approach.

view this post on Zulip Romain Lepert (Oct 13 2026 at 20:45):

yes, clay is a good fit for that ! I do plan do add a game-like demo to show how it composes over a roc-ray game. wouldn't you want to contribute to roc-ray's 3D capabilities ?

view this post on Zulip pixelsprout (Oct 13 2026 at 21:04):

I do like what roc-ray is doing and keen to contribute if I can! I only discovered roc-ray after working on an engine myself, I was building the engine part in Odin before building the Roc platform part. Rocco also has a different approach to roc-ray and aims to avoid side-effects by just updating a Model and Scene per game tick. That being said this is still very experimental and roc-ray is far further along and builds on an awesome game library.

view this post on Zulip Richard Feldman (Oct 14 2026 at 17:44):

@pixelsprout I really like that you made rendering be a pure function! In general I think "rendering is a pure function" takes the best advantage of Roc's design, although roc-ray unfortunately can't really do that because it maps directly to raylib's very side-effectful API :smile:

view this post on Zulip Romain Lepert (Oct 14 2026 at 19:55):

roc-ray could just as well expose draw commands as data instead of direct draw functions. In both cases the host must make the final GPU call. Unlike model update, rendering is not “pure” conceptually because it has side effects. Also a command list is an allocation with copy and realloc as capacity grows. So I am curious what makes it more adequate to roc’s design in your view ?

view this post on Zulip pixelsprout (Oct 14 2026 at 20:26):

Yeah, roc-ray could take the same approach, but then it would stop being a faithful port of raylib. Useful software needs side effects, so in Rocco I push them to the edge, where the engine runs them. Drawing has side effects, but describing a frame doesn't, and the Roc side of Rocco stops at the describing part.

Returning a description also pays off on performance. The host interpolates between the last two Scenes, so it never needs to hold an old Model, and the Model stays at a refcount of 1. That lets Roc safely mutate it in place and skip the copies. As for the command list allocating, I measure that in CI: 2 or 3 allocations per step, 0 reallocs, and the host reads the list straight out of Roc memory without copying it.

Keeping step pure gives us replay as well: log the inputs, replay the log, and you end up in exactly the same state. It also means we can test the game without the engine at all, which is great for agents, because they don't need to see the game running to test how it behaves.

view this post on Zulip Richard Feldman (Oct 14 2026 at 20:47):

Romain Lepert said:

I am curious what makes it more adequate to roc’s design in your view ?

in general, Roc lets you do more with pure functions than you can do with effectful functions - you can run them at compile time, they're easier to test, those tests can never flake, there are caching benefits to tests, etc...

I/O absolutely requires effects, so there's no opportunity to get these benefits there. But rendering not only doesn't require I/O, it's almost always a mistake to do I/O in the middle of rendering

view this post on Zulip Richard Feldman (Oct 14 2026 at 20:48):

so because it doesn't require I/O, it's certainly possible (with tradeoffs of course) to have a rendering API that only uses pure functions.

and if you do that, you get all the benefits of pure functions!

view this post on Zulip Richard Feldman (Oct 14 2026 at 20:49):

so that's what I mean by that.

view this post on Zulip Luke Boswell (Oct 14 2026 at 20:52):

I thought I tried having a pure render and there was some reason why it didn't work well at the time. Maybe that is no longer the case. It might have been laying out fonts or something like that.

view this post on Zulip Luke Boswell (Oct 14 2026 at 20:53):

The app author facing side could also be pure and then the platform translate that into the hosted effects underneath. Basically there are a lot of different ways you could do things.

view this post on Zulip Luke Boswell (Oct 14 2026 at 20:54):

I've definitely tried a lot of different things -- I would not say roc-ray's approach is the best. I'm hoping people improve on that a lot. I'd consider it a successful project if people enjoy building things with it now, and then someone makes something better later that is inspired by it.

view this post on Zulip Richard Feldman (Oct 14 2026 at 20:58):

it's definitely harder to design a pure API on top of an effectful host API, especially while trying to keep performance high :smile:

view this post on Zulip Richard Feldman (Oct 14 2026 at 20:59):

so it's not like it's all upside, but I do think the benefits to app authors are great if you can pull it off!

view this post on Zulip pixelsprout (Oct 14 2026 at 21:07):

Luke Boswell said:

I've definitely tried a lot of different things -- I would not say roc-ray's approach is the best. I'm hoping people improve on that a lot. I'd consider it a successful project if people enjoy building things with it now, and then someone makes something better later that is inspired by it.

I definitely learnt a bunch from roc-ray, I also think leaning into the API is a good choice imo. People who are already familiar with raylib can pick it up far more easily.

I'm finding working on rocco challenging at times as I don't want the design to limit the game making part too much.

It's making progress however, we now have PBR, specular highlights, tone mapping and currently working on shadows.
Screen Recording 2026-10-15 at 10.01.29 AM.mov

view this post on Zulip Romain Lepert (Oct 15 2026 at 09:08):

Rocco aims to avoid side-effects by just updating a Model and Scene per game tick

I believe roc-ray also aims to update Model and render it per game tick.

roc-ray update! is currently effectful only to allow certain synchronous effects like window_read_clipboard!, trace_begin!, etc. . It is not a very big difference.

The current main distinction is that roc-ray's render! : Frame, Model -> {} targets effectful draw call while rocco's view : Model -> Scene outputs a list of draw commands for the engine to execute.

pixelsprout said:

Returning a description also pays off on performance. The host interpolates between the last two Scenes, so it never needs to hold an old Model, and the Model stays at a refcount of 1. That lets Roc safely mutate it in place and skip the copies. As for the command list allocating, I measure that in CI: 2 or 3 allocations per step, 0 reallocs, and the host reads the list straight out of Roc memory without copying it.

I don't think the Scene approach is uniquely capable of keeping refcount of 1. roc-ray also keeps refcount 1 because update!() and render!() happen sequentially and render!() releases refcount for next update!() (aka no copies).

Regarding the command list allocating, I am not referring to the host boundary, but rather that view() will be appending draw values to the list continuously. Roc does a 2x geometric capacity growth anytime you exceed the current list capacity. For O(1000) elements which is likely for rich scenes, that is >10 growth, each of which will realloc and may require to move the list in memory : 0 → 2 → 4 → 8 → 16 → 32 → 64 → 128 → 192 → 288 → 432 → 648 → 972 → 1458. Toy CI tests probably won't show that. So code should instantiate List.with_capacity, avoid intermediate command lists and List combinators like map, keep_if.

Richard Feldman said:

in general, Roc lets you do more with pure functions than you can do with effectful functions - you can run them at compile time, they're easier to test, those tests can never flake, there are caching benefits to tests, etc...

I/O absolutely requires effects, so there's no opportunity to get these benefits there. But rendering not only doesn't require I/O, it's almost always a mistake to do I/O in the middle of rendering

So all in all my read of the two rendering approaches is:

Pros:

Cons:

Is that fair ?

view this post on Zulip Richard Feldman (Oct 15 2026 at 13:00):

capabilities to restrict things like I/O help, yeah :+1:


Last updated: Oct 15 2026 at 17:56 UTC