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);
}
Here is the WIP branch https://github.com/lukewilliamboswell/roc-ray/pull/15 and a demo using the arrow keys.
I think in many cases keyboard maps best to handlers like onKeyPress
that can update the model.
I'm not sure I follow what you mean.
Something like this maybe?
handleKeyChange : model, (model, KeyBoardKey, [Up, Down] -> model) : Task model *
Yeah. Or have a task to register a key press handler (but that might be hard to get right with current roc glue).
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 {}
This would save a lot of Task
s
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.
I feel like I would just model the raylib api directly. (though I would make a Key enum instead of using an int)
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
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
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.
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.
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
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
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.
Just don't change to drawing circles...those tend to be a lot slower
Can confirm they are slower :smiley:
Added utc nanos timestamp, and @Ian McLerran's imclerran/roc-isodate package works well :smiley:
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.
Here's the source for this example https://github.com/lukewilliamboswell/roc-ray/blob/main/examples/2d_camera.roc
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.
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.
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.
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
I would make two versions of each raw call drawCircle
drawCircles
...but a list for simplicity in all cases is also fine
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
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.
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
Still extra overhead, but probably fine.
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.
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.
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
Or maybe spawn a single thread to handle all the drawing and have roc passing commands to that thread.
I spent a while poking at it. Working around rust here seems really painful. I'm guessing what we really want is effect interpreters.
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)
It feels more like we want good glue gen, which effect interpreters are on the other end of
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
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.
I just found the raylib::ffi crate... I think that may what we need -- it's the lower level raylib-sys crate
I got it working!! :tada:
Literally everything is unsafe
... but that's fine I think.
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).
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.
Makes total sense
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.
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.
Normally -sys
crates have a feature flag to just download a library instead of compile it.
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.
It introduced a lot of additional dependencies and fragile config.
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.
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
@Brendan Hansknecht @Dan G Knutson what do you guys think?
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.
But at least the common experience is simple and reliable.
Some people may want to use the library built from source
I honestly wouldn't worry about or support this currently
Caching and vendoring both sound fine
Vendoring means more downloads overall (especially if it recorded in the repro history). How large is the lib?
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.
Ok, took some messing around... but we now have CI set up for linux, macos and windows! :smiley:
I thought it was nice being able to get the raylib source out of the repo :)
First steps with Textures... literally :smiley:
roc-ray-7.gif
Ooh, textures! You're really filling out that API
I've got an animated dude walking around the screen... :smiley: really great fun
Completed the raylib 2d camera core example, link to roc-ray implementation
roc-ray-9.gif
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: .
This is so cool!
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?
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.
its up to the host what panic does
in spirit it should never return
I'm not sure that rust panic works through ffi
Yeah, I think I just found that out the hard way
May need to do something more cursed like setjmp and longjmp
It will leak any live roc memory
This is definitely stretching the limits of my lowlevel knowledge...
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);
Claude says it's possible... I'm going to try it
Yeah, you can do it with setjmp and longjmp
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.
https://doc.rust-lang.org/nomicon/ffi.html#ffi-and-unwinding
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
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):
panic
will cause the process to safely abort.
makes me wonder why catch_unwind
works for me. Seems like it should abort, but it doesn't. Maybe the nomicon is outdated here?
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)
Rip....I guess it isn't ub anymore
What if you change the function abi to the -unwind
variant and just lie to rust
That gives the old behavior (where things work but are probably UB)
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.
Which would also be the case with setjmp/longjmp, right?
Yep
Eventually roc could theoretically generate unwind info that deals with refcounts
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.
for sure
just less convenient and slower for many use cases
easiest if it works for your use case is just arena allocations
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.
Oh, interesting! Can you link to that?
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.
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.
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.
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