-
Notifications
You must be signed in to change notification settings - Fork 87
[emscripten] Web build #1776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
[emscripten] Web build #1776
Conversation
Flamefire
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting approach. I added some comments on the changes if you want to continue with this.
Of course I need to ask for the motivation of this. Besides it clearly being a nice experiment on current capabilities.
E.g. I notice that it is already quite sluggish in the main menu. So why not using the regular builds given that we have performance issues for large maps with that already?
And I need good advice: how do you (settlers freaks) see implementation of local connection without sockets?
I think this would be an incredible effort possibly making the (regular) code much less maintainable. For now we can consider all games non-local/multiplayer for the purpose of the core logic.
Decoupling this means we would have 2 almost completely separate control flows that would need testing and being unable to detect and debug multiplayer issues using the regular flow.
I'd rather not do that.
Is there really no other way?
CMakeLists.txt
Outdated
| set(rttrContribBoostDir ${CMAKE_CURRENT_SOURCE_DIR}/contrib/boost) | ||
| if(EXISTS ${rttrContribBoostDir} AND IS_DIRECTORY ${rttrContribBoostDir}) | ||
| set(BOOST_ROOT ${rttrContribBoostDir} CACHE PATH "Path to find boost at") | ||
| if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Common pitfall: This could get double-extended. Just omit the dollar sign
| if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") | |
| if (CMAKE_SYSTEM_NAME MATCHES "Emscripten") |
| if(EXISTS ${rttrContribBoostDir} AND IS_DIRECTORY ${rttrContribBoostDir}) | ||
| set(BOOST_ROOT ${rttrContribBoostDir} CACHE PATH "Path to find boost at") | ||
| if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") | ||
| set(Boost_INCLUDE_DIR "${rttrContribBoostDir}/include" CACHE PATH "Path to find boost at") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not required: BOOST_ROOT is enough. Possibly should be replaced by Boost_ROOT though.
| option(RTTR_USE_SYSTEM_BOOST_NOWIDE "Use system installed Boost.Nowide. Fails if not found!" "${RTTR_USE_SYSTEM_LIBS}") | ||
|
|
||
| if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") | ||
| set(RTTR_USE_SYSTEM_BOOST_NOWIDE ON) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify indents
| set(RTTR_USE_SYSTEM_BOOST_NOWIDE ON) | |
| set(RTTR_USE_SYSTEM_BOOST_NOWIDE ON) |
external/CMakeLists.txt
Outdated
| if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") | ||
| else () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid those here and down, also correct indent for next line
| if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten") | |
| else () | |
| if (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten") |
| } | ||
|
|
||
| const char* GetDriverName() | ||
| const char* GetAudioDriverName() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks the interface, see
| auto GetDriverName = dll.get<GetDriverName_t>("GetDriverName"); |
| return Initialize(); | ||
| } | ||
|
|
||
| bool VideoDriverWrapper::LoadDriver() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See suggestion for Audiodriver
| renderer_ = std::make_unique<DummyRenderer>(); | ||
| if(!renderer_->initOpenGL(videodriver->GetLoaderFunction())) | ||
| return false; | ||
| #ifndef __EMSCRIPTEN__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For if-else rather use the non-inverted, i.e. start with #ifdef __EMSCRIPTEN__
libs/s25main/ogl/OpenGLRenderer.cpp
Outdated
| bool OpenGLRenderer::initOpenGL(OpenGL_Loader_Proc loader) | ||
| { | ||
| #if RTTR_OGL_ES | ||
| #if defined(RTTR_OGL_ES) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be set, shouldn't it? Using #if instead of #ifdef allows us to detect misspellings so we should keep it
libs/s25main/ogl/OpenGLRenderer.cpp
Outdated
| #if RTTR_OGL_ES | ||
| #if defined(RTTR_OGL_ES) | ||
| return gladLoadGLES2Loader(loader) != 0; | ||
| #elif __EMSCRIPTEN__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be ifdef similar to other places? If so I guess it is easier to put at the top as ifdef and elif RTTR_OGL_ES here
| #include "SDL/SDL.h" | ||
| #include "SDL/SDL_opengl.h" | ||
| #else | ||
| #include <glad/glad.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't glad work with emscripten? This is repeated so often even though there is no other change so I'd think it should work using the same method as glad can handle e.g. OpenGL ES already
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can work, at least latest versions claims support of emscripten. I just didn't find good reason for myself to keep glad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well I'm asking because of maintainability: We are using glad to support all platforms without additional ifdefs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand, will check glad for option to migrate back to glad, right now there is problem runtime gl function resolution.
BTW about networking I see the first packet from server to client is ping 0x01, 0x02, 0x01, 0, 0, 0, right? How does handshake routine look like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Handshake is quite simple:
- Server sends player id to client on connect: GameMessage_Player_Id https://github.com/Return-To-The-Roots/s25client/blob/master/libs/s25main/network/GameServer.cpp#L848
- Client answers with own version: https://github.com/Return-To-The-Roots/s25client/blob/master/libs/s25main/network/GameClient.cpp#L417
- Server confirms the version: https://github.com/Return-To-The-Roots/s25client/blob/master/libs/s25main/network/GameServer.cpp#L929
|
@Flamefire well, motivation is to get original fill and look. First I ported widelands, playable version is here https://turch.in/widelands/ . But it's a bit different game... Besides, it's cool to have version of the game which doesn't require download and install, it just works right now and here in browser. |
|
Well, experiment with hacking of libsock.js was successful + I had to slightly modify game network code: recv messages in complete chunks, without waiting for any additional blocks. |
|
It seems like you have the same problem with the graphics/shadows as my Android port of rttr. Do you know why this happens? Would help me a lot :D |
TBH: The core code isn't exactly optimized for performance. There are a lot of indirections and virtual calls leading to pointer-chasing and poor branch prediction. I don't think there is an easy way to fix this without rewriting a lot of the code which then might reintroduce especially all the async bugs we fixed over the years. Although there are some targeted optimization opportunities like one recently fixed in the pathfinding code: #1734 |
There must something related to textures format/internal format. I'm looking at TerrainRenderer and something tells me that 99% of its code can be done by shaders. |
I started with introducing shaders a long time ago but never got around finishing it. My first point was about handling player textures by the shader instead of the bitmap class. I.e. have the shader combine the main texture and color the masked overlay. And yes the TerrainRenderer is mighty tricky. There are a few tricks already used to make rendering as efficient as possible grouping stuff. A lot might also be gained by caching part of the scenes. E.g. the terrain is static as long as the view isn't scrolled or height levels are adjusted. I introduced listeners to make this feasible but again didn't get around using them for this especially due to the existing code complexity |
|
Well, if I had planning a renderer for terrain there must be one big texture atlas with terrain sprites, generated texture with tiles indexes from atlas + gl program which accepts both 2d samples and by pixel copies atlas sprites to it's onscreen positions using mapping from indexes texture. Then in same way apply gl program to render dither between terrain connections water/land greenland/winter etc. Then gl program to render roads, then objects. |
We have the texture atlas already. The textures are given as positions/triangles inside that. That's actually inherited from S2. If you get some minimal program working just for the terrain textures and transitions that includes the animation I'd be very curious to see that :-) |
I've fixed the shading issues. I don't know if this works for your web port but here are the changes(just the few in the terrain renderer) https://github.com/Farmer-Markus/s25rttr-android/blob/main/patch%2Fs25client.patch#L2207 |
|
@Farmer-Markus awesome, I will try. Currently my only progress is ruining of terrain rendering completely and attempt to show at least something. |
|
What I'm working on: precision highp float;
attribute vec2 aVertexPosition;
uniform mat3 projectionMatrix;
varying vec2 vTextureCoord;
uniform vec4 inputSize;
uniform vec4 outputFrame;
vec4 filterVertexPosition (void) {
vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;
return vec4((projectionMatrix * vec3(position, 1.)).xy, 0., 1.);
}
vec2 filterTextureCoord (void) {
return aVertexPosition * (outputFrame.zw * inputSize.zw);
}
void main (void) {
gl_Position = filterVertexPosition();
vTextureCoord = filterTextureCoord(); // get texture coord
}and main magic happens in fragment shader precision highp float;
varying vec2 vTextureCoord;
uniform vec4 outputFrame;
uniform vec4 inputSize;
uniform sampler2D uAtlas; // really big texture with all loaded sprites
uniform vec2 uAtlasSize; // it's size
uniform vec2 uTileSize; // for now supports only fixed tile size
uniform vec2 uOutSize;// "real" output size
uniform sampler2D uMapping; // texture where we store in "zw" vec2 coords of sprite in atlas
uniform vec2 uMappingSize; // mapping texture size
uniform vec2 uBackgroundSize; // background size in map points
uniform vec2 uBackgroundOffset; // background offset in pixels
void main () {
// copy current pixel color from atlas using mapping texture
vec2 uv = vTextureCoord/outputFrame.zw*inputSize.xy;//remap to [0..1] as output depends on "inner" tileset
vec2 tileAtlas = uTileSize/uAtlasSize;
vec2 screenTile = (uv*uOutSize+uBackgroundOffset)/uTileSize;
vec2 tileCoord = floor(screenTile); // index in mapping sample
vec2 tileInnerCoord = fract(screenTile)*tileAtlas; // relative coords
float mappingIndex = tileCoord.y*uBackgroundSize.x+tileCoord.x;
float mappingY = floor(mappingIndex/uMappingSize.x);
float mappingX = mappingIndex-mappingY*uMappingSize.x;
vec2 shapeCoord = vec2(mappingX, mappingY)/uMappingSize;
// here we finally found x,y of sprite in atlas texture
vec2 tileAtlasCoord = 255.0*texture2D(uMapping, shapeCoord).xy*tileAtlas;
// finally output pixel color using
gl_FragColor = texture2D(uAtlas, tileAtlasCoord+tileInnerCoord);
}nothing really to show yet, but I fill that it might and will work. |
Great work! I also assume it will improve the native app. Did you get the animations for water and lava working with the shader? But as mentioned: I'd really love to have a shader based renderer in any case |
I'd love shader based renderer, maybe that'd help improving the lightning and would be closer to the original :) |
…Arrays with glBegin/glEnd
|
ok, maybe my update will be a bit disappointing, but I overestimated my dev capabilities to impose such significant refactoring like rendering via gl program. Regarding emscripten, there are a lot of bits to do. It requires lwebsocket.js library patching + you need standalone html/js project to run the build. In other words far from PR candidate. |
Sounds great! What exactly was that bottleneck? As for that replacement: I'm actually surprised by that. The immediate mode ( Especially as we have users using OpenGL ES we can't switch back to immediate mode. However having this identified as a bottleneck is still very useful as it means we can improve our usage in this area, e.g. by not transmitting the data on every call but storing it server-side/on GPU. |
|
Well in my case I got performance improvement 5-6 fps (with empty map) -> 45-50fps (very tight road graph) after migration to immediate mode. |
|
I guess we need to check if this is the same for the native builds or if the difference isn't there or opposite of the emscripten build. Maybe there is some emulation of the OpenGL 3 APIs in the library onto the immediate mode pipeline? |
|
Definitely there is emulation https://github.com/ptitSeb/gl4es/blob/a744af14d4afbda77bf472bc53f43b9ceba39cc0/src/gl/gl4es.c#L246 |
…how loading/please wait popup while this operation is performed


I'm working on wasm port.

Demo is available here https://turch.in/rttr/index.html
Right now it's not playable. As emscripten do not support socket listen in web environment.
And I need good advice: how do you (settlers freaks) see implementation of local connection without sockets?