Stream: platform development

Topic: roc-ray


view this post on Zulip Luke Boswell (Oct 12 2024 at 00:54):

I'm trying to find a nice API for working with the keyboard...

I've implemented this which works nicely... but it's only "hey this key was pressed" and doesn't really help if I want to detect say Crtl-C or Shift-K etc as I need to know if a key is being pressed which feels like a different thing.

Just trying to think of something that maps nicely as a Task.

Here's the raylib API functions which seem related.

## Get's the set of keys pressed since last time this function was called.
## Key presses are queued until read.
getKeysPressed : Task (Set KeyBoardKey) *
/// Check if a key has been pressed once
pub fn isKeyPressed(key: KeyboardKey) bool {
    return cdef.IsKeyPressed(key);
}

/// Check if a key has been pressed again (Only PLATFORM_DESKTOP)
pub fn isKeyPressedRepeat(key: KeyboardKey) bool {
    return cdef.IsKeyPressedRepeat(key);
}

/// Check if a key is being pressed
pub fn isKeyDown(key: KeyboardKey) bool {
    return cdef.IsKeyDown(key);
}

/// Check if a key has been released once
pub fn isKeyReleased(key: KeyboardKey) bool {
    return cdef.IsKeyReleased(key);
}

/// Check if a key is NOT being pressed
pub fn isKeyUp(key: KeyboardKey) bool {
    return cdef.IsKeyUp(key);
}

view this post on Zulip Luke Boswell (Oct 12 2024 at 00:56):

Here is the WIP branch https://github.com/lukewilliamboswell/roc-ray/pull/15 and a demo using the arrow keys.

roc-ray-4.gif

view this post on Zulip Brendan Hansknecht (Oct 12 2024 at 01:00):

I think in many cases keyboard maps best to handlers like onKeyPress that can update the model.

view this post on Zulip Luke Boswell (Oct 12 2024 at 01:05):

I'm not sure I follow what you mean.

view this post on Zulip Luke Boswell (Oct 12 2024 at 01:08):

Something like this maybe?

handleKeyChange : model, (model, KeyBoardKey, [Up, Down] -> model) : Task model *

view this post on Zulip Brendan Hansknecht (Oct 12 2024 at 01:09):

Yeah. Or have a task to register a key press handler (but that might be hard to get right with current roc glue).

view this post on Zulip Luke Boswell (Oct 12 2024 at 01:21):

Maybe it would be easiest to pass in the current "state" of the keyboard (and mouse gamepad etc) on every render?

render : Model, Keyboard -> Task Model {}

view this post on Zulip Luke Boswell (Oct 12 2024 at 01:23):

This would save a lot of Tasks

view this post on Zulip Brendan Hansknecht (Oct 12 2024 at 02:18):

Oh, I see raylib really doesn't model keys in a handler friendly form. It expects you to requests the key events you care about on each frame.

view this post on Zulip Brendan Hansknecht (Oct 12 2024 at 02:19):

I feel like I would just model the raylib api directly. (though I would make a Key enum instead of using an int)

view this post on Zulip Brendan Hansknecht (Oct 12 2024 at 02:19):

If you want to be really fancy, some sort of record builder to get a bunch of keys might be nice. But I also think direct tasks should be fine

view this post on Zulip Brendan Hansknecht (Oct 12 2024 at 02:21):

All that said, it seems that raylib fundamentally maps all of the keys into two arrays, previousKeyState and currentKeyState. You might be able to directly pass those into roc if you wanted and handle all the checking in roc

view this post on Zulip Jared Cone (Oct 12 2024 at 03:00):

I've seen raylib has a function to check for new key presses (pops one at a time from a stack), but it doesn't seem to have a function for key releases. My plan was to have the host pass a message to roc on key press, and also keep track of which keys are pressed. Then every frame, the host asks raylib if any of those previously pressed keys are still pressed. If not, send a key release message to roc.

view this post on Zulip Luke Boswell (Oct 13 2024 at 07:57):

Ok, I've gone a bit crazy today and implemented a bunch of things for roc-ray....

There's now the some common stateful things being passed in to each call to render. That reduces the number of Tasks that we need, and makes the API nicer to use for an app author.

I've also implemented an effect to take a screenshot... however I think the effect is actually running first even though I place it at the end of the task chain. I suspect this is my issue because whenever I take a screenshot it's just the cleared screen. I need to investigate this further.

Checkout the updated pong example if you're interested.

view this post on Zulip Luke Boswell (Oct 13 2024 at 10:36):

I can get almost 4,000 frames per second doing normalish things with raylib. So I thought I'd see how much drawing slows down with lots and lots of things on the screen.

