Tangram to jedna z lepszych alternatyw google/bing maps, do tego otwarta i działająca nie tylko w przeglądarce (JS + WebGL), ale tekże w wersji pod OpenGL-ES dla Linuksa, OSX, Androida i iOS. Dodatkowo projekt jest otwarty, więc można go rozszerzać pod własne potrzeby. Niestety, wersji na Windows brak. Jako, że używałem tych map w aplikacji androidowej i sprawdzała się tam nieźle, stwierdziłem, że w imię jednolitości opracuję target dla Windows. Żeby cały wysiłek miał jakiś walor dydaktyczny, pisałem ten artykuł równolegle z kolejno wykonywanymi rzeczami.
Rozeznanie
Pierwsza rzecz, to pobieżne przepatrzenie kodu i opracowanie strategii dalszego działania. Na daną chwilę repozytorium posiada katalog platforms, w którym to są przykładowe aplikacje dla danych systemów. Widać też, że aplikacja linuksowa korzysta z GLFW, który również dostępny jest dla Windows. No, może nie będzie źle.
Druga rzecz rzucająca się w oczy to sposób budowania. W instrukcjach dla platformy linuksowej używa się instrukcji make linux . Drążąc temat dalej widać, że makefile generowany jest przy użyciu CMake. Trzeba będzie go trochę zmodyfikować, by przyjmował target windowsowy.
Przygotowanie platformy windows
Skopiowałem sobie platformę linux i nazwałem windows, zmieniłem nazwy linuxPlatform na windowsPlatform. I tu pojawił się pierwszy commit, by móc śledzić diffa.
Wywaliłem odniesienia do sys/resource.h i sys/syscall.h. Ifdefy linux/raspi też nie są potrzebne, z tym, że zostawiłem kod zawarty w ifdefie linuksowym w initGLExtensions – Windows raczej obsłuży to samo. Dodatkowo pozbyłem się funkcji setCurrentThreadPriority(). Raz, że nie była używana, dwa że jest mocno związana z linuksem, trzy że raczej na początek nie będzie potrzebna.
Pozbyłem się też obsługi sygnału CTRL+C z pliku głównego. Być może później będę musiał to dodać ponownie, zatem tę pojedynczą zmianę zrobiłem jako oddzielny commit. Będzie to wyraźniej widać w historii.
Poprawki CMake
Jeśli chodzi o platformę, główny plik CMake ogranicza się do sprawdzenia aktualnej platformy i upewnienia się, że jest ona na liście obsługiwanych platform. Dodając windows do listy platform spowodowałem próbę dołączenia pliku toolchains/windows.cmake , który oczywiście nie istnieje. Stworzyłem go kopiując linux.cmake i robiąc znajdź-zamień na wyrazie „linux”. Moją uwagę jednak skupiła linia add_definitions(-DTANGRAM_LINUX) . Szukając TANGRAM_LINUX natknąłem się na wystąpienia tylko w platforms/linux (więc niegroźnie) i platform_gl.h w platforms/common. Brzmi jak obietnica zabawy. Tak czy inaczej, zmieniłem TANGRAM_LINUX na TANGRAM_WINDOWS . Zabawa z CMake prawodpodobnie tu się kończy.
Poprawki platform_gl
definicja TANGRAM_LINUX używana jest tylko w jednym miejscu:
#ifdef TANGRAM_LINUX #define GL_GLEXT_PROTOTYPES #include <GLFW/glfw3.h> #endif // TANGRAM_LINUX
Jako, że target windowsowy również ma korzystać z GLFW, zamieniłem to na coś takiego:
#if defined(TANGRAM_LINUX) || defined(TANGRAM_WINDOWS) #define GL_GLEXT_PROTOTYPES #include <GLFW/glfw3.h> #endif // defined(TANGRAM_LINUX) || defined(TANGRAM_WINDOWS)
Odpalając CMake . -G „MinGW Makefiles” któryś z kolejnych cmake’ów podprojektów nakrzyczał na mnie, że nie wolno budować w katalogu głównym. Założyłem zatem katalog build i z jego poziomu zrobiłem CMake .. -G „MinGW Makefiles” . Ruszyło, zaczęło budować…
Błędy przy kompilacji
… I wysypało się w połowie drogi.
D:\prg\_git\tangram-es\core\deps\harfbuzz-icu-freetype\harfbuzz\src\hb-common.cc:224:37: error: 'strdup' was not declared in this scope lang = (hb_language_t) strdup (s); ^
Chwila guglania i rzeczywiście – dany plik nie inkluduje string.h ani cstring, w którym leży funkcja strdup. To jednak nie rozwiązało problemu, zatem zacząłem szukać głębiej i to nie inklud był przyczyną – kompilator uruchomiony był z flagą -std=c++1y i o dziwo w takiej konfiguracji nie ma funkcji strdup – zarówno w formie zwykłej jak i z prefiksem std:: . Zmiana flagi kompilacji na -std=gnu++1y w windows.cmake zrobiła swoje. Kolejny przystanek w 83%:
D:\prg\_git\tangram-es\core\src\gl\primitives.cpp:12:31: fatal error: debugPrimitive_vs.h: No such file or directory
No i faktycznie, pliku nie ma. Patrzę w log kompilacji a tam coś takiego:
Scanning dependencies of target debugPrimitive_vs [ 8%] Generating ../../core/generated/debugPrimitive_vs.h [ 8%] Built target debugPrimitive_vs
No i widać, że ścieżka się nie zgadza. Zapuściłem make’a tym razem z katalogu build/windows, tak by dwa piętra wyżej leżał katalog core. Druga sprawa – podczas kompilacji uruchamiany był program incbin.sh, który nie działał, gdyż nie mam interpretera .sh. Okazuje się, że incbin to wspaniały kawałek kodu, który jest jednocześnie poprawnym skryptem bat i sh domyślnie występujący z rozszerzeniem .bat i nie wiedzieć czemu ekipa od tangram-es zmieniła mu rozszerzenie na .sh łamiąc kompatybilność z Windowsem. Pierwsze co, to przywróciłem rozszerzenie .bat i oryginalną treść. Druga rzecz, to drobna modyfikacja CMakeLists.txt tangram-es/core by zamiast polecenia incbin używał ${INCBIN} zdefiniowanego jako:
if(WIN32) set(INCBIN "${PROJECT_SOURCE_DIR}/../scripts/incbin.bat") else() set(INCBIN "sh ${PROJECT_SOURCE_DIR}/../scripts/incbin.bat") endif()
No i ruszyło i zatrzymało się na 98%:
D:\prg\_git\tangram-es\core\src\util\zlibHelper.cpp:3:18: fatal error: zlib.h: No such file or directory
Naprawiłem to doinstalowując port windowsowy zlib i dodając do CMake’a szukanie i dołączenie jego plików. Poszło dalej, aż nie zaczął się wywalać platforms/common/platform_gl.cpp krzycząc różne dziwne rzeczy o redefinicjach symboli OpenGLa i braku niektórych funkcji. Zwłaszcza ten drugi problem to gigantyczna słabość Windowsa – w sposób podstawowy nagłówki OpenGLa zapewniają wyłącznie stare API do wersji 1.4, jeszcze sprzed rewolucji shaderowej, gdzie dzisiaj już jest wersja 4.5. Pozostałe funkcje trzeba sobie załadować we własnym zakresie.
Przypomniało mi się, że GLFW na swojej stronie zalecał jakiegoś loadera dodatkowych funkcji OGL. Chwila sprawdzania i wyszło na to, że był to GLAD – wraz z praktycznym generatorem plików do dołączenia do projektu. Wygenerowałem sobie goły GLES2.0 z loaderem. Przy kompilacji zaczął krzyczeć z powodu redeklaracji, bo GLFW jakimś cudem dalej ładował nagłówki systemowe. Zgodnie z informacjami w FAQ projektu, GLFW powinien sam wykryć loadera, lecz jeśli tego nie zrobi, należy zdefiniować GLFW_INCLUDE_NONE . Tak też zrobiłem i błędów ubyło, lecz jeszcze nie wszystkie – brakuje m.in. funkcji glMapBuffer, która nie jest częścią standardu. Zaznaczam wszystkie możliwe rozszerzenia i dalej nic – coś jest nie tak.
Okazuje się, że dla innych targetów niektóre niestandardowe rozszerzenia miały zdefiniowane aliasy. Przykładowo dla OSX:
#define glDeleteVertexArrays glDeleteVertexArraysAPPLE #define glGenVertexArrays glGenVertexArraysAPPLE #define glBindVertexArray glBindVertexArrayAPPLE
Wykonałem podobne definicje dla brakujących funkcji, gdyż GLAD deklarował je z końcówką OES. Pomogło. Następny przystanek – cURL. Doinstalowanie pakietu poszło łatwo, ciężej żeby zmusić CMake’a do jego znalezienia, gdyż skrypt szukania cURLa nie wykorzystuje ścieżki instalacji. Z pomocą przychodzi zmienna środowiskowa CMAKE_PREFIX_PATH, która mówi CMake’owi gdzie ma szukać inkludów i libek. I to w sumie na tyle błędów kompilacji. Pozostało coś gorszego…
Błędy linkowania
CMakeFiles\tangram.dir/objects.a(urlClient.cpp.obj):urlClient.cpp:(.text+0x69): undefined reference to `_imp__curl_global_cleanup' CMakeFiles\tangram.dir/objects.a(urlClient.cpp.obj):urlClient.cpp:(.text+0xb72): undefined reference to `_imp__curl_easy_init' CMakeFiles\tangram.dir/objects.a(urlClient.cpp.obj):urlClient.cpp:(.text+0xb78): undefined reference to `_imp__curl_easy_setopt' CMakeFiles\tangram.dir/objects.a(urlClient.cpp.obj):urlClient.cpp:(.text+0xf05): undefined reference to `_imp__curl_easy_perform' CMakeFiles\tangram.dir/objects.a(urlClient.cpp.obj):urlClient.cpp:(.text+0xf36): undefined reference to `_imp__curl_easy_getinfo' CMakeFiles\tangram.dir/objects.a(urlClient.cpp.obj):urlClient.cpp:(.text+0x1095): undefined reference to `_imp__curl_easy_cleanup'
Chwila guglania i okazuje się, że funkcje poprzedzone _imp_ są odwołaniami do funkcji w DLL. Poszukałem jeszcze trochę i okazuje się, że może być to spowodowane korzystaniem przez MinGW z bibliotek budowanych w Visual C++ . Ściągnąłem źródła cURLa, skompilowałem komendą make mingw32 i ruszyło.
lib/libtangramcore.a(tileWorker.cpp.obj):tileWorker.cpp:(.text+0x487): undefined reference to `Tangram::setCurrentThreadPriority(int)' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0xcbd): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0xcdd): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0xd93): undefined reference to `__stack_chk_fail' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0xe03): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0xf47): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x11fd): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x1384): undefined reference to `__stack_chk_fail' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x15f5): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x166d): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x167e): undefined reference to `__stack_chk_fail' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x1709): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x17be): undefined reference to `__stack_chk_guard' lib/libSQLiteCpp.a(Database.cpp.obj):Database.cpp:(.text+0x17d0): undefined reference to `__stack_chk_fail'
Odezwała się funkcja setCurrentThreadPriority, którą wyrzuciłem, więc ponownie ją dodałem z pustym ciałem funkcji. Co do reszty: sqlite było kompilowane z flaga -fstack-protector zaś reszta nie. Dodanie tej flagi do windowsowego cmake’a załatwiło sprawę. Exe gotowe, ale to nie koniec.
Problemy po uruchomieniu
Aplikacja się nie chciała uruchomić z braku paru .dll od zliba i cURLa. Po zaciągnięciu ich aplikacja ruszyła, jednak w oknie konsoli przywitała mnie salwa errorów typu:
ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 2 failed: 'SSL certificate problem: unable to get local issuer certificate' with http status: 0 for url: https://fonts.gstatic.com/s/opensans/v13/wMws1cEtxWZc6AZZIpiqWALUuEpTyoUstqEm5AMlJo4.woff ERROR D:\prg\_git\tangram-es\core\src\scene\sceneLoader.cpp:774: Bad URL request for font open sans_400_normal at URL https://fonts.gstatic.com/s/opensans/v13/wMws1cEtxWZc6AZZIpiqWALUuEpTyoUstqEm5AMlJo4.woff ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 3 failed: 'SSL certificate problem: unable to get local issuer certificate' with http status: 0 for url: https://tile.mapzen.com/mapzen/vector/v1/all/16/19294/24642.mvt?
O ile te drugie związane mogą być z tym, że same w sobie linki nie działają, gdyż nie kompilowałem z podaniem klucza API, o tyle te od gstatic pokazują, że cURL ma sam w sobie problemy z HTTPSem. Stwierdziłem, że to zapewne dlatego, że korzystam z gotowca curla. Skompilowałem go sobie ręcznie i otrzymałem tego typu errory:
ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 1 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://fonts.gstatic.com/s/montserrat/v7/zhcz-_WihjSQC0oHJ9TCYL3hpw3pgy2gAi-Ip7WPMi0.woff ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 2 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://fonts.gstatic.com/s/opensans/v13/wMws1cEtxWZc6AZZIpiqWALUuEpTyoUstqEm5AMlJo4.woff ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 2 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://tile.mapzen.com/mapzen/vector/v1/all/16/19295/24642.mvt? ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 0 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://tile.mapzen.com/mapzen/vector/v1/all/16/19294/24642.mvt? ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 3 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://tile.mapzen.com/mapzen/vector/v1/all/16/19294/24641.mvt? ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 1 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://tile.mapzen.com/mapzen/vector/v1/all/16/19295/24641.mvt? ERROR D:\prg\_git\tangram-es\platforms\common\urlClient.cpp:172: curlLoop 2 failed: 'Protocol "https" not supported or disabled in libcurl' with http status: 0 for url: https://tile.mapzen.com/mapzen/vector/v1/all/16/19294/24643.mvt?
Okazało się, że makefile pod mingw w ogóle nie ma opcji ustawienia ssl. Po dohackowaniu sobie obsługi ssl i zlib wróciłem do tych samych problemów z certyfikatami SSL, zatem to nie było to. Wygląda na to, że nie ma wkompilowanego certyfikatu, więc żeby się z tym w tej chwili dłużej nie bawić dodałem omijanie sprawdzania certyfikatu po stronie tangrama:
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, FALSE);
No i ruszyło, ale to nie koniec. Pojawiły się crashe. Po skompilowaniu tangram-es w trybie debug, GDB mi podpowiedział co nieco:
D:\prg\_git\tangram-es\build\windows\bin>gdb tangram.exe GNU gdb (GDB) 7.2 Copyright (C) 2010 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "mingw32". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>... Reading symbols from D:\prg\_git\tangram-es\build\windows\bin/tangram.exe...done. (gdb) run Starting program: D:\prg\_git\tangram-es\build\windows\bin/tangram.exe [New Thread 18296.0x4940] [New Thread 18296.0x3fb8] [New Thread 18296.0x3388] [New Thread 18296.0xe5c] ... Program received signal SIGSEGV, Segmentation fault. warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) ?? (warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) ) at D:/prg/kompilatory/TDM-GCC/lib/gcc/mingw32/5.1.0/include/c++/bits/move.h:76 76 forward(typename std::remove_reference<_Tp>::type& __t) noexcept warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) (gdb) bt warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) #0 ?? (warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) ) at D:/prg/kompilatory/TDM-GCC/lib/gcc/mingw32/5.1.0/include/c++/bits/move.h:76 warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.) #1 0x00403033 in Tangram::GL::mapBuffer(unsigned int, unsigned int) () #2 0x004a8ad5 in Tangram::MeshBase::subDataUpload(Tangram::RenderState&, signed char*) () at D:\prg\_git\tangram-es\core\src\gl\mesh.cpp:107 #3 0x007d984c in Tangram::DynamicQuadMesh<Tangram::SpriteVertex>::upload(Tangram::RenderState&) () at D:/prg/_git/tangram-es/core/src/gl/dynamicQuadMesh.h:92 #4 0x004817a0 in Tangram::PointStyle::onBeginFrame(Tangram::RenderState&) () at D:\prg\_git\tangram-es\core\src\style\pointStyle.cpp:58 #5 0x00411aab in Tangram::Map::render() () at D:\prg\_git\tangram-es\core\src\map.cpp:498 #6 0x00404d70 in Tangram::GlfwApp::run() () at D:\prg\_git\tangram-es\platforms\common\glfwApp.cpp:129 #7 0x00401b0a in main () at D:\prg\_git\tangram-es\platforms\windows\src\main.cpp:30 warning: (Internal error: pc 0x0 in read in psymtab, but not in symtab.)
Jak widać kłopoty sprawia funkcja glMapBuffer – jedna z tych, które musiałem ręcznie dodefiniować. Chwila guglania i ktoś na stacku wypowiada się, że GLES2.0 jest w sumie kompatybilny z GL3.4. Wygenerowałem GLADa dla tej wersji i zrekompilowałem. Teraz tangram tylko krzyczy, że nie zna dwóch funkcji będących w GLES2 a dodanych dopiero w GL4.0. Jako, że funkcje te po prostu są kopiami innych funkcji przyjmującymi argumenty float zamiast double, zdefiniowałem je makrami aby nie zwiększać zbytnio wymagań aplikacji:
#define glDepthRangef(a, b) glDepthRange((double)(a), (double)(b)) #define glClearDepthf(a) glClearDepth((double)(a))
I na tym historia się kończy.
Epilog
Z tego, co naczytałem się issues projektu, target windowsowy był dość długo oczekiwaną funkcjonalnością. Dziwi mnie, że nikt tego wcześniej nie zrobił. Ba, dziwi mnie, że projekt od samego początku nie supportuje windowsa, zwłaszcza że korzysta z GLFW, które z automatu załatwia wszelką kompatybilność. W międzyczasie wyszła kolejna wersja tangrama, więc będę musiał dosynchronizować się przed pull requestem. No i skompilować OpenSSL z certyfikatem, by móc sprawdzać wiarygodność źródeł.
Następny przystanek: kontrolka tangram-es dla wxWidgets.