1 unstable release
0.1.0 | Jan 10, 2020 |
---|
#1104 in GUI
65KB
1.5K
SLoC
winflip
A Rust-y investigation into making something like https://github.com/floooh/sokol.
This is an EXPERIMENT. I just want to try porting sokol_app.h
to Rust
to learn more about how minimalistic one can make a cross-platform
window setup lib. This may someday become worth using, or maybe it
won't. Currently it only runs on Linux+X11, though it's designed to
have more backends added as necessary.
Why not winit
? Basically, as described
here, I feel
like winit
tries to do too many things perfectly on too many
platforms, and so I want to explore what something with more limited
goals would look like.
Goals
- Works on Windows, Linux, and WebGL+WASM
- Sets up window and processes events
- Sets up OpenGL context
- Smallish
- Easy to use for games
Anti-goals
- Multiple windows
- Multiple OpenGL contexts
- Any particular style of event loop
- Easy to use for native GUI apps
- Multithreading
- Perfection
Things to not worry about YET
- Gamepad support
- Mobile support
Prior art
This is mainly intended to be a straight port of sokol_app.h
but
there's a few other things in this category to consider, if only for
contrast:
winit
glutin
minifb
pixels
Also, running c2rust
on sokol_app.h
actually works pretty well!
This is a promising line of inquiry but has some problems:
sokol_app
is actively developed so we'll have to do this multiple times to incorporate future bugfixes -- can't just run it once and make it work, have to automate it to make it always work- Running
c2rust
is not trivial, it takes a fair bit of pipeline setup to do and yet more to do well - The output of
c2rust
occasionally still needs some massaging by hand to compile. Not usually very much, at least. c2rust
says it doesn't yet support Windows? And doesn't support cross-compiling either, since it can't set and unset all the magical platform-specific ifdef's that might exist in the world. Troublesome!
Current state and thoughts
As an experiment or "spike" this is more or less complete. After about
four days of work it creates a window on Linux using X11, runs an event
loop that calls user-provided callbacks correctly, and exits properly.
It doesn't have some features implemented (notably window title, hidpi
and clipboard support), and its GL context creation is buggy, but it
works if you comment out the GLX setup functions in x11::run()
. It's
also basically prototype state in terms of error handling and doesn't
let the user control the event loop, just the callbacks. Still, it
creates a window and runs an event loop, which is like 80% of what it
needs to do, and is structured so that other backends than X11 can be
added pretty easily. Most of the code is unsafe and unaudited, but
making a safe interface should be pretty simple.
In terms of dependencies, the x11-dl
crate provides most of what we
need for this. glutin_glx_sys
provides a different subset of most
of what we need, arranged just differently enough that it's not a
drop-in replacement; it has more of the GLX stuff and less of the X11
stuff. Sorry, I don't remember the exact details. So for now I just
use x11-dl
and dynamically load the GLX functions and definitions I
need myself.
Porting sokol_app.h
was kinda weird but really pretty straightforward
all in all. I'm sure there are a number of various windowing lib edge
cases it doesn't handle but I couldn't find anything obvious. X11
doesn't rear much of its reputed ugliness in this, it's just fairly
mundane clunky old C code, though some things like error handling are
pretty bad. If you chopped out 90% of X11 that isn't used anymore and
actually documented what was left, it probably wouldn't have the
gruesome reputation it does. sokol_app.h
itself is quite good C code,
excepting the usage of static globals heckin' everywhere, but at least
they're named consistently. So, the result is pretty easy to follow and
port to Rust. Porting C to Rust is once again an exercise in
remembering just how crap C really is in comparison. It's okayish by
1980's standards, but we can do far, far, far better now. RAII,
specific integer types that aren't transparently convertable to each
other, some basic traits like Clone and Drop, and real enum types make
life SO damn much better, especially when you have API's that are
designed to actually use these features. Really the biggest awfulness
of C IMO is its freakish willingness to say "yeah or that can be an
int
and that's fine" to just about any type, which makes it REALLY
HARD to build any kind of strong abstract types. That, and the criminal
lack of standard library: a portable program should not have to write
its own strncmp()
and assert()
. Ugh.
It is interesting seeing how this stacks up against winit
, as well.
winflip
is about 2000 lines of code, and would probably be 2500 were
it finished. If each backend adds another 2000-3000 lines, then
supporting Wayland, Windows, MacOS, and wasm+web-sys, that would
probably be 15k significant LOC in total. As of Jan 2020, winit
is
about 35k lines, and the maintainers themselves are not necessarily
thrilled with how complex it is. I feel that winflip
serves as a
useful data point in how lightweight something that performs the main
functions of winit
could be.
Glutin is even worse though! The glutin_*_sys
crates actually look
really useful as low-level platform bindings, without those glutin
seems to be 9000 lines of code that actually does very little. The
reason for this is historical: Back In The Day, winit
didn't exist,
just glutin
. But eventually it became desirable to separate windowing
and graphics context setup, and windowing was refactored into what is now winit
.
I think
it would be desirable
to chop a bunch of the fluff out of glutin
and make it a much more
slender library that only does graphics context setup, and the
raw-window-handle
crate now means that's possible to do. The actual
OpenGL setup in sokol_app.h
, apart from all the DLL loading that is be
handled by the glutin_*_sys
crates, is only a few hundred lines of
code. Any volunteers?
A caveat, of course lines of code is not a good proxy for complexity. But it's also all we've got. Assuming that nobody's trying to be gratuitously arcane or verbose, we can at least broadly assume that it's some sort of indirect indicator of how much stuff a program or library has in it.
One last thing... winit
's "event loop 2.0" refactor is complicated
enough that it takes a fair amount of work to
explain, even to
people who've done game or UI dev before and know what's going on under
the hood. The reason for this is basically that it tries to present one
API that works with callback-based API's such as web browsers and
Android, AND with the more traditional poll-events-in-a-loop API's like
X11 or Windows. Again, nobody's really thrilled with this but nobody's
had a better idea for structuring it that doesn't sacrifice capabilities
that are important to someone. I personally find winflip
's setup,
which just uses frame()
, update()
and event()
callbacks called in
a loop, to be far nicer to actually use and use correctly, and integrate
into other systems, but it DOES sacrifice things like smooth resizing,
some latency concerns with frame drawing, stuff like that. For my
purposes, these are sacrifices I make gladly.
It's weird to contemplate WHY I like winflip
's setup more, because I'm
not really sure. When you take a step back it's obvious that both
styles are equivalent, you can turn a polling event loop into callbacks
just by providing the event loop that calls callbacks, or do the inverse
by making the callbacks collect events into a shared queue. I THINK
that the different "feeling" because the way winit
does it, very
concrete events such as "user pressed key" are interspersed with much
more abstract events like "frame started" or "event loop cleared" which
are... really not events but rather state changes in the event loop
itself. Then when you mix in the way it uses ControlFlow
to provide
feedback about what to do next... It ends up being a somewhat
weird-feeling intertwingled state machine disguised as an event
callback. I still don't really know for sure though.
So yeah, this was fun!
Dependencies
~115KB