Here's a test program I wrote to try and stress test it a little. Each frame I'm generating a list of random rectangles and then drawing them.

From my very unscientific testing... my laptop runs out of memory and crashes before it gets below 60 fps.

Here's a screenshot with 100,000 rectangles randomly generated on each frame.

Screenshot 2024-10-13 at 21.27.10.png

view this post on Zulip Brendan Hansknecht (Oct 13 2024 at 17:32):

Just don't change to drawing circles...those tend to be a lot slower. Though raylib probably has an optimized way to draw them, so maybe not an issue

view this post on Zulip Brendan Hansknecht (Oct 13 2024 at 17:32):

I suspect this is my issue because whenever I take a screenshot it's just the cleared screen. I need to investigate this further.

You may be taking the screenshot before the screen actually renders? But not sure how all of that is wired together.

view this post on Zulip Luke Boswell (Oct 13 2024 at 22:52):

Just don't change to drawing circles...those tend to be a lot slower

Can confirm they are slower :smiley:

view this post on Zulip Luke Boswell (Oct 13 2024 at 23:20):

Added utc nanos timestamp, and @Ian McLerran's imclerran/roc-isodate package works well :smiley:

roc-ray-5.gif

view this post on Zulip Luke Boswell (Oct 14 2024 at 04:59):

Added Camera's and Mode2D drawing... so now you can have separate World and Screen spaces to draw to and provide better support for different screen resolutions etc.

roc-ray-6.gif

view this post on Zulip Luke Boswell (Oct 14 2024 at 04:59):

Here's the source for this example https://github.com/lukewilliamboswell/roc-ray/blob/main/examples/2d_camera.roc

view this post on Zulip Luke Boswell (Oct 14 2024 at 21:40):

Looking to crowdsource some opinions on an idea for roc-ray....

I'm considering changing the API for app authors from this

Program state : {
    init : Task state {},
    render : state, PlatformState -> Task state {},
}

To something more TEA-like.

Program state : {
    init : Task state {},
    update : state, PlatformState -> Task state {},
    render : state -> List Elems,
}

Elems : [
    Circle { pos : Vec2, ... },
    Rect { pos : Vec2, ... },
    Text { pos : Vec2, text : Str, ... },
    ...
]

My main motivation is that I have been thinking about how to improve the drawing to be more efficient and avoid deeply nested chains of Tasks. This approach would allow me to batch draw Tasks to one per "type", and can all be done in the platform, while keeping the API across the host boundary very simple.

In future if we needed more performance we can change the host API and do more efficient things without affecting any app authors.

What do people think? Is this a bad idea?

I haven't really investigated how this will work with multiple Camera's and Textures and 2D/3D drawing modes... but I am thinking this will make it easier because I can modify the type of render so that it isn't possible to mess up the order of things as an application author.

view this post on Zulip Brendan Hansknecht (Oct 14 2024 at 22:54):

Assuming we change to purity inference and synchronous platform calls (which so the coroutines idea), I don't think you will have to worry about the cost here. I will still add a batched draw call though that can take a list of items to draw.

view this post on Zulip Luke Boswell (Oct 14 2024 at 22:54):

After writing this out... I'm not sure this is a good idea any more. I'm going to think about it some. My main concern is the ordering that raylib wants makes batching things difficult.

view this post on Zulip Luke Boswell (Oct 14 2024 at 22:55):

add a batched draw call though that can take a list of items to draw.

Good idea. I can just make all the draw functions take a RocList

view this post on Zulip Brendan Hansknecht (Oct 14 2024 at 22:56):

I would make two versions of each raw call drawCircle drawCircles...but a list for simplicity in all cases is also fine

view this post on Zulip Luke Boswell (Oct 14 2024 at 22:56):

I think that has minimal impact anyway... from this **

Raylib.drawText! { text: "Hello World", x: 300, y: 50, size: 40, color: Navy }
Raylib.drawRectangle! { x: 100, y: 150, width: 250, height: 100, color: Aqua }
Raylib.drawRectangleGradient! { x: 400, y: 150, width: 250, height: 100, top: Lime, bottom: Green }
Raylib.drawCircle! { x: 200, y: 400, radius: 75, color: Fuchsia }
Raylib.drawCircleGradient! { x: 600, y: 400, radius: 75, inner: Yellow, outer: Maroon }

to this...

