Closed smcameron closed 4 years ago
Regarding Web{sockets,GL,Assembly}, I do have a few more notes to add:
I'd be happy to help wherever I can!
Out of curiosity, could you point me to the relevant functions you use for rendering text using OpenGL? I can take a look and see about porting them to WebAssembly as a proof of concept.
Also- I just discovered that OpenGL features can be emulated in OpenGL ES using Regal. might be worth looking into.
Yeah. You will be happy to find out that I am so incredibly lazy that my method of drawing text is simply .... to draw lines. If you can draw lines, you can draw anything!
It's in snis_graph.c
sng_abs_xy_draw_string() is one function that's used a lot to put text on the screen, which in turn calls sng_abs_xy_draw_letter(), which in turn calls sng_current_draw_line(), which calls graph_dev_draw_line() which is defined in both graph_dev_gdk.c and graph_dev_opengl.c for the limited client and opengl client, respectively. The opengl version is just adding data to a data structure that later gets drawn by draw_vertex_buffer_2d() which does the actual drawing in batches, to some extent. The non-opengl case is simpler, it just calls gdk_draw_line().
The fonts themselves are defined in snis_font.c which contains a bunch of data to describe what line segments are needed to draw each character. There's a snis_make_font() in there function which does some pre-scaling of the fonts to different sizes.
The limited client uses only the following graphics primitives:
gdk_draw_line, gdk_draw_rectangle, gdk_draw_point, gdk_draw_arc, gdk_draw_polygon,
graph_dev.h defines the interface that graph_dev_gdk.c and graph_dev_opengl.c implement. Might make sense to write graph_dev_webasm.c as a 3rd graphics back-end, not sure.
I'm not sure I'm using much from OpenGL that isn't also in OpenGL ES 2.0. I'm not using glBegin or glEnd, or any fixed function stuff. It's kind of hard to tell what the differences are though.
It might make sense to switch away from gtk to SDL2. If I'm not mistaken, SDL2 is already ported to webassembly. The hardest part of this would probably be the keyboard and mouse input handling. The mesh_viewer program already uses SDL (however, it's SDL 1.x, not SDL 2, I'm pretty sure and there are some significant differences). But it might be the fastest sort of test case to port mesh_viewer to sdl2 then to webassembly.
It might make sense to switch away from gtk to SDL2. If I'm not mistaken, SDL2 is already ported to webassembly.
Yeah, that does seem like a good idea.
I put together a super quick PoC based on your font generation code using WebGL2, SDL2 and Emscripten. I have it configured to target OpenGL ES 3.0. It doesn't use graph_dev_opengl, but I suppose it easily could...
The repo is here and a running version of it in Github Pages can be found here. Would be interested to hear your thoughts!
Nice! First thoughts: I am impressed!
I am thinking though that the path forward is likely to write graph_dev_webasm.c to implement the API graph_dev.h defines (unless there's a reason this doesn't make sense that I'm as yet unaware of -- e.g. maybe graph_dev_opengl.c is ok as is?). And of course for a first shot across the bow (esp. considering it's been only a matter of a few hours since the topic was initially broached), that's too tall of an order, so this is fantastic progress! And very impressive that you grok my weird code so quickly to smash it into this new form!
I'm more interested in solutions that re-use as much of the code as possible rather than re-writing a separate webassembly version of the existing code, within reason (For example, breaking the different stations into separate programs might not really make sense... once the graphics primitives are supported, I don't really see an obvious reason to break them up.)
For a sort of roadmap, maybe:
If that can be made to work, then,
It should also be a requirement to have native versions of these co-exist with the webassembly versions, of course. I would not want to sacrifice the native code for webassembly (the native code is easier to debug, if nothing else.)
I converted mesh_viewer from SDL1 to SDL2.
It occurs to me that snis_client reads data from a lot of files, the majority of it being texture image data, but various other text files, and audio files as well. My (very limited) understanding of webassembly tells me this would have to change as there is no access to the filesystem. So that means, presuming this webassembly idea can be made to work, every single instance that someone runs will have to fetch all this data from a webserver somewhere. It occurs to me that this could get pretty expensive to host. The textures for a typical solarsystem are typically about 60-80Mb. Multiply that by a large number of clients, and things start to add up. Will have to figure out a way for people to host this data on their client machines and serve it up to themselves, so they only download it infrequently by the usual "make update-assets" method.
My (very limited) understanding of webassembly tells me this would have to change as there is no access to the filesystem.
Atually, these files can be embedded at compile time and accessed per the usual POSIX file APIs. See here:
https://emscripten.org/docs/porting/files/packaging_files.html
So that means, presuming this webassembly idea can be made to work, every single instance that someone runs will have to fetch all this data from a webserver somewhere. It occurs to me that this could get pretty expensive to host. The textures for a typical solarsystem are typically about 60-80Mb. Multiply that by a large number of clients, and things start to add up. Will have to figure out a way for people to host this data on their client machines and serve it up to themselves, so they only download it infrequently by the usual "make update-assets" method.
If the data were precompiled, I would expect that it would be cached locally by the web browser. This is something we can definitely look into.
Ok, thanks. Just want to make sure we think about it before rather than after I get a huge bill.
We could always provide configuration and documentation as far as how to set up ones own server that hosts all the assets as an alternative.
I've done some poking at Emscripten and mesh_viewer. A few major things come up that will likely need investigation:
Otherwise, the results of my attempts to compile mesh_viewer have been promising. After tearing apart the Makefile a lot, I got it to compile. Next step is to undo my Makefile carnage with what I've learned and try it again.
Well I have good news for you. Lua only runs as part of snis_server. It is definitely not needed by mesh_viewer or snis_client, so this should be resolvable by convincing it not to attempt anything Lua related. If the Makefile somehow thinks it needs Lua for mesh_viewer or snis_client, then that's a Makefile problem. Could be all those makefile variables that are set via pkg_config stuff that runs regardless of what you're trying to build. I know I see the pkg_config complaining a bunch when I build snis_server on my digitil ocean server which doesn't have any of the dependencies that snis_client needs installed (but I just ignore those complaints).
Otherwise, sounds like great progress. I had tried as well with mesh_viewer, but was stymied by libpng (didn't try very hard though because i don't really know what I'm doing.)
I started on a port of snis_client to SDL2, but then kind of backed off the first attempt as it's a bit daunting as there's all the color related stuff, bunch of keyboard related stuff, screen, monitor, aspect ratio... just on and on, and I can't figure a way to do it piece by piece, seems to require a big bang approach, which just makes me feel like I'm smashing things apart with no clear path to putting them back together, which... not a great feeling, ha.
Maybe this commit will help with Lua:
Otherwise, sounds like great progress. I had tried as well with mesh_viewer, but was stymied by libpng (didn't try very hard though because i don't really know what I'm doing.)
Try using this:
$ CFLAGS="-s USE_SDL=2 -s MAX_WEBGL_VERSION=2 -s USE_LIBPNG=1 -std=gnu99 -s WASM=1 --memory-init-file 0 -s TOTAL_MEMORY=268435456 -s USE_PTHREADS=1 -Wl,--shared-memory,--max-memory=268435456" emmake make bin/mesh_viewer
I also had troubles with it trying to find qsort_r. I just added the code from this commit to a local file and included it directly.
Also ove note, I'm using emcc from emsdk instead of from ubuntu's repositories. Should be newer than what's available in Aptitude.
Maybe this commit will help with Lua:
Hmm... I'll give it a look.
On that note, maybe it makes sense to do some refactoring/reorganization to make snis_client a bit more modular? It is a pretty huge source code file- maybe some of that could be split off into it's own files/folders? I know that's a pretty big refactoring task, but it might make future porting easier.
maybe some of that could be split off into it's own files/folders? I know that's a pretty big refactoring task, but it might make future porting easier.
Maybe. Though I don't think it would help with gtk -> sdl2. I don't know that file size by itself matters much (I recognize that this is a bit unorthodox as opinions go, perhaps). The main thing making it hard to break up is the large number of global variables that are assumed to be accessible, and breaking it up and just making those variables extern doesn't really do anything -- that's just moving things around, and ok they're in different files, but... so what? So reasonable subsystems and seams where sensible APIs can be defined need to be identified and built to make such a break up worth while, which isn't easy. Clearly snis_client (and snis_server) are both complex systems that evolved from simple systems (which is the only way working complex systems can come to exist according to Gall's law). Not to say that evolving them further and breaking things apart sensibly isn't worthwhile, but... it is difficult, and it's not obvious to me where those breaks should be. (In my defense, I do break bits and pieces off now and then, for example at least the UI system is pretty modular). So, to sum up, I suppose I am not really very excited about a kind of "hey, let's break it up for the sake of breaking it up" approach, but am not opposed to finding natural seams and defining sensible API boundaries and breaking off bits and pieces at a time, and if that slowly ends up breaking things up to where you're no longer looking at a 20kloc file, then so be it.
I have gotten snis_client to compile and run with SDL2 using some 5 year old patches from @jv4779 as a guide (surprisingly still pretty close to correct, though not close enough to attempt mechanical application).
A screen shot:
Keyboard stuff is not working yet, controlling the window size needs some work, probably other problems I don't yet know about, but good progress, it compiles and runs.
I've got the keyboard mostly working with SDL2 now (mouse clicking on text boxes to give them focus doesn't work yet, but you can TAB to them.) and things seem to mostly work, can connect and drive the ship around and so on. But something's off a bit because performance feels crappy (but it's making 30 FPS just fine, in fact will make 60 FPS just fine if you "set use_60_fps=1" on the demon screen.) I think it's something related to the keyboard, but not really sure. At 60 FPS it smooths out and feels a bit better a little less jittery, but there are plenty of things still in the game that kind of assume 30 FPS, so running it at 60 FPS is not ideal, esp. considering I'd like to be able to run on a Raspberry Pi 4.
I think I have almost all of the SDL2 port done. Only thing left is I can't figure out how to make the window have a fixed aspect ratio (gtk had hints and the window manager could enforce this for you, sdl doesn't, and trying to resize the window yourself in response to a window resize event is... interesting.) And the "performance" issue remains. But I now have a pretty good theory about what's causing it (haven't verified it, so I might be wrong, but it seems to fit my observations.)
With gtk, everything was callbacks. All the user input events were callbacks, even the 30Hz function to refresh the display was a callback. With SDL, nothing is callbacks. The main loop currently looks roughly like this:
while (1) {
poll_and_process_any_queued_events(); /* all user input comes in via this. */
sleep_until_it_is_time_to_draw_the_screen();
draw_the_screen().
}
So the SDL code just lets user input pile up while it is sleeping, while GTK could handle user input at any time. I think it's the batching of event processing at 30Hz intervals is what's making feel bad. So I think the fix will be to poll events more often during the time we're not drawing the screen.
With gtk, everything was callbacks. All the user input events were callbacks, even the 30Hz function to refresh the display was a callback. With SDL, nothing is callbacks.
Have you looked into SDL_AddEventWatch
? that might allow callbacks like you used before. The docs say that thread safety may be something you need to take into account, though. As for the 30hz display refresh- I think the 30hz timer can be done through SDL_AddTimer()
Only thing left is I can't figure out how to make the window have a fixed aspect ratio (gtk had hints and the window manager could enforce this for you, sdl doesn't, and trying to resize the window yourself in response to a window resize event is... interesting.)
Best path I can see in this case is to use SDL_RenderSetLogicalSize()
within your resize event handler to ensure the aspect ratio is maintained. This probably would introduce letterboxing, but I'm not sure there are any other good options.
So the SDL code just lets user input pile up while it is sleeping, while GTK could handle user input at any time. I think it's the batching of event processing at 30Hz intervals is what's making feel bad. So I think the fix will be to poll events more often during the time we're not drawing the screen.
One approach, if event Callbacks don't end up becoming an option, is to do input processing for the next frame right after SDL_GL_SwapWindow(window)
is called. It might add a 1 frame delay between input and rendering, but it should mean that any input is processed in the blank space between two frames.
Yeah. gtk had gtk_threads_enter() and gtk_threads_leave() for thread safety of its internal stuff. Anyway, I'm not quite sure my theory is correct, because I just tried a few things that should have fixed it if my theory was correct, and no luck so far.
The "be very careful" advice is not reassuring, because who knows if SDL internals can withstand such usage without going insane since they do not have the equivalent of gtk_threads_enter() and gtk_threads_leave(), it would seem SDL is not designed with that kind of usage in mind.
I may post the patches in case other people want to play with them.
Did you have any luck moving input processing until after SwapWindows? I'd be curious to see if that leads to any performance gains.
Ok, this is a problem (don't know if it is the problem)...
int advance_game(void)
{
int time_to_switch_servers;
static int skip = 0;
/* Bit of a hack to enable 60 fps. advance game now gets called at 60Hz always, but
* skip every other update if we want to run the game at 30Hz.
* Also, we increment timer only every other frame when running at 60 fps because
* there are many, many places where we assume that timer updates at 30Hz.
*/
if (!use_60_fps) {
if (skip) {
skip = !skip;
return TRUE;
}
frame_rate_hz = 30;
skip = !skip;
timer++;
} else {
When running at 30Hz, we're only calling advance_game() at 30Hz in the SDL code, and it's skipping half the calls because it is supposed to be getting called at 60Hz.
Which would explain why it feels fine at 60Hz.
Ok, fixed the "performance" problem. Poll events and call advance_game() at 60Hz, and draw the screen at 30Hz and it's fine.
Alright, here are the patches in case anyone wants to play with them. Note, this kills the non-opengl limited client, which is unfortunate as that would run on lower-spec machines for the less demanding stations.)
Ugh, this is discouraging.
scameron@wombat /tmp/patches $ ls -ltr
total 616
-rw-r--r-- 1 scameron scameron 110060 Apr 20 14:22 switch-from-gtk-to-sdl2
-rw-r--r-- 1 scameron scameron 9955 Apr 20 14:22 more-progress-on-sdl-conversion
-rw-r--r-- 1 scameron scameron 2248 Apr 20 14:22 call-advance-game-at-correct-frequency
-rw-r--r-- 1 scameron scameron 16256 Apr 20 14:22 remove-invasive-displaymode-pointer-from-snis_ui_element
-rw-r--r-- 1 scameron scameron 23399 Apr 20 14:22 allow-snis-graph-to-support-multiple-windows
-rw-r--r-- 1 scameron scameron 386803 Apr 20 14:23 allow-a-second-main-screen
-rw-r--r-- 1 scameron scameron 9576 Apr 20 14:23 debug-window-weirdness
-rw-r--r-- 1 scameron scameron 59304 Apr 20 14:23 windowize-entity
scameron@wombat /tmp/patches $ diffstat *
Makefile | 50
entity.c | 277 +--
entity.h | 48
entity_private.h | 11
graph_dev.h | 30
graph_dev_gdk.c | 1
graph_dev_opengl.c | 492 +++--
joystick_config.c | 2
joystick_config.h | 2
opengl_cap.h | 2
snis.h | 7
snis_button.c | 76
snis_button.h | 18
snis_client.c | 4125 +++++++++++++++++++++++++-------------------------
snis_gauge.c | 42
snis_gauge.h | 4
snis_graph.c | 305 +--
snis_graph.h | 63
snis_keyboard.c | 450 ++---
snis_label.c | 6
snis_label.h | 2
snis_pull_down_menu.c | 74
snis_pull_down_menu.h | 13
snis_sliders.c | 100 -
snis_sliders.h | 6
snis_strip_chart.c | 38
snis_strip_chart.h | 4
snis_text_input.c | 155 -
snis_text_input.h | 14
snis_text_window.c | 28
snis_text_window.h | 4
snis_ui_element.c | 132 -
snis_ui_element.h | 56
33 files changed, 3347 insertions(+), 3290 deletions(-)
And what do I have to show for that? A pile of crap. SDL is a pile of crap. There's no way to constrain the aspect ratio of a window resize in SDL. GTK can do it (you tell it to give the window manager a hint, and then the window manager does it.) Also, cannot debug the multi-window stuff... and besides its tentacles go everywhere, so I'm not happy with it anyway.
Hmm.
Well, I was able to solve the aspect ratio problem by going around behind SDL2's back and talking to the window manager directly. Means linking in -lX11 though.
constrain-aspect-ratio-via-xlib.txt
When I left California 2 years ago, I came this close to throwing away my X-windows books. So close that I did throw away my Motif books, but I was like, "eh, X-windows is still kicking... I'm gonna keep these." And now here they are coming in handy, first time since the early '90s. Who'd a thunk it.
When I left California 2 years ago, I came this close to throwing away my X-windows books. So close that I did throw away my Motif books, but I was like, "eh, X-windows is still kicking... I'm gonna keep these." And now here they are coming in handy, first time since the early '90s. Who'd a thunk it.
Wow... nice!
On the matter of window hints, did you get a chance to try SDL_RenderSetLogicalSize()
?
I also did a bit more research into SDL_AddTimer
and multithreading. Apparently the timer callback is executed by SDL in another thread by SDL. If we're sure to wrap any modifications to the game's state in a pthread mutex and perform any graphics operations on the main thread, we should be fine as far as multithreading goes.
On the matter of window hints, did you get a chance to try SDL_RenderSetLogicalSize()?
No, but upon inspecting the cursory documentation, I would guess that it's just scaling things.
Apparently the timer callback is executed by SDL in another thread by SDL. If we're sure to wrap any modifications to the game's state in a pthread mutex and perform any graphics operations on the main thread, we should be fine as far as multithreading goes.
This is basically what I was doing with GTK already, so the locks are already in place to make this happen. This could potentially allow event callbacks to run in parallel with the advance_game() + rendering, as they did with GTK) but I don't think it really buys that much as the event handling is typically pretty trivial in terms of the amount of compute required. It was kind of a theory I had awhile back as to why I was seeing weird behavior, but it was really due to a bug of my own making (as noted here https://github.com/smcameron/space-nerds-in-space/issues/277#issuecomment-615890734 )
So I think the SDL code is more or less at parity with the GTK implementation at this point (still need to do more testing though). (We do lose the non-opengl client though, which is not great.)
My main complaint at this point is that my implementation of multiple window support is too complicated (and currently buggy). I'd like to see a tangible benefit from SDL2 over and above the GTK implementation (as opposed to just parity on opengl and total loss of non-opengl) before committing to it completely. This multi-window thing could be that tangible benefit, but I need to debug it before proceeding because I want to be sure the fix for the bug does not require major changes -- as if I hadn't already made major changes, but it's getting ridiculous.
I noticed that CPU usage seems a lot higher when running a single client with two windows vs. two separate clients. I suppose this might be that the single client is "spikier', taking up more of a single CPU and whereas the two client system is spread out among the CPUs a lot more evenly. I guess. The multi-window client also appeared to degrade in performance over time, which I don't really have any explanation for. This is all pointing me away from the multi-window client -- code is quite a bit more complicated and bug-prone than the single window client, and if it performs worse, the only thing left to recommend it is bandwidth savings, but that savings could in theory also be had with multiple clients with a local proxy server (that I haven't really thought deeply about writing yet.)
And that means I do not yet have any tangible benefit for the SDL port. The closest thing to a benefit is a theoretical ease of porting to windows, which I don't even have a windows machine, so that might not even be a benefit, as if it does get ported to windows by some well meaning soul, I will have no way to debug problems that only happen on windows.
Another weird thing I noticed about SDL2. If I alt-tab, the SDL2 app goes to the bottom of the stack of windows instead of just swapping places with the next lower window. Super annoying because you can't just swap back and forth between two different apps by alt-tab.
No, but upon inspecting the cursory documentation, I would guess that it's just scaling things.
I'm not quite sure scaling is the best description. I think it just ensures that the output viewport (in screen space) matches the screen size. For example, if we wanted to force a 16:9 aspect ratio, and I had, for some reason, a 9000x1080 screen, we could set the logical size to 1920x1080 to get the aspect ratio we wanted. On my screen, I'd have a 1920x1080 image in the center, with 4410 pixels of black letterbox on either side of the middle.
Also, could you push a separate branch with the current state of your SDL2 port? i'd like to look at some of the performance stuff you were talking about. I also have some ideas as for instrumenting the code to better facilitate graphics/performance optimization... :)
I'm not quite sure scaling is the best description. I think it just ensures that the output viewport (in screen space) matches the screen size.
You certainly cannot tell that from the documentation, but perhaps you have direct experience. My X11 method does not cause letterboxing, it just prevents the window from being any size that does not match the specified aspect ratio.
Here is the branch for the SDL port:
https://github.com/smcameron/space-nerds-in-space/commits/sdl2-conversion-2020
Up to commit
is where it's at about parity with the GTK branch (no performance problems noticed here.)
Commits after that are all about making the client multi-window. Some of the commits are a bit of a mess still, so if you build on top, consider the ground beneath to be unstable.
If I alt-tab, the SDL2 app goes to the bottom of the stack of windows instead of just swapping places with the next lower window.
It would appear that this is because X11 doesn't offer any simple primitives that would allow anything better in an easy way. It must be my XFCE that's normally restacking the windows, I suppose.
Looks like I might be able to make it work with a combination of XQueryTree() and XRestackWindows().
Beware of making snis closely related to Xorg as it will need to run under wayland at some point.
@MCMic will cross that bridge when it appears. Right now I don't consider wayland to even exist.
Hum, it's already the default on several distributions I think. And on most GNU/Linux phones out there or in the making.
Migrating to SDL will make the game more portable I think, but if at the same time it becomes dependent upon X11 the opposite will happen.
All this X11 stuff is under ifdef and just makes up for minor/gross deficiencies of SDL. If I can't at least achieve parity with gtk, then sdl gets booted.
Hmm, it seems alt-tab behavior only does the wrong thing if the window is full screen. (by which I mean SDL_WINDOW_FULLSCREEN_DESKTOP). If it's not fullscreen, it seems to behave sanely.
This is not what I am seeing, but it is disgusting:
https://www.reddit.com/r/gamedev/comments/8ioav6/sdl2_help_in_alttab_fullscreen/
If we were talking about textures,
some platforms and/or drivers (e.g. Windows D3D, Android under some conditions), will lose/destroy the accelerated video context when your application is switched out.
In this case, you are expected unload all your textures when your app begins to switch out, and reload all your textures when you come back.
Ah, I think SDL2 on Alt-tab is not lowering the window in the stack, it is minimizing the window. Same thing happens if you ctrl-left-arrow to switch to another desktop. That is not how GTK behaves. GTK allows fullscreen windows to behave just as any other windows, be raised and lowered, and you can ctrl-left-arrow or ctrl-right-arrow to other desktops without auto-minimizing a fullscreen window. So with SDL2, we lose the limited client, AND we get super annoying window behavior. And no known advantages other than supposedly easier to port to windows, which nobody is going to do.
AHA!
export SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0
fixes it. Why the fuck isn't that the default????
Pushed a new branch: https://github.com/smcameron/space-nerds-in-space/tree/sdl2-conversion-2020-04-22-B
Didn't want to risk attempting to overwrite the history of "sdl2-conversion-2020-04-22" and potentially screwing it up and accidentally doing something much more horrible, so I just made a new branch "sdl2-conversion-2020-04-22-B"
@MCMic you may appreciate that I isolated the X11 stuff into its own little ghetto: https://github.com/smcameron/space-nerds-in-space/commit/7f7b2ee23a63927aead9c021ae779c0e34fb1eaf
Filed a bug report against SDL regarding default value of SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS on linux. https://bugzilla.libsdl.org/show_bug.cgi?id=5106
SDL port is on master now. Limited client is dead.
It might be possible to compile snis_client to webassembly. Probably there would need to be some modifications for this. Worth looking into to see if there are any hard parts that make it impossible.
https://webassembly.org/getting-started/developers-guide/
If successful, it would allow people to play the damn thing in a web browser!