# 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](https://wiki.alopex.li/TheStateOfGGEZ2020#other-stuff), 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](https://github.com/rust-windowing/glutin/issues/1243)
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](https://docs.rs/winit/0.20.0/winit/#event-handling), 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!