Raylib.drawText! [{ text: "Hello World", x: 300, y: 50, size: 40, color: Navy }]
Raylib.drawRectangle! [{ x: 100, y: 150, width: 250, height: 100, color: Aqua }]
Raylib.drawRectangleGradient! [{ x: 400, y: 150, width: 250, height: 100, top: Lime, bottom: Green }]
Raylib.drawCircle! [{ x: 200, y: 400, radius: 75, color: Fuchsia }]
Raylib.drawCircleGradient! [{ x: 600, y: 400, radius: 75, inner: Yellow, outer: Maroon }]

edit ** I'm not so sure this is minimal on performance

view this post on Zulip Luke Boswell (Oct 14 2024 at 22:57):

Maybe the List version would be less efficient if it's only used for one thing everytime, because its adding an allocated type in the middle.

view this post on Zulip Brendan Hansknecht (Oct 14 2024 at 22:58):

That's fair. Just a ton of extra malloc and free call (but they all should be in the fast bins for single element allocations). So in practice, it should keep reusing the same memory location for all of those list with a good malloc impl

view this post on Zulip Brendan Hansknecht (Oct 14 2024 at 22:58):

Still extra overhead, but probably fine.

view this post on Zulip Luke Boswell (Oct 14 2024 at 22:58):

change to purity inference and synchronous platform calls (which so the coroutines idea)

Based on this... I'm thinking I'll just leave it. We should see how that pans out in practice.

view this post on Zulip Luke Boswell (Oct 15 2024 at 01:24):

Also -- I think I'm going to try replacing Zig with Rust for roc-ray. I've been cooking ideas, and my lack of zig experience is a bit of an challenge. I think with rust (and glue) I'll be more confident adding features quickly.

view this post on Zulip Luke Boswell (Oct 15 2024 at 03:41):

Got something minimal working in https://github.com/lukewilliamboswell/roc-ray/tree/spike-rust

$ git checkout spike-rust
$ roc build --no-link examples/basic-shapes.roc
$ cp examples/basic-shapes.o app.o
$ cargo run

But I think I'm going to need to use the ThreadSafeRefcountedResourceHeap or something to keep track of all the resources from Raylib. Like Textures or DrawHandles

view this post on Zulip Luke Boswell (Oct 15 2024 at 03:47):

Or maybe spawn a single thread to handle all the drawing and have roc passing commands to that thread.

view this post on Zulip Luke Boswell (Oct 15 2024 at 05:17):

I spent a while poking at it. Working around rust here seems really painful. I'm guessing what we really want is effect interpreters.

view this post on Zulip Brendan Hansknecht (Oct 15 2024 at 05:28):

Why? I don't understand the dilemma? Why is it different from basic cli or any other current platform? effect interpretters really only help with async (and raylib isn't async)

view this post on Zulip Sam Mohr (Oct 15 2024 at 07:00):

It feels more like we want good glue gen, which effect interpreters are on the other end of

view this post on Zulip Luke Boswell (Oct 15 2024 at 07:13):

The raylib-rs library models things in a way to make everything safe... so you need to have a handle that is !Sync and !Send

view this post on Zulip Luke Boswell (Oct 15 2024 at 07:51):

Here's an example. https://github.com/raylib-rs/raylib-rs/blob/a170ceed3b4453c3b5287fbc049f2472d3d3425a/raylib/src/core/drawing.rs#L23

You call begin_drawing and then get a RaylibDrawHandle... and you need that handle to do any drawing.

view this post on Zulip Luke Boswell (Oct 15 2024 at 07:54):

I just found the raylib::ffi crate... I think that may what we need -- it's the lower level raylib-sys crate

view this post on Zulip Luke Boswell (Oct 15 2024 at 08:16):

I got it working!! :tada:

view this post on Zulip Luke Boswell (Oct 15 2024 at 08:17):

Literally everything is unsafe ... but that's fine I think.

view this post on Zulip Brendan Hansknecht (Oct 15 2024 at 17:03):

Literally everything is unsafe

This is why zig is probably a better medium for platforms a lot of the time (same as with the roc's stdlib).

view this post on Zulip Luke Boswell (Oct 15 2024 at 19:05):

If we had roc_std in zig and glue for zig then I also think zig is an excellent choice for hosts. I enjoyed working with it, but I'm more comfortable with rust at this point, and for roc-ray I really want to work with larger more complex types across the host interface and glue is really appreciated.

view this post on Zulip Brendan Hansknecht (Oct 15 2024 at 19:12):

Makes total sense

view this post on Zulip Luke Boswell (Oct 16 2024 at 10:27):

Ok, completed the switch across to Rust... it's looking pretty nice I think. https://github.com/lukewilliamboswell/roc-ray

For platform and app authors the dependencies are down to roc, rust, and build-essential + git which is pretty nice.

I realised we can use the release builds from the raylib team directly... and then just generate our own bindings from the C header files. So much simpler to work with and removes a lot of crazy (large and messy) dependencies.

I haven't finished the build config for Windows... but have tested and that is also working nicely. Only set up for x64-linux and aarch64-mac because that's all I have been able to test on. Next up I'd like to investigate support for wasm/browsers.

view this post on Zulip Brendan Hansknecht (Oct 16 2024 at 14:34):

Luke Boswell said:

I realised we can use the release builds from the raylib team directly... and then just generate our own bindings from the C header files. So much simpler to work with and removes a lot of crazy (large and messy) dependencies.

I'm surprised there isn't a raylib-sys or similar that is exactly that.

view this post on Zulip Brendan Hansknecht (Oct 16 2024 at 14:35):

Normally -sys crates have a feature flag to just download a library instead of compile it.

view this post on Zulip Luke Boswell (Oct 16 2024 at 15:39):

That is what raylib-sys does, but it builds everything from source using Cmake which was a pain to get working reliably across different OS's.

view this post on Zulip Luke Boswell (Oct 16 2024 at 15:40):

It introduced a lot of additional dependencies and fragile config.

view this post on Zulip Luke Boswell (Oct 18 2024 at 04:27):

Question for the crowd ... I'm currently downloading the libraylib.a from the official releases from GH. This is working ok, but it's kind of buried in the build script. I've been thinking it's not great hitting github everytime we build, even though I cache the binary in target/. I'm wondering if I should instead include/vendor the raylib releases in the repository so after cloning there is no need to make any more http requests to get the files.

view this post on Zulip Luke Boswell (Oct 18 2024 at 04:27):

I'd love for the releases to have a hash or something so we know if they change. I figure if it's in the repository then it wont change under us without a PR.
edit -- I guess I could just hash it myself and store that with the download URL

view this post on Zulip Luke Boswell (Oct 18 2024 at 04:28):

@Brendan Hansknecht @Dan G Knutson what do you guys think?

view this post on Zulip Luke Boswell (Oct 18 2024 at 04:31):

Some people may want to use the library built from source, so I could include a flag to not copy the vendored version across and leave it up to the user then how they provide the library.

view this post on Zulip Luke Boswell (Oct 18 2024 at 04:31):

But at least the common experience is simple and reliable.

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 05:45):

Some people may want to use the library built from source

I honestly wouldn't worry about or support this currently

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 05:46):

Caching and vendoring both sound fine

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 05:47):

Vendoring means more downloads overall (especially if it recorded in the repro history). How large is the lib?

view this post on Zulip Brendan Hansknecht (Oct 18 2024 at 05:48):

Lib looks decently big. Some people may prefer just do download and cache the one version they need. So I would maybe push for just caching.

view this post on Zulip Luke Boswell (Oct 18 2024 at 07:36):

Ok, took some messing around... but we now have CI set up for linux, macos and windows! :smiley:

view this post on Zulip Jared Cone (Oct 18 2024 at 15:34):

I thought it was nice being able to get the raylib source out of the repo :)

view this post on Zulip Luke Boswell (Oct 19 2024 at 04:21):

First steps with Textures... literally :smiley:
roc-ray-7.gif

view this post on Zulip Sam Mohr (Oct 19 2024 at 04:50):

Ooh, textures! You're really filling out that API

view this post on Zulip Luke Boswell (Oct 19 2024 at 04:51):

I've got an animated dude walking around the screen... :smiley: really great fun

view this post on Zulip Luke Boswell (Oct 20 2024 at 10:56):

Completed the raylib 2d camera core example, link to roc-ray implementation
roc-ray-9.gif

view this post on Zulip Luke Boswell (Oct 24 2024 at 05:08):

Another example -- 2D split-screen multiplayer :smiley: source code

Apologies for the chaotic/erratic player movement... I can't drive a keyboard two handed :sweat_smile: .

roc-ray-10.gif

view this post on Zulip Aurélien Geron (Oct 25 2024 at 00:56):

This is so cool!

view this post on Zulip Luke Boswell (Nov 10 2024 at 03:41):

Does anyone know what happens when roc_panic is called? Say we have called into roc, and then roc panics.... does roc return anything from that original call?

I.e. if the API is:

renderForHost! : Box Model, Effect.PlatformStateFromHost => Result (Box Model) Str

And we get a roc_panic during the call to renderForHost! does that return?

view this post on Zulip Luke Boswell (Nov 10 2024 at 03:46):

I'm guessing I need to panic! or something and then catch that somehow back in the place where we called into roc or something like that... but I really have no idea, just guessing.

view this post on Zulip Ayaz Hafiz (Nov 10 2024 at 04:09):

its up to the host what panic does

view this post on Zulip Ayaz Hafiz (Nov 10 2024 at 04:09):

in spirit it should never return

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 04:10):

I'm not sure that rust panic works through ffi

view this post on Zulip Luke Boswell (Nov 10 2024 at 04:10):

Yeah, I think I just found that out the hard way

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 04:10):

May need to do something more cursed like setjmp and longjmp

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 04:11):

It will leak any live roc memory

view this post on Zulip Luke Boswell (Nov 10 2024 at 04:11):

This is definitely stretching the limits of my lowlevel knowledge...

view this post on Zulip Luke Boswell (Nov 10 2024 at 04:13):

Basically, I'm not sure how to get back to the point where I called into roc;

// roc might panic in this call... I can do stuff in `roc_panic` ... but I need to get back here
let result = render_caller(self.model, &mut self.state);

view this post on Zulip Luke Boswell (Nov 10 2024 at 04:14):

Claude says it's possible... I'm going to try it

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 04:15):

Yeah, you can do it with setjmp and longjmp

view this post on Zulip teskje (Nov 10 2024 at 16:39):

I had that same issue and catch_unwind seems to work for me. In roc_panic I simply convert the Roc panic into a Rust panic (source) that I then catch at the invocation side (source).

That seems to work for me. I can't say anything about the safety of unwinding through FFI boundaries though.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 16:44):

https://doc.rust-lang.org/nomicon/ffi.html#ffi-and-unwinding

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 16:47):

Sounds like they now have partial solution allowing unwinding c++ stack frames. That said, a roc stack frame is not a c++ stack frame....so probably ub

view this post on Zulip teskje (Nov 10 2024 at 17:02):

Hm, this part:

If an unwinding operation does encounter an ABI boundary that is not permitted to unwind, the behavior depends on the source of the unwinding (Rust panic or a foreign exception):

makes me wonder why catch_unwind works for me. Seems like it should abort, but it doesn't. Maybe the nomicon is outdated here?

view this post on Zulip teskje (Nov 10 2024 at 17:08):

Ah, never mind. It was me who had an outdated Rust version :)

thread caused non-unwinding panic. aborting.
fish: Job 1, 'cargo run' terminated by signal SIGABRT (Abort)

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:08):

Rip....I guess it isn't ub anymore

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:09):

What if you change the function abi to the -unwind variant and just lie to rust

view this post on Zulip teskje (Nov 10 2024 at 17:11):

That gives the old behavior (where things work but are probably UB)

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:19):

May technically not be ub. Might just be that we have no unwind info, so it just dumps all the functions and leaks anything from the roc side.

view this post on Zulip teskje (Nov 10 2024 at 17:23):

Which would also be the case with setjmp/longjmp, right?

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:26):

Yep

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:26):

Eventually roc could theoretically generate unwind info that deals with refcounts

view this post on Zulip teskje (Nov 10 2024 at 17:34):

Since the platform also controls allocation, it seems feasible that it could keep track of all allocations performed by roc and manually free the leaked ones after a crash.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:50):

for sure

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:50):

just less convenient and slower for many use cases

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:50):

easiest if it works for your use case is just arena allocations

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 17:52):

Also, the bigger problem is ensuring all resource are cleaned up more so than memory. For rust we have a solution for that in the roc std library for rust.

view this post on Zulip teskje (Nov 10 2024 at 18:27):

Oh, interesting! Can you link to that?

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 18:56):

Library: https://github.com/roc-lang/roc/blob/main/crates/roc_std_heap/src/lib.rs

Use in basic cli to automatically deal with files: https://github.com/roc-lang/basic-cli/blob/4048641e0067e2be35955226acd50e11eba1cf45/crates/roc_host/src/lib.rs#L34

This enables automatically closing files when roc frees a file object. It would also be possible to keep track of a live list or scan the entire heap during a panic to free all currently open files.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 18:57):

Still not the best solution, but a step closer. That plus arena allocation would let a platform fully clean up everything in roc after a panic without too much overhead.

view this post on Zulip teskje (Nov 10 2024 at 19:21):

Thanks! My current use case uses roc only to run pure functions, so I think I only need to worry about allocations. But in general it would be good to have a blessed way to handle roc crashes in platforms that want to keep going and not just exit the process.

view this post on Zulip Brendan Hansknecht (Nov 10 2024 at 19:24):

Yeah, currently it is 100% up to the platform and only easy if arena allocations works for you.


Last updated: Jul 05 2025 at 12:14 UTC