diff --git a/.gitignore b/.gitignore index ce57cff..2adff1b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build/ build-ninja/ .cache +cache.sqlite-journal CMakeCache.txt CMakeFiles/ CMakeScripts/ @@ -31,3 +32,10 @@ vendor/ *.log cache.sqlite +cache.sqlite-journal + +# Additional ignores +build-*/ +.DS_Store + +dumps/* diff --git a/.vscode/settings.json b/.vscode/settings.json index a1e5ed3..6b18675 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,5 +57,9 @@ "unittests", "unpremult", "vcpkg" - ] + ], + "files.associations": { + "__tree": "cpp", + "map": "cpp" + } } diff --git a/AGENTS.md b/AGENTS.md index aebdce4..e711c0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,12 +14,20 @@ Under no circumstances is the agent permitted to execute commands using `sudo`. When making changes to C++ files (`.cpp`, `.hpp`), it is mandatory to run `clang-format` to ensure code style consistency. -**Important:** The `vendor`, `build`, and `.git` directories must be excluded from formatting. +**Important:** Exclude the following directories from formatting: `vendor/`, `build/`, `build-ninja/`, and `.git/`. You can format all relevant files using the following command: ```bash -find . -name "*.cpp" -o -name "*.hpp" | grep -v "^./vendor/" | grep -v "^./build/" | grep -v "^./.git/" | xargs clang-format -i +find . \( -name "*.cpp" -o -name "*.hpp" \) \ + | grep -v -E "^\./(vendor|build|build-ninja|\.git)/" \ + | xargs clang-format -i +``` + +Alternatively, run the helper script: + +```bash +./scripts/format-cpp.sh ``` ## Build and Run (macOS) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7da591b..d4f9409 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.21) -if(APPLE) + +# Check if the generator supports Swift on macOS +if(CMAKE_GENERATOR MATCHES "Unix Makefiles|Ninja" AND APPLE) + project(maplibre-native-slint LANGUAGES C CXX OBJC OBJCXX) + message(STATUS "Using ${CMAKE_GENERATOR} generator - Swift language disabled") +elseif(APPLE) project(maplibre-native-slint LANGUAGES C CXX OBJC OBJCXX Swift) else() project(maplibre-native-slint LANGUAGES C CXX) @@ -8,8 +13,16 @@ endif() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Prefer static libraries to simplify runtime distribution +set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE) + # Add testing support (default OFF) option(BUILD_TESTS "Build tests" OFF) +option(SLINT_MAPLIBRE_USE_METAL "Enable Metal zero-copy backend on Apple" OFF) +option(BUILD_METAL_ONLY "On Apple, skip GLES lookups and focus on Metal" ON) +option(MLNS_WITH_SLINT_GL "Enable Slint OpenGL zero-copy path" OFF) +# Force-fetch Slint from source to control enabled renderers (avoids picking an installed Slint without Skia) +option(MLNS_FORCE_FETCH_SLINT "Fetch/build Slint from source instead of find_package" ON) # Make FetchContent available early (keep for other fetches if needed) include(FetchContent) @@ -53,6 +66,8 @@ if(APPLE) set(CMAKE_XCODE_ATTRIBUTE_EXCLUDED_ARCHS[sdk=macosx*] "x86_64") endif() +# MapLibre options +set(MLN_WITH_RTTI ON CACHE BOOL "Enable RTTI for MapLibre" FORCE) # Disable MapLibre Native tests/benches/render-tests in the vendored tree set(MLN_WITH_TESTS OFF CACHE BOOL "Disable MapLibre tests" FORCE) set(MLN_WITH_RENDER_TESTS OFF CACHE BOOL "Disable MapLibre render tests" FORCE) @@ -78,23 +93,41 @@ endif() # -# Slint: try to find it, otherwise fetch & build it via FetchContent -# -find_package(Slint QUIET) +# Slint: configure features (enable Skia/OpenGL on Windows for robust GPU) +# These must be set before FetchContent_MakeAvailable(Slint) +if (WIN32) + set(SLINT_FEATURE_BACKEND_WINIT ON CACHE BOOL "" FORCE) + # Prefer FemtoVG OpenGL on Windows to avoid D3D symbol requirements from skia-safe + if (MLNS_WITH_SLINT_GL) + # Enable FemtoVG OpenGL renderer for zero-copy GL path on Windows + set(SLINT_FEATURE_RENDERER_FEMTOVG ON CACHE BOOL "" FORCE) + set(SLINT_FEATURE_RENDERER_FEMTOVG_WGPU OFF CACHE BOOL "" FORCE) + # Keep software renderer available as fallback but prefer GL at runtime + set(SLINT_FEATURE_RENDERER_SOFTWARE ON CACHE BOOL "" FORCE) + set(SLINT_FEATURE_RENDERER_SKIA OFF CACHE BOOL "" FORCE) + set(SLINT_FEATURE_RENDERER_SKIA_OPENGL OFF CACHE BOOL "" FORCE) + else() + set(SLINT_FEATURE_RENDERER_FEMTOVG ON CACHE BOOL "" FORCE) + set(SLINT_FEATURE_RENDERER_SOFTWARE ON CACHE BOOL "" FORCE) + set(SLINT_FEATURE_RENDERER_FEMTOVG_WGPU OFF CACHE BOOL "" FORCE) + endif() +endif() -if (NOT TARGET Slint::Slint) - message(STATUS "Slint CMake target not found. Downloading Slint (api/cpp) via FetchContent and building locally.") - # show progress for debugging if needed +# +# Slint: prefer FetchContent so we can enable Skia/OpenGL features on Windows +# +if (MLNS_FORCE_FETCH_SLINT) + message(STATUS "Using FetchContent for Slint (MLNS_FORCE_FETCH_SLINT=ON)") set(FETCHCONTENT_QUIET FALSE) - FetchContent_Declare( Slint GIT_REPOSITORY https://github.com/slint-ui/slint.git GIT_TAG release/1 SOURCE_SUBDIR api/cpp ) - FetchContent_MakeAvailable(Slint) +else() + find_package(Slint REQUIRED) endif() # Ensure FetchContent actually populated the Slint content if we expected it @@ -119,11 +152,149 @@ if (NOT TARGET cpr::cpr) endif() # --- executable --- -add_executable(maplibre-slint-example - examples/main.cpp +# Choose entry based on Metal usage on Apple +set(MAPLIBRE_SLINT_EXE_SOURCES src/slint_maplibre_headless.cpp - platform/custom_file_source.cpp -) + platform/custom_file_source.cpp) + +if(APPLE AND SLINT_MAPLIBRE_USE_METAL) + list(APPEND MAPLIBRE_SLINT_EXE_SOURCES examples/main.mm) +else() + list(APPEND MAPLIBRE_SLINT_EXE_SOURCES examples/main.cpp) +endif() + +add_executable(maplibre-slint-example ${MAPLIBRE_SLINT_EXE_SOURCES}) + +# Optional Slint OpenGL zero-copy library (scaffold). This does not change the +# default rendering path yet; it provides a target we can evolve without +# breaking the current build. The example links it when enabled so symbols are +# available, guarded by compile-time macros. +if(MLNS_WITH_SLINT_GL) + add_library(slint_maplibre_gl STATIC + src/gl/slint_gl_maplibre.cpp + src/gl/slint_gl_renderer_backend.cpp + ) + target_include_directories(slint_maplibre_gl PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_compile_features(slint_maplibre_gl PUBLIC cxx_std_20) + target_link_libraries(slint_maplibre_gl PUBLIC Slint::Slint mbgl-core ${OPENGL_LIBRARIES}) + target_compile_definitions(slint_maplibre_gl PUBLIC MLNS_WITH_SLINT_GL=1) + if (WIN32) + target_compile_definitions(slint_maplibre_gl PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN _USE_MATH_DEFINES) + endif() + + # Link GL path into the example only when requested + target_link_libraries(maplibre-slint-example PRIVATE slint_maplibre_gl) + target_compile_definitions(maplibre-slint-example PRIVATE MLNS_WITH_SLINT_GL=1) + + if (WIN32) + # Pre-check: require MSVC (Visual Studio 2022 toolset) and x64 + if (NOT MSVC) + message(FATAL_ERROR "MLNS_WITH_SLINT_GL=ON requires MSVC on Windows") + endif() + if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message(FATAL_ERROR "MLNS_WITH_SLINT_GL=ON requires 64-bit build on Windows") + endif() + + # Skia/OpenGL renderer (via Slint) and winit on Windows need a few extra SDK libs + find_library(D3DCompiler_LIB d3dcompiler) + find_library(D3D12_LIB d3d12) + find_library(DXGI_LIB dxgi) + find_library(DXGUID_LIB dxguid) + find_library(Dwmapi_LIB dwmapi) + find_library(UxTheme_LIB uxtheme) + find_library(Imm32_LIB imm32) + find_library(Comctl32_LIB comctl32) + find_library(PropSys_LIB propsys) + + set(_win_extra_libs) + foreach(lib IN ITEMS D3DCompiler_LIB D3D12_LIB DXGI_LIB DXGUID_LIB Dwmapi_LIB UxTheme_LIB Imm32_LIB Comctl32_LIB PropSys_LIB) + if (${lib}) + list(APPEND _win_extra_libs ${${lib}}) + else() + message(WARNING "Windows SDK library not found for GL path: ${lib}") + endif() + endforeach() + + target_link_libraries(maplibre-slint-example PRIVATE ${_win_extra_libs}) + + # Only link Skia if the Skia renderer is enabled + if (SLINT_FEATURE_RENDERER_SKIA) + # Try vcpkg's exported Skia targets. Newer ports expose skia::skia via + # skiaConfig.cmake; older ones expose unofficial::skia::skia. Support both. + find_package(skia CONFIG QUIET) + if (TARGET skia::skia) + set(SKIA_TARGET skia::skia) + else() + find_package(unofficial-skia CONFIG REQUIRED) + if (TARGET unofficial::skia::skia) + set(SKIA_TARGET unofficial::skia::skia) + else() + message(FATAL_ERROR "Skia CMake target not found; expected skia::skia or unofficial::skia::skia") + endif() + endif() + + target_link_libraries(maplibre-slint-example PRIVATE ${SKIA_TARGET}) + if (TARGET slint_maplibre_gl) + target_link_libraries(slint_maplibre_gl PRIVATE ${SKIA_TARGET}) + endif() + + # Skia text rendering via Slint typically needs shaper/paragraph/unicode. + # These are split as separate module targets by vcpkg's unofficial-skia port. + find_package(unofficial-skia CONFIG QUIET) + set(_skia_optional_modules + unofficial::skia::modules::skshaper + unofficial::skia::modules::skparagraph + unofficial::skia::modules::skunicode_icu + unofficial::skia::modules::skunicode_core) + foreach(mod ${_skia_optional_modules}) + if (TARGET ${mod}) + target_link_libraries(maplibre-slint-example PRIVATE ${mod}) + if (TARGET slint_maplibre_gl) + target_link_libraries(slint_maplibre_gl PRIVATE ${mod}) + endif() + endif() + endforeach() + + # The Rust crate `skia-safe` exposes a C shim library named `skia-bindings.lib` + # that provides symbols like C_SkCodec_delete, C_ParagraphBuilder_make, ... + # Corrosion places it under the Cargo build tree. Discover and link it. + if (EXISTS "${CMAKE_BINARY_DIR}/cargo") + file(GLOB_RECURSE _skia_bindings_lib + LIST_DIRECTORIES FALSE + "${CMAKE_BINARY_DIR}/cargo/build/*/release/build/*/out/skia/skia-bindings.lib") + list(LENGTH _skia_bindings_lib _skia_bindings_count) + if (_skia_bindings_count GREATER 0) + # If multiple, pick the newest by timestamp + list(SORT _skia_bindings_lib ORDER DESCENDING) + list(GET _skia_bindings_lib 0 _skia_bindings_selected) + message(STATUS "Linking Skia C shim: ${_skia_bindings_selected}") + target_link_libraries(maplibre-slint-example PRIVATE "${_skia_bindings_selected}") + if (TARGET slint_maplibre_gl) + target_link_libraries(slint_maplibre_gl PRIVATE "${_skia_bindings_selected}") + endif() + else() + message(WARNING "skia-bindings.lib not found under cargo build tree; Skia C symbols may be unresolved") + endif() + endif() + endif() + endif() +endif() + +# Metal zero-copy backend sources (Objective-C++) +if(APPLE AND SLINT_MAPLIBRE_USE_METAL) + message(STATUS "Building with Metal zero-copy backend sources") + target_compile_definitions(maplibre-slint-example PRIVATE SLINT_MAPLIBRE_USE_METAL=1) + # Add Metal sources if present + set(METAL_SOURCES + src/metal/slint_metal_maplibre.mm + ) + foreach(src ${METAL_SOURCES}) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${src}") + target_sources(maplibre-slint-example PRIVATE ${src}) + set_source_files_properties(${src} PROPERTIES COMPILE_FLAGS "-fobjc-arc") + endif() + endforeach() +endif() if (WIN32) target_compile_definitions(maplibre-slint-example @@ -154,27 +325,40 @@ else() endif() # Find system deps -find_package(OpenGL REQUIRED) -find_package(PkgConfig REQUIRED) +if(APPLE) + find_package(OpenGL REQUIRED) +elseif(NOT BUILD_METAL_ONLY) + find_package(OpenGL REQUIRED) + find_package(PkgConfig REQUIRED) +else() + find_package(PkgConfig REQUIRED) +endif() -# Check if we're on macOS and handle OpenGL ES differently +# Platform-specific handling for GLES vs OpenGL if(APPLE) # macOS specific settings set(CMAKE_OSX_ARCHITECTURES "arm64") set(CMAKE_OSX_DEPLOYMENT_TARGET "14.3") - # Find Metal framework for MapLibre + # Find Metal-related frameworks find_library(METAL_FRAMEWORK Metal) + find_library(APPKIT_FRAMEWORK AppKit) + find_library(QUARTZCORE_FRAMEWORK QuartzCore) - # On macOS, use OpenGL instead of GLES + # Use desktop OpenGL on macOS; no GLES lookup + set(GLES3_FOUND TRUE) + set(GLES3_INCLUDE_DIRS "") + set(GLES3_LIBRARIES "") +elseif(WIN32) + # On Windows we use desktop OpenGL (opengl32); skip pkg-config glesv2 entirely set(GLES3_FOUND TRUE) set(GLES3_INCLUDE_DIRS "") set(GLES3_LIBRARIES "") else() - # On other platforms, try to find GLES + # Linux and others: try GLES via pkg-config, otherwise fall back to OpenGL pkg_check_modules(GLES3 glesv2) if(NOT GLES3_FOUND) - message(WARNING "GLES2 not found, falling back to OpenGL") + message(STATUS "GLESv2 not found, falling back to desktop OpenGL") set(GLES3_FOUND TRUE) set(GLES3_INCLUDE_DIRS "") set(GLES3_LIBRARIES "") @@ -185,9 +369,12 @@ endif() target_include_directories(maplibre-slint-example PRIVATE src + src/metal platform ${CMAKE_SOURCE_DIR}/vendor/maplibre-native/include ${CMAKE_BINARY_DIR}/vendor/maplibre-native/include + ${CMAKE_SOURCE_DIR}/vendor/maplibre-native/src + ${CMAKE_SOURCE_DIR}/vendor/maplibre-native/vendor/metal-cpp ${CMAKE_CURRENT_BINARY_DIR} ${GLES3_INCLUDE_DIRS} ) @@ -198,16 +385,26 @@ target_link_libraries(maplibre-slint-example Slint::Slint mbgl-core cpr::cpr - ${GLES3_LIBRARIES} - ${OPENGL_LIBRARIES} + $<$:${GLES3_LIBRARIES}> + $<$:${OPENGL_LIBRARIES}> $<$:${METAL_FRAMEWORK}> + $<$:${APPKIT_FRAMEWORK}> + $<$:${QUARTZCORE_FRAMEWORK}> ) +if(APPLE) + # Ensure dependent dylibs are discoverable at runtime from build tree + set_target_properties(maplibre-slint-example PROPERTIES + BUILD_RPATH "@loader_path;@executable_path" + INSTALL_RPATH "@loader_path;@executable_path/../lib") + # Explicit frameworks for keyboard APIs + target_link_libraries(maplibre-slint-example PRIVATE "-framework Carbon" "-framework ApplicationServices") +endif() + # On Windows, copy the Slint DLL next to the application binary so that it's found if (WIN32) - add_custom_command(TARGET maplibre-slint-example POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy $ $ - COMMAND_EXPAND_LISTS) + # Skip copying runtime DLLs; this project prefers static linking via vcpkg. + # When there are no runtime DLLs, the copy command would be empty and fail. endif() # Add testing support @@ -216,3 +413,12 @@ enable_testing() if(BUILD_TESTS) add_subdirectory(tests) endif() + +# Convenience target to run app (macOS) +if(APPLE) + add_custom_target(run-macos-metal + COMMAND "$" + DEPENDS maplibre-slint-example + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running MapLibre + Slint example") +endif() diff --git a/README.md b/README.md index 9f604a7..279b869 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ For detailed build instructions, see the platform-specific guides: - **Linux**: [Ubuntu 24.04 Build Guide](docs/build/Linux_Ubuntu_24.md) - **macOS**: [macOS Build Guide](docs/build/macOS_Apple_Silicon.md) - **Windows**: [Windows Build Guide](docs/build/Windows.md) _(coming soon)_ + - For GL zero-copy (Slint Skia OpenGL) on Windows, see [Windows_GL.md](docs/build/Windows_GL.md) ## Screenshots diff --git a/docs/build_guides/Windows_11.md b/docs/build_guides/Windows_11.md index 331e2ac..9f3e02f 100644 --- a/docs/build_guides/Windows_11.md +++ b/docs/build_guides/Windows_11.md @@ -65,8 +65,13 @@ set VCPKG_OVERLAY_TRIPLETS=%cd%\vendor\maplibre-native\platform\windows\vendor\v > The project uses **vcpkg manifest mode**. You **don’t** need to pass package names to `vcpkg install`; CMake will drive vcpkg using `vcpkg.json`. ```bat +rmdir /s /q build-ninja + cmake -S . -B build-ninja -G "Ninja" ^ -DCMAKE_BUILD_TYPE=Release ^ + -DMLNS_WITH_SLINT_GL=ON ^ + -DMLNS_FORCE_FETCH_SLINT=ON ^ + -DCMAKE_CXX_COMPILER_LAUNCHER=C:\ccache\ccache-4.11.3-windows-x86_64\ccache.exe ^ -DCMAKE_TOOLCHAIN_FILE=C:/src/vcpkg/scripts/buildsystems/vcpkg.cmake ^ -DVCPKG_TARGET_TRIPLET=x64-windows ^ -DVCPKG_HOST_TRIPLET=x64-windows @@ -80,7 +85,7 @@ cmake -S . -B build-ninja -G "Ninja" ^ ## 5) Build ```bat -cmake --build build-ninja +cmake --build build-ninja -j ``` --- @@ -90,8 +95,21 @@ cmake --build build-ninja The example app and DLLs are placed in the build directory: ```bat -cd build-ninja -maplibre-slint-example.exe +set MLNS_DISABLE_GL_ZEROCOPY=1 +set SLINT_RENDERER=software + +build-ninja\maplibre-slint-example.exe +``` + +or enable GL renderer: + +```bat +set MLNS_DISABLE_GL_ZEROCOPY=0 +set SLINT_RENDERER=gl +set MLNS_FORCE_DESKTOP_GL=1 +set MLNS_GL_SAFE_MODE=2 + +build-ninja\maplibre-slint-example.exe ``` --- diff --git a/examples/main.cpp b/examples/main.cpp index 5234118..06d0d0d 100644 --- a/examples/main.cpp +++ b/examples/main.cpp @@ -1,103 +1,309 @@ +#include +#include #include #include +#include #include #include "map_window.h" #include "slint_maplibre_headless.hpp" +#ifdef MLNS_WITH_SLINT_GL +#include "gl/slint_gl_maplibre.hpp" +#endif + +static std::string now_ts() { + using namespace std::chrono; + const auto now = system_clock::now(); + const auto t = system_clock::to_time_t(now); + std::tm lt{}; +#ifdef _WIN32 + localtime_s(<, &t); +#else + localtime_r(&t, <); +#endif + const auto ms = duration_cast(now.time_since_epoch()) % 1000; + std::ostringstream oss; + oss << std::put_time(<, "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) + << std::setfill('0') << ms.count(); + return oss.str(); +} + +#define LOGI(expr) \ + do { \ + std::ostringstream _oss; \ + _oss << expr; \ + std::cout << now_ts() << " " << _oss.str() << std::endl; \ + } while (0) +#define LOGE(expr) \ + do { \ + std::ostringstream _oss; \ + _oss << expr; \ + std::cerr << now_ts() << " " << _oss.str() << std::endl; \ + } while (0) int main(int argc, char** argv) { - std::cout << "[main] Starting application" << std::endl; + LOGI("[main] Starting application"); +#ifdef MLNS_WITH_SLINT_GL + // 選択肢B: SLINT_RENDERER=gl が見えたら無条件で GL 経路(最優先)。 + // 追加: skia-opengl もデスクトップ OpenGL(WGL) の可能性が高いので許可。 + // 回避: MLNS_DISABLE_GL_ZEROCOPY=1 なら GL ゼロコピーを無効化して CPU + // パスにフォールバック。 + bool kUseGL = false; + bool disable_gl_zerocopy = false; + if (const char* dis = std::getenv("MLNS_DISABLE_GL_ZEROCOPY")) { + std::string v(dis); + for (auto& c : v) + c = static_cast(::tolower(static_cast(c))); + if (v == "1" || v == "true" || v == "on" || v == "yes") { + disable_gl_zerocopy = true; + } + } + const char* sr_env = std::getenv("SLINT_RENDERER"); + if (!disable_gl_zerocopy && !kUseGL && sr_env) { + std::string r(sr_env); + for (auto& c : r) + c = static_cast(::tolower(static_cast(c))); + if (r == "gl" || r == "skia-opengl") + kUseGL = true; + } + // 追加の緩和: 明示の MLNS_USE_GL が true 系なら GL 経路に寄せる(従属)。 + if (!disable_gl_zerocopy && !kUseGL) { + if (const char* use_gl = std::getenv("MLNS_USE_GL")) { + std::string val(use_gl); + for (auto& c : val) + c = static_cast(::tolower(static_cast(c))); + if (val == "1" || val == "true" || val == "on" || val == "yes") + kUseGL = true; + } + } + // WindowsでGLパスを使う場合のみ、かつユーザーがSLINT_RENDERERを指定していない場合に限り、GLを明示。 +#ifdef _WIN32 + if (!disable_gl_zerocopy && kUseGL) { + const char* env_renderer = std::getenv("SLINT_RENDERER"); + if (!env_renderer || std::string(env_renderer).empty()) { + _putenv_s("SLINT_BACKEND", ""); + _putenv_s("SLINT_SKIA_BACKEND", ""); + _putenv_s("SLINT_RENDERER", "gl"); + } + } else if (disable_gl_zerocopy) { + // 強制的にソフトウェアレンダラーへ + _putenv_s("SLINT_BACKEND", ""); + _putenv_s("SLINT_SKIA_BACKEND", ""); + _putenv_s("SLINT_RENDERER", "software"); + } +#endif + LOGI("[main] GL decision: SLINT_RENDERER=" + << (sr_env ? sr_env : "") << " MLNS_DISABLE_GL_ZEROCOPY=" + << (std::getenv("MLNS_DISABLE_GL_ZEROCOPY") + ? std::getenv("MLNS_DISABLE_GL_ZEROCOPY") + : "") + << " => GL path " + << ((!disable_gl_zerocopy && kUseGL) ? "ON" : "OFF")); + std::shared_ptr zero_copy_gl; + std::shared_ptr gl_map; +#else + const bool kUseGL = false; + std::cout << "[main] GL feature disabled at build. Using CPU path." + << std::endl; +#endif auto main_window = MapWindow::create(); - auto slint_map_libre = std::make_shared(); +#ifdef MLNS_WITH_SLINT_GL + // Debug: print the renderer env we set + auto env_backend = std::getenv("SLINT_BACKEND"); + auto env_renderer = std::getenv("SLINT_RENDERER"); + auto env_skia = std::getenv("SLINT_SKIA_BACKEND"); + LOGI("[main] SLINT_BACKEND=" + << (env_backend ? env_backend : "") + << " SLINT_RENDERER=" << (env_renderer ? env_renderer : "") + << " SLINT_SKIA_BACKEND=" << (env_skia ? env_skia : "")); +#endif + std::shared_ptr slint_map_libre = + kUseGL ? nullptr : std::make_shared(); // Delay initialization until a non-zero window size is known auto initialized = std::make_shared(false); const auto size = main_window->get_window_size(); - std::cout << "Initial Window Size: " << static_cast(size.width) << "x" - << static_cast(size.height) << std::endl; + LOGI("Initial Window Size: " << static_cast(size.width) << "x" + << static_cast(size.height)); - // This function is ONLY for rendering. It will be called by the observer. + // This function is ONLY for rendering in the CPU path. auto render_function = [=]() { - std::cout << "Rendering map..." << std::endl; + if (kUseGL) + return; // GL path is driven by Slint's render notifier + LOGI("Rendering map..."); auto image = slint_map_libre->render_map(); main_window->global().set_map_texture(image); }; // Pass the render function to SlintMapLibre, which will pass it to the // observer. - slint_map_libre->setRenderCallback(render_function); + if (!kUseGL) { + slint_map_libre->setRenderCallback(render_function); + } // The timer in .slint file will trigger this callback periodically. // This callback drives the MapLibre run loop and, if needed, performs // rendering on the UI thread. - main_window->global().on_tick_map_loop([=]() { - slint_map_libre->run_map_loop(); - if (slint_map_libre->take_repaint_request() || - slint_map_libre->consume_forced_repaint()) { - render_function(); - } - }); + main_window->global().on_tick_map_loop( + [=, &zero_copy_gl, &gl_map]() { + if (kUseGL) { + if (gl_map) + gl_map->run_map_loop(); + } else { + slint_map_libre->run_map_loop(); + if (slint_map_libre->take_repaint_request() || + slint_map_libre->consume_forced_repaint()) { + render_function(); + } + } + }); main_window->global().on_style_changed( [=](const slint::SharedString& url) { - slint_map_libre->setStyleUrl(std::string(url.data(), url.size())); + LOGI("[UI] style_changed url=" << std::string(url.data(), + url.size())); + if (kUseGL) { + if (gl_map) + gl_map->setStyleUrl(std::string(url.data(), url.size())); + } else { + slint_map_libre->setStyleUrl( + std::string(url.data(), url.size())); + } }); // Connect mouse events - main_window->global().on_mouse_press( - [=](float x, float y) { slint_map_libre->handle_mouse_press(x, y); }); + main_window->global().on_mouse_press([=](float x, float y) { + LOGI("[UI] mouse_press x=" << x << " y=" << y); + if (kUseGL) { + if (gl_map) + gl_map->handle_mouse_press(x, y); + } else { + slint_map_libre->handle_mouse_press(x, y); + } + }); - main_window->global().on_mouse_release( - [=](float x, float y) { slint_map_libre->handle_mouse_release(x, y); }); + main_window->global().on_mouse_release([=](float x, float y) { + LOGI("[UI] mouse_release x=" << x << " y=" << y); + if (kUseGL) { + if (gl_map) + gl_map->handle_mouse_release(x, y); + } else { + slint_map_libre->handle_mouse_release(x, y); + } + }); main_window->global().on_mouse_move( [=](float x, float y, bool pressed) { - slint_map_libre->handle_mouse_move(x, y, pressed); + LOGI("[UI] mouse_move x=" << x << " y=" << y << " pressed=" + << (pressed ? "true" : "false")); + if (kUseGL) { + if (gl_map) + gl_map->handle_mouse_move(x, y, pressed); + } else { + slint_map_libre->handle_mouse_move(x, y, pressed); + } }); // Double click zoom with Shift for zoom-out main_window->global().on_double_click_with_shift( [=](float x, float y, bool shift) { - slint_map_libre->handle_double_click(x, y, shift); + LOGI("[UI] double_click x=" << x << " y=" << y << " shift=" + << (shift ? "true" : "false")); + if (kUseGL) { + if (gl_map) + gl_map->handle_double_click(x, y, shift); + } else { + slint_map_libre->handle_double_click(x, y, shift); + } }); // Wheel zoom main_window->global().on_wheel_zoom( [=](float x, float y, float dy) { - slint_map_libre->handle_wheel_zoom(x, y, dy); + LOGI("[UI] wheel x=" << x << " y=" << y << " dy=" << dy); + if (kUseGL) { + if (gl_map) + gl_map->handle_wheel_zoom(x, y, dy); + } else { + slint_map_libre->handle_wheel_zoom(x, y, dy); + } }); main_window->global().on_fly_to( [=](const slint::SharedString& location) { - slint_map_libre->fly_to( - std::string(location.data(), location.size())); + auto loc = std::string(location.data(), location.size()); + LOGI("[UI] fly_to loc=" << loc); + if (kUseGL) { + if (gl_map) + gl_map->fly_to(loc); + } else { + slint_map_libre->fly_to(loc); + } }); // Initialize/resize MapLibre to match the map image area - main_window->on_map_size_changed([=]() { + main_window->on_map_size_changed([=, &zero_copy_gl, &gl_map]() mutable { const auto s = main_window->get_map_size(); const int w = static_cast(s.width); const int h = static_cast(s.height); - std::cout << "Map Area Size Changed: " << w << "x" << h << std::endl; + LOGI("Map Area Size Changed: " << w << "x" << h); if (w > 0 && h > 0) { if (!*initialized) { - slint_map_libre->initialize(w, h); + if (kUseGL) { + gl_map = std::make_shared(); + gl_map->initialize(w, h); + // Create and attach the GL zero-copy path only after we + // have a non-zero window size so Slint's GL renderer can + // initialize. + LOGI("[main] Attaching GL notifier after non-zero size"); + zero_copy_gl = std::make_shared(); + zero_copy_gl->set_on_ready([&](const slint::Image& img) { + std::cout << "[main] on_ready: borrowed image set" + << std::endl; + main_window->global().set_map_texture(img); + }); + auto& win = main_window->window(); + zero_copy_gl->attach( + win, [&](const mlns::GLRenderTarget& rt) { + LOGI("[main] render hook: fbo=" + << rt.fbo << " tex=" << rt.color_tex + << " size=" << rt.width << "x" << rt.height); + if (gl_map) + gl_map->render_to_texture(rt.color_tex, + rt.width, rt.height); + }); + // Use the map image area size as render target + zero_copy_gl->set_surface_size(w, h); + } else { + slint_map_libre->initialize(w, h); + } *initialized = true; } else { - slint_map_libre->resize(w, h); + if (kUseGL) { + if (gl_map) + gl_map->resize(w, h); + if (zero_copy_gl) + zero_copy_gl->set_surface_size(w, h); + } else { + slint_map_libre->resize(w, h); + } } } }); - std::cout << "[main] Entering UI event loop" << std::endl; + // GLパスを使わない場合は、GLノティファイは一切アタッチしない + + // Note: close-request hook can be added here if needed. + + LOGI("[main] Entering UI event loop"); try { main_window->run(); - std::cout << "[main] UI event loop exited normally" << std::endl; + LOGI("[main] UI event loop exited normally"); return 0; } catch (const std::exception& e) { - std::cerr << "[main] Unhandled exception: " << e.what() << std::endl; + LOGE("[main] Unhandled exception: " << e.what()); } catch (...) { - std::cerr << "[main] Unhandled unknown exception" << std::endl; + LOGE("[main] Unhandled unknown exception"); } return 1; } diff --git a/scripts/_build-ninja-win.cmd b/scripts/_build-ninja-win.cmd new file mode 100644 index 0000000..9c1c9f9 --- /dev/null +++ b/scripts/_build-ninja-win.cmd @@ -0,0 +1,73 @@ +@echo off +setlocal enabledelayedexpansion +REM Resolve preferred VS2022 (prefer Community, then Professional, Enterprise, then any) +set "VSROOT=" +for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products Microsoft.VisualStudio.Product.Community -property installationPath`) do set "VSROOT=%%I" +if not defined VSROOT for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products Microsoft.VisualStudio.Product.Professional -property installationPath`) do set "VSROOT=%%I" +if not defined VSROOT for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products Microsoft.VisualStudio.Product.Enterprise -property installationPath`) do set "VSROOT=%%I" +if not defined VSROOT for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath`) do set "VSROOT=%%I" +if not defined VSROOT ( + echo [ERROR] Visual Studio 2022 not found. + exit /b 9001 +) + +( + call "%VSROOT%\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 + echo [INFO] VS path: %VSROOT% + ver + echo [INFO] After VsDevCmd, trying to locate tools... + where cl + echo [INFO] VCToolsInstallDir=%VCToolsInstallDir% + echo [INFO] VisualStudioVersion=%VisualStudioVersion% + echo [INFO] WindowsSDKDir=%WindowsSDKDir% + cd /d C:\Users\yuiseki\src\maplibre-native-slint + + REM Disable VS-bundled vcpkg and set overlay triplets per project + set "VCPKG_ROOT=" + set "VCPKG_OVERLAY_TRIPLETS=%cd%\vendor\maplibre-native\platform\windows\vendor\vcpkg-custom-triplets" + REM Constrain skia-bindings features to avoid Direct3D wrappers (vcpkg skia lacks D3D) + set "SKIA_FEATURES=gl,skshaper,skparagraph,icu" + + REM Clear cross/toolchain vars that may leak from parent env (breaks MSVC detection) + for %%V in (CC CXX CFLAGS CXXFLAGS CPPFLAGS LDFLAGS AR LD RC STRIP RANLIB OBJC OBJCXX) do set "%%V=" + + REM Verify compilers and ninja + where cl || ( + echo [ERROR] cl.exe not found. VsDevCmd may have failed. + exit /b 9009 + ) + + where ninja >nul 2>&1 || ( + set "NINJA_EXE=%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe" + if exist "%NINJA_EXE%" set "CMAKE_MAKE_FLAG=-DCMAKE_MAKE_PROGRAM=%NINJA_EXE%" + ) + + REM Clean as instructed + if exist build-ninja rmdir /s /q build-ninja + + REM Configure with Ninja (vcpkg toolchain specified) + cmake -S . -B build-ninja -G "Ninja" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DMLNS_WITH_SLINT_GL=ON ^ + -DMLNS_FORCE_FETCH_SLINT=ON ^ + -DCMAKE_TOOLCHAIN_FILE=C:/src/vcpkg/scripts/buildsystems/vcpkg.cmake ^ + -DVCPKG_TARGET_TRIPLET=x64-windows ^ + -DVCPKG_HOST_TRIPLET=x64-windows ^ + %CMAKE_MAKE_FLAG% + if errorlevel 1 exit /b !errorlevel! + + REM Build as instructed + cmake --build build-ninja -v -j + set "_ret=!errorlevel!" + if not "%_ret%"=="0" exit /b %_ret% + + REM Auto-run the example with GL zero-copy if requested (default ON) + if not defined MLNS_AUTO_RUN set "MLNS_AUTO_RUN=1" + if "%MLNS_AUTO_RUN%"=="1" ( + pushd build-ninja + echo [auto-run] Launching maplibre-slint-example with MLNS_USE_GL=1 (SLINT_RENDERER=gl) + start "maplibre-slint-example" cmd /C "set MLNS_USE_GL=1& set SLINT_RENDERER=gl& maplibre-slint-example.exe" + popd + ) + exit /b 0 +) diff --git a/scripts/_reconfigure-win.cmd b/scripts/_reconfigure-win.cmd new file mode 100644 index 0000000..1f88c2c --- /dev/null +++ b/scripts/_reconfigure-win.cmd @@ -0,0 +1,30 @@ +@echo off +setlocal + +rem Resolve Visual Studio 2022 installation (prefer Community) +set "VSROOT=" +for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products Microsoft.VisualStudio.Product.Community -property installationPath`) do set "VSROOT=%%I" +if not defined VSROOT for /f "usebackq delims=" %%I in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath`) do set "VSROOT=%%I" +if not defined VSROOT ( + echo [ERROR] Visual Studio 2022 not found. + exit /b 1 +) + +call "%VSROOT%\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 || exit /b %errorlevel% +cd /d C:\Users\yuiseki\src\maplibre-native-slint || exit /b %errorlevel% + +rem Align vcpkg env with project +set VCPKG_ROOT= +set VCPKG_OVERLAY_TRIPLETS=%cd%\vendor\maplibre-native\platform\windows\vendor\vcpkg-custom-triplets +set SKIA_FEATURES=gl,skshaper,skparagraph,icu + +cmake -S . -B build-ninja -G "Ninja" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DMLNS_WITH_SLINT_GL=ON ^ + -DMLNS_FORCE_FETCH_SLINT=ON ^ + -DCMAKE_TOOLCHAIN_FILE=C:/src/vcpkg/scripts/buildsystems/vcpkg.cmake ^ + -DVCPKG_TARGET_TRIPLET=x64-windows ^ + -DVCPKG_HOST_TRIPLET=x64-windows || exit /b %errorlevel% + +cmake --build build-ninja -v -j +exit /b %errorlevel% diff --git a/scripts/_run-gl-zerocopy.cmd b/scripts/_run-gl-zerocopy.cmd new file mode 100644 index 0000000..9f5afd3 --- /dev/null +++ b/scripts/_run-gl-zerocopy.cmd @@ -0,0 +1,18 @@ +@echo off +setlocal +set "REPO=C:\Users\yuiseki\src\maplibre-native-slint" +cd /d "%REPO%" || exit /b 1 + +REM Ensure app is built +if not exist build-ninja\maplibre-slint-example.exe ( + echo [build] Configuring + building (Ninja) + call "%REPO%\_reconfigure-win.cmd" || exit /b %errorlevel% +) + +echo [run] MLNS_USE_GL=1 SLINT_RENDERER=gl +set MLNS_USE_GL=1 +set SLINT_RENDERER=gl +cd build-ninja || exit /b 1 +start "maplibre-slint-example" maplibre-slint-example.exe +exit /b 0 + diff --git a/scripts/analyze-dump.cmd b/scripts/analyze-dump.cmd new file mode 100644 index 0000000..ca03fc0 --- /dev/null +++ b/scripts/analyze-dump.cmd @@ -0,0 +1,161 @@ +@echo off +setlocal ENABLEDELAYEDEXPANSION + +REM Analyze a crash dump with cdb and write a clean log (no copy/paste hassles). +REM Usage: +REM scripts\analyze-dump.cmd (fast local-symbols, analyzes latest dumps\*.dmp) +REM scripts\analyze-dump.cmd (fast local-symbols) +REM scripts\analyze-dump.cmd --full [dump_path] (full symbols via MS server + build-ninja) +REM scripts\analyze-dump.cmd --mini [dump_path] (very quick scan, minimal output) + +REM Resolve repository root (this script is under scripts/) +set "REPO=%~dp0.." +for %%G in ("%REPO%") do set "REPO=%%~fG" + +REM Prefer the 64-bit Console Debugger (cdb) +set "CDB=" +for %%P in ( + "%ProgramFiles(x86)%\Windows Kits\11\Debuggers\x64\cdb.exe" + "%ProgramFiles%\Windows Kits\11\Debuggers\x64\cdb.exe" + "%ProgramFiles(x86)%\Windows Kits\10\Debuggers\x64\cdb.exe" + "%ProgramFiles%\Windows Kits\10\Debuggers\x64\cdb.exe" +) do ( + if exist %%~P ( + set "CDB=%%~P" + goto :cdb_found + ) +) +for /f "delims=" %%F in ('where cdb 2^>NUL') do if not defined CDB set "CDB=%%~F" + +:cdb_found +if not defined CDB ( + echo [error] cdb.exe not found. Install "Debugging Tools for Windows" via VS Installer. + exit /b 1 +) + +REM Parse args (optional --full) +set "FULL=0" +set "MINI=0" +if /I "%~1"=="--full" ( + set "FULL=1" + shift +) +if /I "%~1"=="--mini" ( + set "MINI=1" + shift +) + +REM Pick dump file +set "DUMP=%~1" +if not defined DUMP ( + if exist "%REPO%\dumps\*.dmp" ( + for /f "delims=" %%F in ('dir /b /a:-d /o:-d "%REPO%\dumps\*.dmp"') do ( + set "DUMP=%REPO%\dumps\%%F" + goto :dump_ok + ) + ) +) +:dump_ok +if not defined DUMP ( + echo [error] No .dmp specified and no dumps found under %REPO%\dumps + exit /b 2 +) +if not exist "%DUMP%" ( + echo [error] Dump not found: %DUMP% + exit /b 3 +) + +REM Timestamp for log file +for /f %%t in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "TS=%%t" +if not defined TS set "TS=now" +if not exist "%REPO%\dumps" mkdir "%REPO%\dumps" >NUL 2>&1 +set "LOG=%REPO%\dumps\analyze_%TS%.txt" + +REM Ensure symbol cache folder exists +if not exist "C:\symbols" mkdir "C:\symbols" >NUL 2>&1 + +REM Compose debugger command script (write to file to avoid '!' expansion issues) +set "CMDS=%REPO%\dumps\dbgcmds_%TS%.txt" +if "%MINI%"=="1" ( + ( + echo .symfix + echo .sympath srv*c:\symbols + echo .reload + echo .exr -1 + echo .ecxr + echo r + echo ln @rip + echo u @rip-32 L80 + echo kv 40 + echo lm m opengl32 + echo lm m nv*gl* + echo q + ) > "%CMDS%" +) else if "%FULL%"=="1" ( + ( + echo .symfix + echo .sympath+ srv*c:\symbols*https://msdl.microsoft.com/download/symbols + echo .sympath+ "%REPO%\build-ninja" + echo .reload /f + echo .exr -1 + echo .ecxr + echo r + echo ln @rip + echo u @rip-40 L100 + echo kv 200 + echo !analyze -v + echo .echo [loaded GL modules] + echo lm m opengl32 + echo lm m nv*gl* + echo lm m ig9icd64 + echo lm m amd*ogl* + echo .echo [suspect exports] + echo x opengl32!*Swap* + echo x opengl32!*wgl* + echo .echo [threads top15] + echo ~* kP 15 + echo .echo [end] + echo q + ) > "%CMDS%" +) else ( + REM Fast path: local symbols only, targeted module reloads to avoid stalls + ( + echo .symfix + echo .sympath srv*c:\symbols + echo .reload /f opengl32 + echo .reload /f ntdll + echo .reload /f kernel32 + echo .reload /f user32 + echo .exr -1 + echo .ecxr + echo r + echo ln @rip + echo u @rip-40 L100 + echo kv 200 + echo !analyze -v + echo .echo [loaded GL modules] + echo lm m opengl32 + echo lm m nv*gl* + echo lm m ig9icd64 + echo lm m amd*ogl* + echo .echo [suspect exports] + echo x opengl32!*Swap* + echo x opengl32!*wgl* + echo .echo [end] + echo q + ) > "%CMDS%" +) + +echo [info] CDB: "%CDB%" +echo [info] Dump: "%DUMP%" +echo [info] Log : "%LOG%" +echo [info] Cmds: "%CMDS%" + +"%CDB%" -z "%DUMP%" -logo "%LOG%" -cf "%CMDS%" +set "ERR=%ERRORLEVEL%" +if not "%ERR%"=="0" ( + echo [warn] cdb exited with code %ERR%. Review the log for details. +) +echo. +echo [done] Wrote analysis to: "%LOG%" +exit /b %ERR% diff --git a/scripts/build-win-from-wsl.sh b/scripts/build-win-from-wsl.sh new file mode 100644 index 0000000..cbe93bf --- /dev/null +++ b/scripts/build-win-from-wsl.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build helper to invoke VS2022 x64 Native Tools from WSL2 and run +# `cmake --build build-ninja -j` in the Windows repo path. +# +# Usage (run inside this repo under WSL2): +# scripts/build-win-from-wsl.sh [--verbose] +# +# Notes: +# - This does not reconfigure CMake. Ensure `build-ninja` already exists. +# - It locates VS2022 via vswhere, calls VsDevCmd, then builds. + +verbose=false +if [[ "${1:-}" == "--verbose" ]]; then + verbose=true + shift || true +fi + +# Resolve repository root and convert to a Windows path +REPO_WSL_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +REPO_WIN_ROOT="$(wslpath -w "$REPO_WSL_ROOT")" + +# Compose the command that runs under a single cmd.exe session so that +# VsDevCmd environment persists into the build step. +read -r -d '' CMD_BODY <<'PSCMDS' || true +$vs = $null +try { + $vs = & "$Env:ProgramFiles(x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath +} catch {} +if (-not $vs) { + $candidates = @( + "$Env:ProgramFiles\Microsoft Visual Studio\2022\Community", + "$Env:ProgramFiles\Microsoft Visual Studio\2022\Professional", + "$Env:ProgramFiles\Microsoft Visual Studio\2022\Enterprise" + ) + foreach ($c in $candidates) { if (Test-Path $c) { $vs = $c; break } } +} +if (-not $vs) { Write-Error "Visual Studio 2022 not found (vswhere/default paths)"; exit 9001 } +$bat = '"' + $vs + '\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64' +$cd = 'cd /d "${REPO_WIN_ROOT}"' +$log = '"${REPO_WIN_ROOT}\\build-ninja\\wsl_build.log"' +$cm = 'cmake --build build-ninja -j 1 > ' + $log + ' 2>&1' +if ('${VERBOSE}' -eq '1') { $cm = 'cmake --build build-ninja -v -j 1 > ' + $log + ' 2>&1' } +$line = 'call ' + $bat + ' && ' + $cd + ' && ' + $cm +cmd.exe /c $line +Write-Output "`n--- tail: build-ninja\\wsl_build.log (last 200 lines) ---" +Get-Content -Tail 200 -Path "${REPO_WIN_ROOT}\build-ninja\wsl_build.log" +exit $LASTEXITCODE +PSCMDS + +# Inject variables and invoke PowerShell +pwsh_args=( + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command +) + +VERBOSE_FLAG="0" +"$verbose" && VERBOSE_FLAG="1" + +# Replace placeholders in the PS script +PS_SCRIPT="${CMD_BODY//\$\{REPO_WIN_ROOT\}/$REPO_WIN_ROOT}" +PS_SCRIPT="${PS_SCRIPT//\$\{VERBOSE\}/$VERBOSE_FLAG}" + +"${pwsh_args[@]}" "$PS_SCRIPT" diff --git a/scripts/format-cpp.sh b/scripts/format-cpp.sh new file mode 100644 index 0000000..cdac5e8 --- /dev/null +++ b/scripts/format-cpp.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Format all C++ sources, excluding vendor/, build/, build-ninja/, and .git/ +# Usage: scripts/format-cpp.sh + +if ! command -v clang-format >/dev/null 2>&1; then + echo "clang-format not found in PATH" >&2 + exit 127 +fi + +# Build file list while excluding directories via -prune, then print each +find . \ + -type d \( -path ./vendor -o -path ./build -o -path ./build-ninja -o -path ./.git \) -prune \ + -o \( -name "*.cpp" -o -name "*.hpp" \) -print0 \ +| while IFS= read -r -d '' f; do + echo "format: $f" + clang-format -i "$f" + done + +echo "C++ sources formatted (exclusions: vendor/, build/, build-ninja/, .git/)." diff --git a/scripts/run_windbg_maplibre.bat b/scripts/run_windbg_maplibre.bat new file mode 100644 index 0000000..5b42d5e --- /dev/null +++ b/scripts/run_windbg_maplibre.bat @@ -0,0 +1,85 @@ +@echo off +setlocal ENABLEDELAYEDEXPANSION +REM Run maplibre-slint-example under WinDbg/WinDbgX and auto-capture crash info (Windows 10/11) + +REM Resolve repository root (this script may be invoked from scripts/) +set "REPO=%~dp0.." +for %%# in ("%REPO%") do set "REPO=%%~f#" +pushd "%REPO%" >NUL +REM Target app under repo root +set "APP_EXE=%REPO%\build-ninja\maplibre-slint-example.exe" +set "DBGEXE=" +for %%P in ( + "%ProgramFiles%\Windows Kits\11\Debuggers\x64\windbgx.exe" + "%ProgramFiles(x86)%\Windows Kits\11\Debuggers\x64\windbgx.exe" + "%ProgramFiles%\Windows Kits\10\Debuggers\x64\windbgx.exe" + "%ProgramFiles(x86)%\Windows Kits\10\Debuggers\x64\windbgx.exe" + "%ProgramFiles%\Windows Kits\11\Debuggers\x64\windbg.exe" + "%ProgramFiles(x86)%\Windows Kits\11\Debuggers\x64\windbg.exe" + "%ProgramFiles%\Windows Kits\10\Debuggers\x64\windbg.exe" + "%ProgramFiles(x86)%\Windows Kits\10\Debuggers\x64\windbg.exe" + "%ProgramFiles%\Windows Kits\11\Debuggers\x64\cdb.exe" + "%ProgramFiles(x86)%\Windows Kits\11\Debuggers\x64\cdb.exe" + "%ProgramFiles%\Windows Kits\10\Debuggers\x64\cdb.exe" + "%ProgramFiles(x86)%\Windows Kits\10\Debuggers\x64\cdb.exe" +) do ( + if exist %%~P ( + set "DBGEXE=%%~P" + goto :dbg_found + ) +) + +REM Fallback: try locating via WHERE (might be slower) +for %%D in (windbgx.exe windbg.exe cdb.exe) do ( + for /f "delims=" %%F in ('where %%D 2^>NUL') do ( + if not defined DBGEXE set "DBGEXE=%%~F" + ) +) + +:dbg_found +if not defined DBGEXE ( + echo [error] Debugger not found. Install "Windows SDK - Debugging Tools for Windows". + echo Tips: In Visual Studio Installer, add "Windows 11/10 SDK" and "Debugging Tools". + exit /b 1 +) + +if not exist "%APP_EXE%" ( + echo [error] App not found: %APP_EXE% + echo Build first (e.g. scripts\_reconfigure-win.cmd or scripts\_build-ninja-win.cmd) + popd >NUL + exit /b 2 +) + +REM Environment for reproducible crash (tune if needed) +set "SLINT_RENDERER=gl" +set "MLNS_FORCE_DESKTOP_GL=1" +REM Guard + isolate mode from args +REM usage: run_windbg_maplibre.bat [iso|noiso] +set "_ISO=%1" +if /i "%_ISO%"=="iso" ( + set "MLNS_GL_ISOLATE_CONTEXT=1" +) else ( + set "MLNS_GL_ISOLATE_CONTEXT=0" +) +REM Use minimal safe-mode to keep renderer functional, and enable vptr guard +set "MLNS_GL_SAFE_MODE=1" +set "MLNS_GL_VPTR_GUARD=1" + +REM WinDbg startup commands: +REM - set symbol path, open timestamped log +REM - break on AV (0xC0000005) and 0xC000041D with an analysis macro, then quit +set "WDBGCMD=.symfix; .sympath+ srv*c:\symbols*https://msdl.microsoft.com/download/symbols; .sympath+ %REPO%\build-ninja; .reload;" +set "WDBGCMD=%WDBGCMD% .logopen /t %REPO%\windbg_maplibre.log; sxd gp;" +set "WDBGCMD=%WDBGCMD% sxe -c \".printf --- EX AV ---\n; .time; !gle; .exr -1; .ecxr; kv 200; !analyze -v; .dump /ma %REPO%\windbg_maplibre.dmp; .logclose; q\" av;" +set "WDBGCMD=%WDBGCMD% sxe -c \".printf --- EX 0xC000041D ---\n; .time; !gle; .exr -1; .ecxr; kv 200; !analyze -v; .dump /ma %REPO%\windbg_maplibre.dmp; .logclose; q\" 041D;" +set "WDBGCMD=%WDBGCMD% g" + +echo [info] Launching debugger: "%DBGEXE%" +echo [info] App: "%APP_EXE%" +echo [info] Commands: %WDBGCMD% + +"%DBGEXE%" -o -g -G -c "%WDBGCMD%" "%APP_EXE%" + +popd >NUL + +endlocal diff --git a/src/gl/slint_gl_maplibre.cpp b/src/gl/slint_gl_maplibre.cpp new file mode 100644 index 0000000..b108aff --- /dev/null +++ b/src/gl/slint_gl_maplibre.cpp @@ -0,0 +1,1597 @@ +#include "gl/slint_gl_maplibre.hpp" + +#include +#include +#include +#include +#include +#include +#include +#ifdef _MSC_VER +#include +#endif + +// Forward declare guard for use before its definition in some compilers +static bool is_executable_ptr(const void* p); +static bool looks_like_vftable(const void* vft_ptr); +#ifdef _WIN32 +static void log_vftable(const char* tag, const void* vft_ptr); +#endif +#ifdef _WIN32 +enum VptrGuardMode { GUARD_OFF = 0, GUARD_WARN = 1, GUARD_STRICT = 2 }; +static int get_vptr_guard_mode() { + static int mode = []() { + const char* v = std::getenv("MLNS_GL_VPTR_GUARD"); + if (!v) + return GUARD_WARN; // default warn-only + std::string s(v); + for (auto& c : s) + c = static_cast(::tolower(static_cast(c))); + if (s == "0" || s == "false" || s == "off" || s == "no") + return GUARD_OFF; + if (s == "2" || s == "strict") + return GUARD_STRICT; + return GUARD_WARN; // 1/on/true/warn + }(); + return mode; +} +#endif + +// was early misplaced; definitions moved below within namespace mlns where GL +// helpers are available +#ifdef _WIN32 +#include +#include +// Provide missing calling convention macro used in extension typedefs +#ifndef APIENTRYP +#ifndef APIENTRY +#define APIENTRY __stdcall +#endif +#define APIENTRYP APIENTRY* +#endif +// Older GL headers on Windows may not define these pointer-sized types +#include +#ifndef GLintptr +typedef ptrdiff_t GLintptr; +#endif +#ifndef GLsizeiptr +typedef ptrdiff_t GLsizeiptr; +#endif +#ifndef GLbitfield +typedef unsigned int GLbitfield; +#endif +#ifndef GLchar +typedef char GLchar; +#endif +// Define missing enums for GL 1.1 headers on Windows +#ifndef GL_FRAMEBUFFER +#define GL_FRAMEBUFFER 0x8D40 +#endif +#ifndef GL_DRAW_FRAMEBUFFER +#define GL_DRAW_FRAMEBUFFER 0x8CA9 +#endif +#ifndef GL_READ_FRAMEBUFFER +#define GL_READ_FRAMEBUFFER 0x8CA8 +#endif +#ifndef GL_RENDERBUFFER +#define GL_RENDERBUFFER 0x8D41 +#endif +#ifndef GL_COLOR_ATTACHMENT0 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#endif +#ifndef GL_DEPTH_STENCIL_ATTACHMENT +#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A +#endif +#ifndef GL_FRAMEBUFFER_COMPLETE +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#endif +#ifndef GL_FRAMEBUFFER_BINDING +#define GL_FRAMEBUFFER_BINDING 0x8CA6 +#endif +#ifndef GL_RGBA8 +#define GL_RGBA8 0x8058 +#endif +#ifndef GL_CLAMP_TO_EDGE +#define GL_CLAMP_TO_EDGE 0x812F +#endif +#ifndef GL_TEXTURE_BINDING_2D +#define GL_TEXTURE_BINDING_2D 0x8069 +#endif +// Common enums not present in GL 1.1 headers +#ifndef GL_ARRAY_BUFFER +#define GL_ARRAY_BUFFER 0x8892 +#endif +#ifndef GL_ELEMENT_ARRAY_BUFFER +#define GL_ELEMENT_ARRAY_BUFFER 0x8893 +#endif +#ifndef GL_VERTEX_ARRAY_BINDING +#define GL_VERTEX_ARRAY_BINDING 0x85B5 +#endif +#ifndef GL_ACTIVE_TEXTURE +#define GL_ACTIVE_TEXTURE 0x84E0 +#endif +#ifndef GL_TEXTURE0 +#define GL_TEXTURE0 0x84C0 +#endif +#ifndef GL_SHADING_LANGUAGE_VERSION +#define GL_SHADING_LANGUAGE_VERSION 0x8B8C +#endif +#ifndef GL_DEPTH24_STENCIL8 +#define GL_DEPTH24_STENCIL8 0x88F0 +#endif +#ifndef GL_FRAMEBUFFER_SRGB +#define GL_FRAMEBUFFER_SRGB 0x8DB9 +#endif + +// Declare function pointer types for FBO entry points +typedef void(APIENTRYP PFNGLGENFRAMEBUFFERSPROC)(GLsizei, GLuint*); +typedef void(APIENTRYP PFNGLDELETEFRAMEBUFFERSPROC)(GLsizei, const GLuint*); +typedef void(APIENTRYP PFNGLBINDFRAMEBUFFERPROC)(GLenum, GLuint); +typedef void(APIENTRYP PFNGLFRAMEBUFFERTEXTURE2DPROC)(GLenum, GLenum, GLenum, + GLuint, GLint); +typedef GLenum(APIENTRYP PFNGLCHECKFRAMEBUFFERSTATUSPROC)(GLenum); +typedef void(APIENTRYP PFNGLGENRENDERBUFFERSPROC)(GLsizei, GLuint*); +typedef void(APIENTRYP PFNGLBINDRENDERBUFFERPROC)(GLenum, GLuint); +typedef void(APIENTRYP PFNGLRENDERBUFFERSTORAGEPROC)(GLenum, GLenum, GLsizei, + GLsizei); +typedef void(APIENTRYP PFNGLFRAMEBUFFERRENDERBUFFERPROC)(GLenum, GLenum, GLenum, + GLuint); +typedef void(APIENTRYP PFNGLDELETERENDERBUFFERSPROC)(GLsizei, const GLuint*); + +// Additional GL entry points we may need to stub in ultra-safe mode +typedef const GLubyte*(APIENTRYP PFNGLGETSTRINGIPROC)(GLenum, GLuint); +typedef void(APIENTRYP PFNGLBINDFRAGDATALOCATIONPROC)(GLuint, GLuint, + const GLchar*); +typedef void*(APIENTRYP PFNGLMAPBUFFERRANGEPROC)(GLenum, GLintptr, GLsizeiptr, + GLbitfield); +typedef void(APIENTRYP PFNGLFLUSHMAPPEDBUFFERRANGEPROC)(GLenum, GLintptr, + GLsizeiptr); +typedef void(APIENTRYP PFNGLINVALIDATEFRAMEBUFFERPROC)(GLenum, GLsizei, + const GLenum*); +typedef void(APIENTRYP PFNGLINVALIDATESUBFRAMEBUFFERPROC)(GLenum, GLsizei, + const GLenum*, GLint, + GLint, GLsizei, + GLsizei); + +// Forward declarations for ultra-safe stubs (defined at file end) +static const GLubyte* APIENTRY stub_glGetStringi(GLenum, GLuint); +static void APIENTRY stub_glBindFragDataLocation(GLuint, GLuint, const GLchar*); +static void* APIENTRY stub_glMapBufferRange(GLenum, GLintptr, GLsizeiptr, + GLbitfield); +static void APIENTRY stub_glFlushMappedBufferRange(GLenum, GLintptr, + GLsizeiptr); +static void APIENTRY stub_glInvalidateFramebuffer(GLenum, GLsizei, + const GLenum*); +static void APIENTRY stub_glInvalidateSubFramebuffer(GLenum, GLsizei, + const GLenum*, GLint, + GLint, GLsizei, GLsizei); + +static PFNGLGENFRAMEBUFFERSPROC p_glGenFramebuffers = nullptr; +static PFNGLDELETEFRAMEBUFFERSPROC p_glDeleteFramebuffers = nullptr; +static PFNGLBINDFRAMEBUFFERPROC p_glBindFramebuffer = nullptr; +static PFNGLFRAMEBUFFERTEXTURE2DPROC p_glFramebufferTexture2D = nullptr; +static PFNGLCHECKFRAMEBUFFERSTATUSPROC p_glCheckFramebufferStatus = nullptr; +static PFNGLGENRENDERBUFFERSPROC p_glGenRenderbuffers = nullptr; +static PFNGLBINDRENDERBUFFERPROC p_glBindRenderbuffer = nullptr; +static PFNGLRENDERBUFFERSTORAGEPROC p_glRenderbufferStorage = nullptr; +static PFNGLFRAMEBUFFERRENDERBUFFERPROC p_glFramebufferRenderbuffer = nullptr; +static PFNGLDELETERENDERBUFFERSPROC p_glDeleteRenderbuffers = nullptr; + +// wgl current context +typedef HGLRC(WINAPI* PFNWGLGETCURRENTCONTEXTPROC)(); +static PFNWGLGETCURRENTCONTEXTPROC p_wglGetCurrentContext = nullptr; +typedef HDC(WINAPI* PFNWGLGETCURRENTDCPROC)(); +static PFNWGLGETCURRENTDCPROC p_wglGetCurrentDC = nullptr; +typedef HGLRC(WINAPI* PFNWGLCREATECONTEXTPROC)(HDC); +static PFNWGLCREATECONTEXTPROC p_wglCreateContext = nullptr; +typedef BOOL(WINAPI* PFNWGLDELETECONTEXTPROC)(HGLRC); +static PFNWGLDELETECONTEXTPROC p_wglDeleteContext = nullptr; +typedef BOOL(WINAPI* PFNWGLMAKECURRENTPROC)(HDC, HGLRC); +static PFNWGLMAKECURRENTPROC p_wglMakeCurrent = nullptr; +typedef BOOL(WINAPI* PFNWGLSHARELISTSPROC)(HGLRC, HGLRC); +static PFNWGLSHARELISTSPROC p_wglShareLists = nullptr; + +// A few modern helpers we may use to leave GL in a clean state after +// MapLibre renders. These are loaded dynamically when available and skipped +// otherwise so we can still run against legacy headers/contexts. +typedef void(APIENTRYP PFNGLUSEPROGRAMPROC)(GLuint); +typedef void(APIENTRYP PFNGLBINDBUFFERPROC)(GLenum, GLuint); +typedef void(APIENTRYP PFNGLBINDVERTEXARRAYPROC)(GLuint); +typedef void(APIENTRYP PFNGLGENVERTEXARRAYSPROC)(GLsizei, GLuint*); +typedef void(APIENTRYP PFNGLDELETEVERTEXARRAYSPROC)(GLsizei, const GLuint*); +typedef void(APIENTRYP PFNGLACTIVETEXTUREPROC)(GLenum); +typedef void(APIENTRYP PFNGLDRAWBUFFERSPROC)(GLsizei, const GLenum*); + +static PFNGLUSEPROGRAMPROC p_glUseProgram = nullptr; +static PFNGLBINDBUFFERPROC p_glBindBuffer = nullptr; +static PFNGLBINDVERTEXARRAYPROC p_glBindVertexArray = nullptr; +static PFNGLACTIVETEXTUREPROC p_glActiveTexture = nullptr; +static PFNGLDRAWBUFFERSPROC p_glDrawBuffers = nullptr; +static PFNGLGENVERTEXARRAYSPROC p_glGenVertexArrays = nullptr; +static PFNGLDELETEVERTEXARRAYSPROC p_glDeleteVertexArrays = nullptr; + +// Minimal cross-loader for GL/ES symbols on Windows. Prefer the active API: +// - If a WGL context is current, use wglGetProcAddress/opengl32.dll +// - Otherwise try ANGLE/EGL: eglGetProcAddress + libGLESv2.dll +static bool env_truthy(const char* v) { + if (!v) + return false; + std::string s(v); + for (auto& c : s) + c = static_cast(::tolower(static_cast(c))); + return (s == "1" || s == "true" || s == "on" || s == "yes"); +} + +static FARPROC load_gl_proc(const char* name) { + static const bool force_desktop_gl = + env_truthy(std::getenv("MLNS_FORCE_DESKTOP_GL")); + // Bootstrap: resolve WGL entry points directly from opengl32.dll + if (std::strncmp(name, "wgl", 3) == 0) { + HMODULE lib = GetModuleHandleA("opengl32.dll"); + if (lib) { + FARPROC p = GetProcAddress(lib, name); + if (p) + return p; + } + } + // Ensure we can query WGL current context + if (!p_wglGetCurrentContext) { + HMODULE lib = GetModuleHandleA("opengl32.dll"); + if (lib) { + p_wglGetCurrentContext = + reinterpret_cast( + GetProcAddress(lib, "wglGetCurrentContext")); + } + } + const bool have_wgl = + p_wglGetCurrentContext && p_wglGetCurrentContext() != nullptr; + if (have_wgl) { + // When a WGL context is current, NEVER mix in EGL/GLES function + // pointers. + FARPROC p = wglGetProcAddress(name); + if (!p) { + HMODULE lib = GetModuleHandleA("opengl32.dll"); + if (lib) + p = GetProcAddress(lib, name); + } + if (p && p != reinterpret_cast(1) && + p != reinterpret_cast(2) && + p != reinterpret_cast(3) && + p != reinterpret_cast(-1)) { + return p; + } + // No fallback to EGL here by design; return null so callers can + // stub/deny. + return nullptr; + } + if (force_desktop_gl) { + // Avoid falling back to EGL/ANGLE if explicitly requested. + return nullptr; + } + // ANGLE / EGL path (only when no WGL context is current) + using PFNEGLGETPROCADDRESSPROC = void*(WINAPI*)(const char*); + static PFNEGLGETPROCADDRESSPROC p_eglGetProcAddress = nullptr; + static bool egl_loaded = false; + if (!egl_loaded) { + HMODULE egl = GetModuleHandleA("libEGL.dll"); + if (!egl) + egl = GetModuleHandleA("EGL.dll"); + if (egl) + p_eglGetProcAddress = reinterpret_cast( + GetProcAddress(egl, "eglGetProcAddress")); + egl_loaded = true; + } + // Prefer core symbols from libGLESv2.dll for ES core functions + void* vp = nullptr; + HMODULE gles = GetModuleHandleA("libGLESv2.dll"); + if (!gles) + gles = GetModuleHandleA("GLESv2.dll"); + if (!gles) + gles = LoadLibraryA("libGLESv2.dll"); + if (!gles) + gles = LoadLibraryA("GLESv2.dll"); + if (gles) + vp = reinterpret_cast(GetProcAddress(gles, name)); + // If still not found, try eglGetProcAddress (extensions) + if (!vp && p_eglGetProcAddress) + vp = p_eglGetProcAddress(name); + return reinterpret_cast(vp); +} + +static bool ensure_gl_functions_loaded() { + if (p_glGenFramebuffers) + return true; // already loaded + // Ensure we can query WGL current status in this process +#ifdef _WIN32 + if (!p_wglGetCurrentContext) { + HMODULE lib = GetModuleHandleA("opengl32.dll"); + if (lib) + p_wglGetCurrentContext = + reinterpret_cast( + GetProcAddress(lib, "wglGetCurrentContext")); + if (lib) { + p_wglGetCurrentDC = reinterpret_cast( + GetProcAddress(lib, "wglGetCurrentDC")); + p_wglCreateContext = reinterpret_cast( + GetProcAddress(lib, "wglCreateContext")); + p_wglDeleteContext = reinterpret_cast( + GetProcAddress(lib, "wglDeleteContext")); + p_wglMakeCurrent = reinterpret_cast( + GetProcAddress(lib, "wglMakeCurrent")); + p_wglShareLists = reinterpret_cast( + GetProcAddress(lib, "wglShareLists")); + } + } +#endif + auto load_any = [](const char* const* names) -> FARPROC { + for (size_t i = 0; names[i]; ++i) { + FARPROC p = load_gl_proc(names[i]); + if (p) + return p; + } + return nullptr; + }; + { + const char* names[] = {"glGenFramebuffers", "glGenFramebuffersEXT", + "glGenFramebuffersOES", nullptr}; + p_glGenFramebuffers = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glDeleteFramebuffers", + "glDeleteFramebuffersEXT", + "glDeleteFramebuffersOES", nullptr}; + p_glDeleteFramebuffers = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glBindFramebuffer", "glBindFramebufferEXT", + "glBindFramebufferOES", nullptr}; + p_glBindFramebuffer = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glFramebufferTexture2D", + "glFramebufferTexture2DEXT", + "glFramebufferTexture2DOES", nullptr}; + p_glFramebufferTexture2D = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glCheckFramebufferStatus", + "glCheckFramebufferStatusEXT", + "glCheckFramebufferStatusOES", nullptr}; + p_glCheckFramebufferStatus = + reinterpret_cast(load_any(names)); + } + // Renderbuffer functions (depth-stencil) + { + const char* names[] = {"glGenRenderbuffers", "glGenRenderbuffersEXT", + "glGenRenderbuffersOES", nullptr}; + p_glGenRenderbuffers = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glBindRenderbuffer", "glBindRenderbufferEXT", + "glBindRenderbufferOES", nullptr}; + p_glBindRenderbuffer = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glRenderbufferStorage", + "glRenderbufferStorageEXT", + "glRenderbufferStorageOES", nullptr}; + p_glRenderbufferStorage = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glFramebufferRenderbuffer", + "glFramebufferRenderbufferEXT", + "glFramebufferRenderbufferOES", nullptr}; + p_glFramebufferRenderbuffer = + reinterpret_cast(load_any(names)); + } + { + const char* names[] = {"glDeleteRenderbuffers", + "glDeleteRenderbuffersEXT", + "glDeleteRenderbuffersOES", nullptr}; + p_glDeleteRenderbuffers = + reinterpret_cast(load_any(names)); + } + if (!p_wglGetCurrentContext) { + p_wglGetCurrentContext = reinterpret_cast( + load_gl_proc("wglGetCurrentContext")); + } + bool ok = p_glGenFramebuffers && p_glDeleteFramebuffers && + p_glBindFramebuffer && p_glFramebufferTexture2D && + p_glCheckFramebufferStatus; + if (!ok) { + std::cerr << "[GL] Missing FBO symbols:" << " gen=" + << (p_glGenFramebuffers ? "ok" : "null") + << " del=" << (p_glDeleteFramebuffers ? "ok" : "null") + << " bind=" << (p_glBindFramebuffer ? "ok" : "null") + << " tex2D=" << (p_glFramebufferTexture2D ? "ok" : "null") + << " status=" << (p_glCheckFramebufferStatus ? "ok" : "null") + << " rbo.gen=" << (p_glGenRenderbuffers ? "ok" : "null") + << " rbo.bind=" << (p_glBindRenderbuffer ? "ok" : "null") + << " rbo.store=" << (p_glRenderbufferStorage ? "ok" : "null") + << " rbo.attach=" + << (p_glFramebufferRenderbuffer ? "ok" : "null") + << " rbo.del=" << (p_glDeleteRenderbuffers ? "ok" : "null") + << std::endl; + } + return ok; +} + +#define glGenFramebuffers p_glGenFramebuffers +#define glDeleteFramebuffers p_glDeleteFramebuffers +#define glBindFramebuffer p_glBindFramebuffer +#define glFramebufferTexture2D p_glFramebufferTexture2D +#define glCheckFramebufferStatus p_glCheckFramebufferStatus +#define glGenRenderbuffers p_glGenRenderbuffers +#define glBindRenderbuffer p_glBindRenderbuffer +#define glRenderbufferStorage p_glRenderbufferStorage +#define glFramebufferRenderbuffer p_glFramebufferRenderbuffer +#define glDeleteRenderbuffers p_glDeleteRenderbuffers + +static void ensure_optional_gl_functions_loaded() { + if (!p_glUseProgram) + p_glUseProgram = + reinterpret_cast(load_gl_proc("glUseProgram")); + if (!p_glBindBuffer) + p_glBindBuffer = + reinterpret_cast(load_gl_proc("glBindBuffer")); + if (!p_glBindVertexArray) + p_glBindVertexArray = reinterpret_cast( + load_gl_proc("glBindVertexArray")); + if (!p_glGenVertexArrays) + p_glGenVertexArrays = reinterpret_cast( + load_gl_proc("glGenVertexArrays")); + if (!p_glDeleteVertexArrays) + p_glDeleteVertexArrays = reinterpret_cast( + load_gl_proc("glDeleteVertexArrays")); + if (!p_glActiveTexture) + p_glActiveTexture = reinterpret_cast( + load_gl_proc("glActiveTexture")); + if (!p_glDrawBuffers) { + p_glDrawBuffers = reinterpret_cast( + load_gl_proc("glDrawBuffers")); + if (!p_glDrawBuffers) + p_glDrawBuffers = reinterpret_cast( + load_gl_proc("glDrawBuffersEXT")); + } +} +#endif + +using namespace std; + +namespace mlns { + +static std::string now_ts() { + using namespace std::chrono; + const auto now = system_clock::now(); + const auto t = system_clock::to_time_t(now); + std::tm lt{}; +#ifdef _WIN32 + localtime_s(<, &t); +#else + localtime_r(&t, <); +#endif + const auto ms = duration_cast(now.time_since_epoch()) % 1000; + std::ostringstream oss; + oss << std::put_time(<, "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) + << std::setfill('0') << ms.count(); + return oss.str(); +} + +#define LOGI(expr) \ + do { \ + std::ostringstream _oss; \ + _oss << expr; \ + std::cout << now_ts() << " " << _oss.str() << std::endl; \ + } while (0) +#define LOGE(expr) \ + do { \ + std::ostringstream _oss; \ + _oss << expr; \ + std::cerr << now_ts() << " " << _oss.str() << std::endl; \ + } while (0) + +static void gl_check(const char* where) { +#ifdef _WIN32 + // On Windows' legacy headers, GL errors are defined; use them to log. + GLenum err = glGetError(); + if (err != 0) { + LOGE("[GL] error 0x" << std::hex << err << std::dec << " at " << where); + } +#endif +} + +// Returns false if GL calls look invalid (e.g. context not current) +static bool gl_context_seems_current(const char* where) { +#ifdef _WIN32 + if (p_wglGetCurrentContext && p_wglGetCurrentContext() == nullptr) { + LOGE("[ZeroCopyGL] GL not current (wglGetCurrentContext=null) at " + << where); + return false; + } + GLint fbo_probe = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo_probe); + GLenum err = glGetError(); + if (err == GL_INVALID_OPERATION) { + LOGE("[ZeroCopyGL] GL not current at " << where + << " (GL_INVALID_OPERATION)"); + return false; + } +#endif + (void)where; + return true; +} + +static void delete_rt(GLRenderTarget& rt) { + LOGI("[ZeroCopyGL] delete_rt: fbo=" << rt.fbo << " tex=" << rt.color_tex + << " size=" << rt.width << "x" + << rt.height); + if (rt.fbo) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &rt.fbo); + rt.fbo = 0; + } + if (rt.depth_stencil_rb) { + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glDeleteRenderbuffers(1, &rt.depth_stencil_rb); + rt.depth_stencil_rb = 0; + } + if (rt.color_tex) { + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &rt.color_tex); + rt.color_tex = 0; + } + rt.width = rt.height = 0; +} + +SlintZeroCopyGL::~SlintZeroCopyGL() { + teardown(); +} + +void SlintZeroCopyGL::teardown() { + std::cout << "[ZeroCopyGL] teardown()" << std::endl; + delete_rt(rt_); + borrowed_.reset(); +} + +void SlintZeroCopyGL::setup_if_needed(int w, int h) { + LOGI("[ZeroCopyGL] setup_if_needed(w=" << w << ", h=" << h << ")"); + if (rt_.valid() && rt_.width == w && rt_.height == h) { + std::cout << "[ZeroCopyGL] Already valid and size unchanged" + << std::endl; + return; + } + + // Recreate on size change + delete_rt(rt_); + + rt_.width = w; + rt_.height = h; + +#ifdef _WIN32 + if (!ensure_gl_functions_loaded()) { + LOGE("[SlintZeroCopyGL] Failed to load OpenGL FBO functions on " + "Windows."); + return; + } +#endif + + // Preserve Slint bindings during resource creation + GLint prevTex = 0; + GLint prevFbo = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTex); + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo); + + glGenTextures(1, &rt_.color_tex); + glBindTexture(GL_TEXTURE_2D, rt_.color_tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + + glGenFramebuffers(1, &rt_.fbo); + glBindFramebuffer(GL_FRAMEBUFFER, rt_.fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + rt_.color_tex, 0); + + // Create a depth-stencil renderbuffer; many pipelines assume depth testing. + glGenRenderbuffers(1, &rt_.depth_stencil_rb); + glBindRenderbuffer(GL_RENDERBUFFER, rt_.depth_stencil_rb); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, rt_.depth_stencil_rb); + + const auto status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + LOGI("[ZeroCopyGL] Created FBO=" << rt_.fbo << " tex=" << rt_.color_tex + << " status=0x" << std::hex << status + << std::dec); + if (status != GL_FRAMEBUFFER_COMPLETE) { + LOGE("[SlintZeroCopyGL] FBO incomplete: 0x" << std::hex << status + << std::dec); + delete_rt(rt_); + return; + } + + // Restore previous bindings for Slint's renderer + glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFbo)); + glBindTexture(GL_TEXTURE_2D, static_cast(prevTex)); + + // Create/refresh the borrowed Slint image for this texture id/size. + borrowed_ = slint::Image::create_from_borrowed_gl_2d_rgba_texture( + rt_.color_tex, + slint::Size{static_cast(w), + static_cast(h)}, + slint::Image::BorrowedOpenGLTextureOrigin::BottomLeft); + if (on_ready_ && borrowed_.has_value()) { + LOGI("[ZeroCopyGL] on_ready: borrowed tex=" << rt_.color_tex << " size=" + << w << "x" << h); + // Defer UI property update to the event loop to avoid mutating Slint + // state from within the rendering notifier callback. + auto img = *borrowed_; + auto cb = on_ready_; + slint::invoke_from_event_loop([img, cb]() { cb(img); }); + } +} + +void SlintZeroCopyGL::resize(int w, int h) { + setup_if_needed(w, h); +} + +void SlintZeroCopyGL::attach(slint::Window& window, + RenderHook on_before_render) { + on_before_render_ = std::move(on_before_render); + if (attached_) + return; + attached_ = true; + + window.set_rendering_notifier([this, &window](slint::RenderingState s, + slint::GraphicsAPI) { + switch (s) { + case slint::RenderingState::RenderingSetup: { + int target_w = desired_w_ > 0 + ? desired_w_ + : static_cast(window.size().width); + int target_h = desired_h_ > 0 + ? desired_h_ + : static_cast(window.size().height); + pending_w_ = target_w; + pending_h_ = target_h; + resources_ready_ = rt_.valid(); + LOGI("[ZeroCopyGL] RenderingSetup: window.size=" + << window.size().width << "x" << window.size().height + << ", target=" << pending_w_ << "x" << pending_h_ + << ", resources_ready=" + << (resources_ready_ ? "true" : "false")); + break; + } + case slint::RenderingState::BeforeRendering: { + // Refresh desired size each frame. If an explicit surface size was + // set (map image area), prefer that; otherwise use window size. + int target_w = desired_w_ > 0 + ? desired_w_ + : static_cast(window.size().width); + int target_h = desired_h_ > 0 + ? desired_h_ + : static_cast(window.size().height); + pending_w_ = target_w; + pending_h_ = target_h; + frame_counter_++; + LOGI("[ZeroCopyGL] BeforeRendering: frame=" + << frame_counter_ + << " resources_ready=" << (resources_ready_ ? "true" : "false") + << ", rt_valid=" << (rt_.valid() ? "true" : "false") + << " pending=" << pending_w_ << "x" << pending_h_); + // (GL info log omitted on Windows MSVC to avoid macro parsing + // quirks) + + if ((!resources_ready_ || !rt_.valid()) && pending_w_ > 0 && + pending_h_ > 0) { + setup_if_needed(pending_w_, pending_h_); + resources_ready_ = rt_.valid(); + LOGI("[ZeroCopyGL] BeforeRendering: setup_if_needed done, " + "rt_valid=" + << (rt_.valid() ? "true" : "false")); + // : + warmup_remaining_ = 2; + return; + } else if (rt_.valid() && + (rt_.width != pending_w_ || rt_.height != pending_h_) && + pending_w_ > 0 && pending_h_ > 0) { + setup_if_needed(pending_w_, pending_h_); + resources_ready_ = rt_.valid(); + LOGI("[ZeroCopyGL] BeforeRendering: resized RT, rt_valid=" + << (rt_.valid() ? "true" : "false")); + warmup_remaining_ = 2; + return; + } + if (!rt_.valid()) { + LOGI("[ZeroCopyGL] BeforeRendering: rt not valid, skip render"); + return; + } + if (warmup_remaining_ > 0) { + LOGI("[ZeroCopyGL] BeforeRendering: warmup skip"); + warmup_remaining_ -= 1; + return; + } + // Perform actual render here (BeforeRendering). Avoid touching swap + // path. + if (!gl_context_seems_current("BeforeRendering-entry") || + in_render_) { + break; + } + in_render_ = true; + GLint prevFbo = 0; + GLint prevViewport[4] = {0, 0, 0, 0}; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFbo); + glGetIntegerv(GL_VIEWPORT, prevViewport); + glBindFramebuffer(GL_FRAMEBUFFER, rt_.fbo); + glViewport(0, 0, rt_.width, rt_.height); + // Ultra-minimal path: do not touch masks, sRGB state, VAO, draw + // buffers, etc. + if (on_before_render_) { + LOGI("[ZeroCopyGL] BeforeRendering: calling render hook"); + try { + on_before_render_(rt_); + } catch (const std::exception& e) { + LOGE("[ZeroCopyGL] render hook threw: " << e.what()); + } catch (...) { + LOGE("[ZeroCopyGL] render hook threw unknown exception"); + } + } + glBindFramebuffer(GL_FRAMEBUFFER, static_cast(prevFbo)); + glViewport(prevViewport[0], prevViewport[1], prevViewport[2], + prevViewport[3]); + in_render_ = false; + break; + } + case slint::RenderingState::AfterRendering: { + // Avoid GL calls here to keep driver swap path stable. + window.request_redraw(); + break; + } + case slint::RenderingState::RenderingTeardown: { + std::cout << "[ZeroCopyGL] RenderingTeardown" << std::endl; + teardown(); + break; + } + } + }); +} + +} // namespace mlns + +// Controller +namespace mlns { +SlintGLMapLibre::SlintGLMapLibre() = default; +SlintGLMapLibre::~SlintGLMapLibre() { +#ifdef _WIN32 + { + std::lock_guard lk(iso_mu_); + iso_stop_ = true; + } + iso_cv_.notify_all(); + if (iso_thread_.joinable()) + iso_thread_.join(); +#endif +} + +void SlintGLMapLibre::initialize(int w, int h) { + std::cout << "[SlintGLMapLibre] initialize " << w << "x" << h << std::endl; + width = w; + height = h; + // Record style URL (used by thread-owned path or immediate init) + if (const char* env_style = std::getenv("MLNS_STYLE_URL"); + env_style && *env_style) { + style_url_ = env_style; + std::cout << "[SlintGLMapLibre] Using MLNS_STYLE_URL=" << env_style + << std::endl; + } else { + style_url_ = "https://demotiles.maplibre.org/style.json"; + } + +#ifdef _WIN32 + // Read isolate flag + isolate_ctx_ = []() { + const char* v = std::getenv("MLNS_GL_ISOLATE_CONTEXT"); + if (!v) + return true; // default ON on Windows + std::string s(v); + for (auto& c : s) + c = static_cast(::tolower(static_cast(c))); + return !(s == "0" || s == "false" || s == "off" || s == "no"); + }(); + if (isolate_ctx_) { + // Defer MapLibre object creation to the isolated render thread + thread_owns_map_ = true; + return; + } +#endif + if (!run_loop) + run_loop = std::make_unique(); + backend = std::make_unique(mbgl::gfx::ContextMode::Unique); + backend->setSize( + mbgl::Size{static_cast(w), static_cast(h)}); + auto rnd = std::make_unique(*backend, 1.0f); + frontend = + std::make_unique(std::move(rnd), *backend); + mbgl::ResourceOptions resourceOptions; + resourceOptions.withCachePath("cache.sqlite").withAssetPath("."); + map = + std::make_unique(*frontend, *this, + mbgl::MapOptions() + .withMapMode(mbgl::MapMode::Continuous) + .withSize(backend->getSize()) + .withPixelRatio(1.0f), + resourceOptions); + map->getStyle().loadURL(style_url_); +} + +void SlintGLMapLibre::resize(int w, int h) { + std::cout << "[SlintGLMapLibre] resize " << w << "x" << h << std::endl; + width = w; + height = h; + if (backend) + backend->setSize({static_cast(w), static_cast(h)}); + if (map) + map->setSize({static_cast(w), static_cast(h)}); +} + +void SlintGLMapLibre::run_map_loop() { + // NOTE: Avoid pumping MapLibre's RunLoop from Slint's timer callback to + // prevent re-entrant event processing inside the UI thread. We instead + // drive the run loop just-in-time from render_to_fbo() where the GL + // context is current, similar to the Metal path's deferred rendering. + (void)run_loop; +} + +void SlintGLMapLibre::render_to_fbo(uint32_t fbo, int w, int h) { +#ifdef _WIN32 + if (isolate_ctx_) { + // Ignore FBO; render via isolated context into shared texture path + // only. + return; + } +#endif + std::cout << "[SlintGLMapLibre] render_to_fbo fbo=" << fbo << " size=" << w + << "x" << h << std::endl; + if (!backend || !frontend) + return; + // Log GL strings once to confirm context profile + static bool info_logged = false; + if (!info_logged) { + const GLubyte* ver = glGetString(GL_VERSION); + const GLubyte* ven = glGetString(GL_VENDOR); + const GLubyte* ren = glGetString(GL_RENDERER); + const GLubyte* sl = glGetString(GL_SHADING_LANGUAGE_VERSION); + LOGI("[GL] version=" + << (ver ? reinterpret_cast(ver) : "?") + << " vendor=" << (ven ? reinterpret_cast(ven) : "?") + << " renderer=" << (ren ? reinterpret_cast(ren) : "?") + << " sl=" << (sl ? reinterpret_cast(sl) : "?")); + info_logged = true; + } + const auto desired = + mbgl::Size{static_cast(w), static_cast(h)}; + if (backend->getSize().width != desired.width || + backend->getSize().height != desired.height) { + backend->setSize(desired); + if (map) + map->setSize(desired); + } + backend->updateFramebuffer( + fbo, {static_cast(w), static_cast(h)}); + backend->setViewport(0, 0, backend->getSize()); + + // Make GL draw to our FBO with an explicit draw buffer selection (core + // profile safety) + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + ensure_optional_gl_functions_loaded(); + if (p_glDrawBuffers) { + const GLenum bufs[1] = {GL_COLOR_ATTACHMENT0}; + p_glDrawBuffers(1, bufs); + } + // Core profile safety: ensure a VAO is bound + static GLuint s_dummy_vao = 0; + if (p_glGenVertexArrays && p_glBindVertexArray && s_dummy_vao == 0) { + p_glGenVertexArrays(1, &s_dummy_vao); + } + if (p_glBindVertexArray && s_dummy_vao) { + p_glBindVertexArray(s_dummy_vao); + } + + // Process MapLibre updates outside Slint's event dispatch to avoid + // re-entrancy. Generate update parameters and then render. + if (run_loop) { + try { + run_loop->runOnce(); + } catch (...) { + // Best-effort: keep rendering even if no pending tasks + } + } + if (map) { + map->triggerRepaint(); + } + if (run_loop) { + try { + run_loop->runOnce(); + } catch (...) { + } + } + + // Extra safety: verify vtable pointers point to executable memory +#ifdef _WIN32 + const int vptr_guard_mode = get_vptr_guard_mode(); + if (vptr_guard_mode != GUARD_OFF) { + void* fe_vptr = + frontend ? *reinterpret_cast(frontend.get()) : nullptr; + void* be_vptr = + backend ? *reinterpret_cast(backend.get()) : nullptr; + mbgl::Renderer* ren_ptr = frontend ? frontend->getRenderer() : nullptr; + void* ren_vptr = ren_ptr ? *reinterpret_cast(ren_ptr) : nullptr; +#ifdef _WIN32 + static bool vft_once = false; + if (!vft_once) { + log_vftable("frontend", fe_vptr); + log_vftable("backend", be_vptr); + log_vftable("renderer", ren_vptr); + vft_once = true; + } +#endif +#ifdef _WIN32 + const bool ok = looks_like_vftable(fe_vptr) && + looks_like_vftable(be_vptr) && + looks_like_vftable(ren_vptr); +#else + const bool ok = fe_vptr && be_vptr && ren_vptr; +#endif + if (!ok) { + if (vptr_guard_mode == GUARD_STRICT) { + LOGE("[GLGuard] vtable sanity failed: frontend=" + << fe_vptr << " backend=" << be_vptr + << " renderer=" << ren_vptr << "; skip render"); + return; // 厳格モードのみスキップ + } else { + LOGE("[GLGuard] vtable sanity failed: frontend=" + << fe_vptr << " backend=" << be_vptr + << " renderer=" << ren_vptr << "; warn only"); + } + } + } +#endif + + // Clear any pre-existing GL error to avoid tripping our check + while (glGetError() != 0) { + } + gl_check("before MapLibre render"); +#ifdef _MSC_VER + auto prev_seh = + _set_se_translator([](unsigned int code, _EXCEPTION_POINTERS*) { + char msg[64]; + std::snprintf(msg, sizeof(msg), "SEH 0x%08X", code); + throw std::runtime_error(msg); + }); +#endif + try { + frontend->render(); + } catch (const std::exception& e) { + LOGE("[SlintGLMapLibre] frontend->render threw: " << e.what()); + return; + } catch (...) { + LOGE("[SlintGLMapLibre] frontend->render threw unknown exception"); + return; + } +#ifdef _MSC_VER + _set_se_translator(prev_seh); +#endif + LOGI("[SlintGLMapLibre] MapLibre render returned"); + gl_check("after MapLibre render"); + if (first_render_) { + first_render_ = false; + // Flush first frame once to avoid driver hiccups at startup + glFinish(); + std::cout << "[SlintGLMapLibre] glFinish after first render" + << std::endl; + } +} + +void SlintGLMapLibre::render_to_texture(uint32_t texture, int w, int h) { +#ifdef _WIN32 + if (isolate_ctx_) { + ensure_isolated_context_created(); + if (map_hglrc_) { + request_isolated_render(texture, w, h); + return; + } + if (!map_hglrc_ || !p_wglGetCurrentContext || !p_wglGetCurrentDC || + !p_wglMakeCurrent) { + return; + } + HGLRC prevRC = p_wglGetCurrentContext(); + HDC prevDC = p_wglGetCurrentDC(); + HDC hdc = static_cast(slint_hdc_); + if (!p_wglMakeCurrent(hdc, static_cast(map_hglrc_))) + return; + + ::ensure_gl_functions_loaded(); + // (Re)create iso FBO as needed + if (iso_fbo_ == 0 || iso_w_ != w || iso_h_ != h) { + if (iso_fbo_) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &iso_fbo_); + iso_fbo_ = 0; + } + glGenFramebuffers(1, &iso_fbo_); + iso_w_ = w; + iso_h_ = h; + } + glBindFramebuffer(GL_FRAMEBUFFER, iso_fbo_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, texture, 0); + // Ensure depth-stencil attachment (MapLibre relies on depth test) + if (iso_depth_rb_ == 0) { + glGenRenderbuffers(1, &iso_depth_rb_); + } + glBindRenderbuffer(GL_RENDERBUFFER, iso_depth_rb_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, iso_depth_rb_); + // Viewport for our render area + glViewport(0, 0, w, h); + // Core profile safety: select draw buffer and bind a dummy VAO + ensure_optional_gl_functions_loaded(); + if (p_glDrawBuffers) { + const GLenum bufs[1] = {GL_COLOR_ATTACHMENT0}; + p_glDrawBuffers(1, bufs); + } + static GLuint s_dummy_vao = 0; + if (p_glGenVertexArrays && p_glBindVertexArray && s_dummy_vao == 0) { + p_glGenVertexArrays(1, &s_dummy_vao); + } + if (p_glBindVertexArray && s_dummy_vao) { + p_glBindVertexArray(s_dummy_vao); + } + glDisable(GL_SCISSOR_TEST); + + // FBO completeness check (once per size) + static int last_chk_w = 0, last_chk_h = 0; + if (last_chk_w != w || last_chk_h != h) { + GLenum st = glCheckFramebufferStatus(GL_FRAMEBUFFER); + LOGI("[Iso] FBO=" << iso_fbo_ << " status=0x" << std::hex << st + << std::dec << " size=" << w << "x" << h); + last_chk_w = w; + last_chk_h = h; + } + backend->updateFramebuffer( + iso_fbo_, + mbgl::Size{static_cast(w), static_cast(h)}); + backend->setSize( + mbgl::Size{static_cast(w), static_cast(h)}); + + // Debug clear (optional) + static bool dbg_clear = []() { + const char* v = std::getenv("MLNS_GL_DEBUG_CLEAR"); + if (!v) + return false; + std::string s(v); + for (auto& c : s) + c = (char)tolower((unsigned char)c); + return (s == "1" || s == "true" || s == "on" || s == "yes"); + }(); + if (dbg_clear) { + glClearColor(0.0f, 0.6f, 0.2f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | + GL_STENCIL_BUFFER_BIT); + } + + // Drive MapLibre updates similar to legacy path + if (run_loop) { + try { + run_loop->runOnce(); + } catch (...) { + } + } + if (map) { + map->triggerRepaint(); + } + if (run_loop) { + try { + run_loop->runOnce(); + } catch (...) { + } + } + + { +#ifdef _MSC_VER + auto prev_seh = + _set_se_translator([](unsigned int code, _EXCEPTION_POINTERS*) { + char msg[64]; + std::snprintf(msg, sizeof(msg), "SEH 0x%08X", code); + throw std::runtime_error(msg); + }); +#endif + try { + // One-time GL info log + static bool info_logged_iso = false; + if (!info_logged_iso) { + const GLubyte* ver = glGetString(GL_VERSION); + const GLubyte* ven = glGetString(GL_VENDOR); + const GLubyte* ren = glGetString(GL_RENDERER); + LOGI("[IsoGL] version=" + << (ver ? reinterpret_cast(ver) : "?") + << " vendor=" + << (ven ? reinterpret_cast(ven) : "?") + << " renderer=" + << (ren ? reinterpret_cast(ren) : "?")); + info_logged_iso = true; + } + // Sync backend assumed state and render in scoped backend + backend->updateAssumedState(); + { + // Validate vptrs again in isolated path (renderer included) +#ifdef _WIN32 + const int vptr_guard_mode = get_vptr_guard_mode(); + if (vptr_guard_mode != GUARD_OFF) { + void* fe_vptr = + frontend ? *reinterpret_cast(frontend.get()) + : nullptr; + void* be_vptr = + backend ? *reinterpret_cast(backend.get()) + : nullptr; + mbgl::Renderer* ren_ptr = + frontend ? frontend->getRenderer() : nullptr; + void* ren_vptr = + ren_ptr ? *reinterpret_cast(ren_ptr) + : nullptr; +#ifdef _WIN32 + const bool ok = looks_like_vftable(fe_vptr) && + looks_like_vftable(be_vptr) && + looks_like_vftable(ren_vptr); +#else + const bool ok = fe_vptr && be_vptr && ren_vptr; +#endif + if (!ok) { +#ifdef _WIN32 + log_vftable("iso.frontend", fe_vptr); + log_vftable("iso.backend", be_vptr); + log_vftable("iso.renderer", ren_vptr); +#endif + if (vptr_guard_mode == GUARD_STRICT) { + LOGE("[IsoGLGuard] vtable sanity failed: " + "frontend=" + << fe_vptr << " backend=" << be_vptr + << " renderer=" << ren_vptr + << "; skip render"); + goto after_render_scope; // 厳格モードのみスキップ + } else { + LOGE("[IsoGLGuard] vtable sanity failed: " + "frontend=" + << fe_vptr << " backend=" << be_vptr + << " renderer=" << ren_vptr + << "; warn only"); + } + } + } +#endif + mbgl::gfx::BackendScope guard{ + *backend, mbgl::gfx::BackendScope::ScopeType::Implicit}; + try { + frontend->render(); + } catch (const std::exception& e) { + LOGE( + "[IsoRender] frontend->render threw: " << e.what()); + goto after_render_scope; + } catch (...) { + LOGE("[IsoRender] frontend->render threw unknown " + "exception"); + goto after_render_scope; + } + } + after_render_scope: + glFlush(); + } catch (const std::exception& e) { + LOGE("[IsoRender] frontend->render threw: " << e.what()); + } catch (...) { + LOGE("[IsoRender] frontend->render threw unknown exception"); + } +#ifdef _MSC_VER + _set_se_translator(prev_seh); +#endif + } + // Restore Slint context + p_wglMakeCurrent(prevDC, prevRC); + return; + } +#endif + // Fallback: bind a temporary FBO in the current context and attach texture + GLuint tempFbo = 0; + glGenFramebuffers(1, &tempFbo); + glBindFramebuffer(GL_FRAMEBUFFER, tempFbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + texture, 0); + render_to_fbo(tempFbo, w, h); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &tempFbo); +} + +#ifdef _WIN32 +void SlintGLMapLibre::ensure_isolated_context_created() { + if (map_hglrc_) + return; + if (!p_wglGetCurrentContext || !p_wglGetCurrentDC || !p_wglCreateContext || + !p_wglShareLists || !p_wglMakeCurrent) + return; + slint_hglrc_ = p_wglGetCurrentContext(); + slint_hdc_ = p_wglGetCurrentDC(); + if (!slint_hglrc_ || !slint_hdc_) + return; + if (!iso_thread_.joinable()) { + iso_stop_ = false; + iso_ready_ = false; + iso_thread_ = + std::thread([this]() { this->isolated_render_thread_main(); }); + // wait up to 1s for ready + std::unique_lock lk(iso_mu_); + iso_cv_.wait_for(lk, std::chrono::milliseconds(1000), + [this] { return iso_ready_; }); + } +} + +void SlintGLMapLibre::request_isolated_render(uint32_t tex, int w, int h) { + std::lock_guard lk(iso_mu_); + iso_req_.tex = tex; + iso_req_.w = w; + iso_req_.h = h; + iso_req_.seq++; + iso_cv_.notify_one(); +} + +void SlintGLMapLibre::isolated_render_thread_main() { + if (!p_wglMakeCurrent || !p_wglCreateContext || !p_wglShareLists) + return; + // Create hidden window and its HDC for this thread + WNDCLASSW wc{}; + wc.lpfnWndProc = DefWindowProcW; + wc.hInstance = GetModuleHandleW(nullptr); + wc.lpszClassName = L"MLNS_IsoWin"; + RegisterClassW(&wc); + HWND hwnd = CreateWindowExW( + 0, wc.lpszClassName, L"mlns_iso", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + CW_USEDEFAULT, 16, 16, nullptr, nullptr, wc.hInstance, nullptr); + if (!hwnd) { + LOGE("[IsoInit] CreateWindowExW failed"); + return; + } + HDC hdc = GetDC(hwnd); + if (!hdc) { + LOGE("[IsoInit] GetDC failed"); + return; + } + PIXELFORMATDESCRIPTOR pfd{}; + pfd.nSize = sizeof(pfd); + pfd.nVersion = 1; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.cColorBits = 24; + pfd.cDepthBits = 24; + pfd.cStencilBits = 8; + int pf = ChoosePixelFormat(hdc, &pfd); + if (pf == 0 || !SetPixelFormat(hdc, pf, &pfd)) { + ReleaseDC(hwnd, hdc); + DestroyWindow(hwnd); + LOGE("[IsoInit] SetPixelFormat failed"); + return; + } + // Create RC and share with UI RC + HGLRC rc = p_wglCreateContext(hdc); + if (!rc) { + ReleaseDC(hwnd, hdc); + DestroyWindow(hwnd); + LOGE("[IsoInit] wglCreateContext failed"); + return; + } + if (!p_wglShareLists(static_cast(slint_hglrc_), rc)) { + p_wglDeleteContext(rc); + ReleaseDC(hwnd, hdc); + DestroyWindow(hwnd); + LOGE("[IsoInit] wglShareLists failed"); + return; + } + if (!p_wglMakeCurrent(hdc, rc)) { + p_wglDeleteContext(rc); + ReleaseDC(hwnd, hdc); + DestroyWindow(hwnd); + LOGE("[IsoInit] wglMakeCurrent failed"); + return; + } + { + std::lock_guard lk(iso_mu_); + iso_hwnd_ = hwnd; + iso_hdc_ = hdc; + map_hglrc_ = rc; + iso_ready_ = true; + } + iso_cv_.notify_all(); + ::ensure_gl_functions_loaded(); + ensure_optional_gl_functions_loaded(); + LOGI("[IsoInit] ready hwnd=" << hwnd << " hdc=" << hdc << " rc=" << rc); + while (true) { + IsoReq req; + { + std::unique_lock lk(iso_mu_); + iso_cv_.wait(lk, [this] { + return iso_stop_ || iso_done_seq_ != iso_req_.seq; + }); + if (iso_stop_) + break; + req = iso_req_; + } + LOGI("[IsoRender] begin seq=" << req.seq << " tex=" << req.tex + << " size=" << req.w << "x" << req.h); + // Prepare FBO + if (iso_fbo_ == 0 || iso_w_ != req.w || iso_h_ != req.h) { + if (iso_fbo_) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &iso_fbo_); + iso_fbo_ = 0; + } + glGenFramebuffers(1, &iso_fbo_); + iso_w_ = req.w; + iso_h_ = req.h; + } + glBindFramebuffer(GL_FRAMEBUFFER, iso_fbo_); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, req.tex, 0); + if (iso_depth_rb_ == 0) + glGenRenderbuffers(1, &iso_depth_rb_); + glBindRenderbuffer(GL_RENDERBUFFER, iso_depth_rb_); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, req.w, + req.h); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, + GL_RENDERBUFFER, iso_depth_rb_); + glViewport(0, 0, req.w, req.h); + if (p_glDrawBuffers) { + const GLenum bufs[1] = {GL_COLOR_ATTACHMENT0}; + p_glDrawBuffers(1, bufs); + } + static GLuint s_dummy_vao = 0; + if (p_glGenVertexArrays && p_glBindVertexArray && s_dummy_vao == 0) + p_glGenVertexArrays(1, &s_dummy_vao); + if (p_glBindVertexArray && s_dummy_vao) + p_glBindVertexArray(s_dummy_vao); + + // Update backend state + backend->updateFramebuffer(iso_fbo_, + mbgl::Size{static_cast(req.w), + static_cast(req.h)}); + backend->setSize(mbgl::Size{static_cast(req.w), + static_cast(req.h)}); + + // Drive MapLibre once and render + if (run_loop) { + try { + run_loop->runOnce(); + } catch (...) { + } + } + if (map) { + map->triggerRepaint(); + } + if (run_loop) { + try { + run_loop->runOnce(); + } catch (...) { + } + } + try { + mbgl::gfx::BackendScope guard{ + *backend, mbgl::gfx::BackendScope::ScopeType::Implicit}; + frontend->render(); + } catch (...) { + // skip frame + } + glFlush(); + { + std::lock_guard lk(iso_mu_); + iso_done_seq_ = req.seq; + } + LOGI("[IsoRender] end seq=" << req.seq); + } + p_wglMakeCurrent(nullptr, nullptr); + if (map_hglrc_) { + p_wglDeleteContext(static_cast(map_hglrc_)); + map_hglrc_ = nullptr; + } + if (iso_hdc_ && iso_hwnd_) { + ReleaseDC(static_cast(iso_hwnd_), static_cast(iso_hdc_)); + iso_hdc_ = nullptr; + } + if (iso_hwnd_) { + DestroyWindow(static_cast(iso_hwnd_)); + iso_hwnd_ = nullptr; + } +} +#endif + +void SlintGLMapLibre::setStyleUrl(const std::string& url) { + if (map) + map->getStyle().loadURL(url); +} + +void SlintGLMapLibre::handle_mouse_press(float x, float y) { + last_pos = {x, y}; +} + +void SlintGLMapLibre::handle_mouse_move(float x, float y, bool pressed) { + if (pressed && map) { + mbgl::Point current_pos = {x, y}; + map->moveBy(current_pos - last_pos); + last_pos = current_pos; + } +} + +void SlintGLMapLibre::handle_double_click(float x, float y, bool shift) { + if (!map) + return; + const mbgl::LatLng ll = map->latLngForPixel(mbgl::ScreenCoordinate{x, y}); + const auto cam = map->getCameraOptions(); + const double currentZoom = cam.zoom.value_or(0.0); + const double delta = shift ? -1.0 : 1.0; + const double targetZoom = + std::min(max_zoom, std::max(min_zoom, currentZoom + delta)); + mbgl::CameraOptions next; + next.withCenter(std::optional(ll)); + next.withZoom(std::optional(targetZoom)); + map->jumpTo(next); +} + +void SlintGLMapLibre::handle_wheel_zoom(float x, float y, float dy) { + if (!map) + return; + constexpr double step = 1.2; + // Make scroll-up zoom in (align with Metal path/user expectation) + double scale = (dy > 0.0) ? step : (1.0 / step); + map->scaleBy(scale, mbgl::ScreenCoordinate{x, y}); +} + +void SlintGLMapLibre::fly_to(const std::string& location) { + if (!map) + return; + mbgl::LatLng target; + if (location == "paris") + target = {48.8566, 2.3522}; + else if (location == "new_york") + target = {40.7128, -74.0060}; + else + target = {35.6895, 139.6917}; + mbgl::CameraOptions next; + next.center = target; + next.zoom = 10.0; + map->jumpTo(next); +} + +} // namespace mlns +// Ultra-safe mode stubs (Windows) +// Windows helpers: memory protection checks for pointers +static bool is_executable_ptr(const void* p) { +#ifdef _WIN32 + if (!p) + return false; + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(p, &mbi, sizeof(mbi)) != sizeof(mbi)) + return false; + const DWORD prot = mbi.Protect & 0xFF; + switch (prot) { + case PAGE_EXECUTE: + case PAGE_EXECUTE_READ: + case PAGE_EXECUTE_READWRITE: + case PAGE_EXECUTE_WRITECOPY: + return true; + default: + return false; + } +#else + (void)p; + return true; +#endif +} + +#ifdef _WIN32 +static bool is_readable_ptr(const void* p) { + if (!p) + return false; + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(p, &mbi, sizeof(mbi)) != sizeof(mbi)) + return false; + const DWORD prot = mbi.Protect & 0xFF; + switch (prot) { + case PAGE_READONLY: + case PAGE_READWRITE: + case PAGE_WRITECOPY: + case PAGE_EXECUTE_READ: + case PAGE_EXECUTE_READWRITE: + case PAGE_EXECUTE_WRITECOPY: + return true; + default: + return false; + } +} + +// Heuristic: Validate vtable by checking first few entries are executable +static bool looks_like_vftable(const void* vft_ptr) { + if (!is_readable_ptr(vft_ptr)) + return false; + const void* const* vfptr_table = + reinterpret_cast(vft_ptr); + for (int i = 0; i < 4; ++i) { + const void* entry = nullptr; +#if defined(_MSC_VER) + __try { + entry = vfptr_table[i]; + } __except (EXCEPTION_EXECUTE_HANDLER) { + return false; + } +#else + entry = vfptr_table[i]; +#endif + if (!is_executable_ptr(entry)) + return false; + } + return true; +} +#endif + +#ifdef _WIN32 +static void log_vftable(const char* tag, const void* vft_ptr) { + if (!vft_ptr) { + std::cout << "[VFT] " << tag << " vptr=null" << std::endl; + return; + } + const void* const* table = reinterpret_cast(vft_ptr); + const void* e0 = nullptr; + const void* e1 = nullptr; + __try { + e0 = table[0]; + e1 = table[1]; + } __except (EXCEPTION_EXECUTE_HANDLER) { + std::cout << "[VFT] " << tag << " vptr=" << vft_ptr + << " entries=ACCESS_VIOLATION" << std::endl; + return; + } + std::cout << "[VFT] " << tag << " vptr=" << vft_ptr << " e0=" << e0 + << " e1=" << e1 << std::endl; +} +#endif +static const GLubyte* APIENTRY stub_glGetStringi(GLenum, GLuint) { + static const GLubyte empty[] = ""; + return empty; // report no extensions +} +static void APIENTRY stub_glBindFragDataLocation(GLuint, GLuint, + const GLchar*) { + // no-op +} +static void* APIENTRY stub_glMapBufferRange(GLenum, GLintptr, GLsizeiptr, + GLbitfield) { + return nullptr; // force fallback path +} +static void APIENTRY stub_glFlushMappedBufferRange(GLenum, GLintptr, + GLsizeiptr) { + // no-op +} +static void APIENTRY stub_glInvalidateFramebuffer(GLenum, GLsizei, + const GLenum*) { + // no-op +} +static void APIENTRY stub_glInvalidateSubFramebuffer(GLenum, GLsizei, + const GLenum*, GLint, + GLint, GLsizei, GLsizei) { + // no-op +} +// (no forward declarations needed; static stubs are defined above) diff --git a/src/gl/slint_gl_maplibre.hpp b/src/gl/slint_gl_maplibre.hpp new file mode 100644 index 0000000..e5413d8 --- /dev/null +++ b/src/gl/slint_gl_maplibre.hpp @@ -0,0 +1,273 @@ +// OpenGL zero-copy scaffold for Slint integration +#pragma once + +#include +#include +#include +#include +#include + +// Cross‑platform minimal GL includes (no loader). We only need basic types +// and a handful of functions; the final link is provided by OpenGL. +#if defined(__APPLE__) +#include +#include +#else +#ifdef _WIN32 +#include +#endif +#include +#endif + +namespace mlns { + +// Simple RAII holder for a single‑sample color texture attached to an FBO. +struct GLRenderTarget { + GLuint fbo = 0; + GLuint color_tex = 0; + GLuint depth_stencil_rb = 0; + int width = 0; + int height = 0; + bool valid() const { + return fbo != 0 && color_tex != 0 && width > 0 && height > 0; + } +}; + +// Minimal OpenGL zero‑copy pipeline manager. +// Responsibilities: +// - Create/resize a GL texture + FBO in Slint's GL context +// - Expose a borrowed Slint Image referencing the texture (BottomLeft origin) +// - Install a rendering notifier on the Slint window to drive per‑frame hooks +class SlintZeroCopyGL final { +public: + using RenderHook = + std::function; // caller renders into fbo + + SlintZeroCopyGL() = default; + ~SlintZeroCopyGL(); + + // Attach to a Slint window and install a rendering notifier. The provided + // render hook is invoked every frame with a bound FBO of the current size. + void attach(slint::Window& window, RenderHook on_before_render); + + // Returns the borrowed image. Recreated only on (re)size. + std::optional borrowed_image() const { + return borrowed_; + } + + // Notify client when a new borrowed texture becomes available (created or + // resized). Callback is invoked from Slint's rendering notifier on the UI + // thread while the GL context is current. + void set_on_ready(std::function cb) { + on_ready_ = std::move(cb); + } + + // Explicitly set the desired render target size (map image area) + void set_surface_size(int w, int h) { + desired_w_ = w; + desired_h_ = h; + resources_ready_ = false; // force (re)create on next frame + } + + // Force teardown now (otherwise handled by notifier). + void teardown(); + +private: + void setup_if_needed(int w, int h); + void resize(int w, int h); + + GLRenderTarget rt_{}; + std::optional borrowed_{}; + RenderHook on_before_render_{}; + bool attached_ = false; + std::function on_ready_{}; + + // Defer creation until first AfterRendering to avoid touching GL before + // Slint's femtovg renderer is fully initialized. + bool resources_ready_ = false; + int pending_w_ = 0; + int pending_h_ = 0; + int warmup_remaining_ = 0; // 初期化/リサイズ直後の描画見送りフレーム数 + uint64_t frame_counter_ = 0; + bool in_render_ = false; // guard against re-entrancy + + // Desired surface size (map image area). If zero, fall back to window size. + int desired_w_ = 0; + int desired_h_ = 0; + + // Window/mapサイズの安定化判定 + int last_w_ = 0; + int last_h_ = 0; + int size_stable_count_ = 0; + static constexpr int SIZE_STABLE_REQUIRED_ = 2; + + enum class RenderStage { Before, After }; + RenderStage stage_ = + RenderStage::After; // default: render in AfterRendering +}; + +} // namespace mlns + +// ---- MapLibre OpenGL Backend + Frontend + Controller (zero-copy) ---- +// These are placed in the same header for simplicity of this phase. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlns { + +class SlintGLBackend; + +class SlintRendererFrontend : public mbgl::RendererFrontend { +public: + SlintRendererFrontend(std::unique_ptr renderer, + mbgl::gfx::RendererBackend& backend); + ~SlintRendererFrontend() override; + + void reset() override; + void setObserver(mbgl::RendererObserver&) override; + void update(std::shared_ptr) override; + const mbgl::TaggedScheduler& getThreadPool() const override; + void render(); + + mbgl::Renderer* getRenderer(); + +private: + mbgl::gfx::RendererBackend& backend; + std::unique_ptr renderer; + std::shared_ptr updateParameters; +}; + +class SlintGLBackend final : public mbgl::gl::RendererBackend, + public mbgl::gfx::Renderable { +public: + explicit SlintGLBackend(mbgl::gfx::ContextMode mode); + ~SlintGLBackend() override; + + mbgl::gfx::Renderable& getDefaultRenderable() override { + return *this; + } + + void activate() override { + updateAssumedState(); + } + void deactivate() override { /* no-op */ + } + + mbgl::gl::ProcAddress getExtensionFunctionPointer( + const char* name) override; + void updateAssumedState() override; + + void updateFramebuffer(uint32_t fbo, const mbgl::Size& newSize); + void setSize(const mbgl::Size& newSize) { + size = newSize; + } + uint32_t currentFBO() const { + return fbo_; + } + +private: + uint32_t fbo_ = 0; +}; + +class SlintGLMapLibre : public mbgl::MapObserver { +public: + SlintGLMapLibre(); + ~SlintGLMapLibre(); + + void initialize(int width, int height); + void resize(int width, int height); + void run_map_loop(); + // Legacy path: render directly to an FBO in the current context. + void render_to_fbo(uint32_t fbo, int width, int height); + // Isolated path: enqueue render into a shared texture using a dedicated GL + // context. + void render_to_texture(uint32_t texture, int width, int height); + + void setStyleUrl(const std::string& url); + void handle_mouse_press(float x, float y); + void handle_mouse_move(float x, float y, bool pressed); + void handle_mouse_release(float, float) { + } + void handle_double_click(float x, float y, bool shift); + void handle_wheel_zoom(float x, float y, float dy); + void fly_to(const std::string& location); + + // MapObserver (log basic lifecycle) + void onWillStartLoadingMap() override { + std::cout << "[MapObserver] will start loading map" << std::endl; + } + void onDidFinishLoadingStyle() override { + std::cout << "[MapObserver] did finish loading style" << std::endl; + } + void onDidBecomeIdle() override { + std::cout << "[MapObserver] did become idle" << std::endl; + } + void onDidFailLoadingMap(mbgl::MapLoadError err, + const std::string& msg) override { + std::cout << "[MapObserver] did fail loading map: err=" << (int)err + << " msg=" << msg << std::endl; + } + +private: + std::unique_ptr run_loop; + std::unique_ptr backend; + std::unique_ptr frontend; + std::unique_ptr map; + std::string style_url_{}; + bool thread_owns_map_ = false; + + int width = 0; + int height = 0; + mbgl::Point last_pos; + double min_zoom = 0.0; + double max_zoom = 22.0; + + bool first_render_ = true; + +#ifdef _WIN32 + // Context isolation (Windows/WGL) + // ------------------------------------------------- + bool isolate_ctx_ = false; // MLNS_GL_ISOLATE_CONTEXT + void ensure_isolated_context_created(); + void isolated_render_thread_main(); + void request_isolated_render(uint32_t texture, int w, int h); + + void* slint_hdc_ = nullptr; // HDC (from wglGetCurrentDC) + void* slint_hglrc_ = nullptr; // HGLRC (from wglGetCurrentContext) + void* map_hglrc_ = nullptr; // isolated HGLRC (dedicated thread) + void* iso_hdc_ = nullptr; // isolated HDC (hidden window) + void* iso_hwnd_ = nullptr; // hidden HWND + // isolated uses its own FBO to attach the shared texture + uint32_t iso_fbo_ = 0; + uint32_t iso_depth_rb_ = 0; + int iso_w_ = 0; + int iso_h_ = 0; + + std::thread iso_thread_; + std::mutex iso_mu_; + std::condition_variable iso_cv_; + bool iso_stop_ = false; + bool iso_ready_ = false; // thread created HDC/HGLRC and loader + struct IsoReq { + uint32_t tex = 0; + int w = 0; + int h = 0; + uint64_t seq = 0; + } iso_req_{}; + uint64_t iso_done_seq_ = 0; + uint64_t iso_last_sent_seq_ = 0; + int iso_miss_count_ = 0; +#endif +}; + +} // namespace mlns diff --git a/src/gl/slint_gl_renderer_backend.cpp b/src/gl/slint_gl_renderer_backend.cpp new file mode 100644 index 0000000..ab20050 --- /dev/null +++ b/src/gl/slint_gl_renderer_backend.cpp @@ -0,0 +1,309 @@ +// Windows OpenGL backend and renderer frontend split from slint_maplibre_gl.cpp +#include "gl/slint_gl_maplibre.hpp" + +#ifdef _WIN32 +#include +#include +// Legacy Windows GL headers may miss some enums; provide fallbacks. +#ifndef APIENTRYP +#ifndef APIENTRY +#define APIENTRY __stdcall +#endif +#define APIENTRYP APIENTRY* +#endif +#ifndef GL_FRAMEBUFFER +#define GL_FRAMEBUFFER 0x8D40 +#endif +#ifndef GL_FRAMEBUFFER_BINDING +#define GL_FRAMEBUFFER_BINDING 0x8CA6 +#endif +#endif + +#include +#include + +using namespace std; + +namespace mlns { + +#ifdef _WIN32 +// Minimal loader for GL/ES symbols with ANGLE/EGL fallback +static FARPROC load_gl_proc(const char* name) { + using PFNWGLGETCURRENTCONTEXTPROC = HGLRC(WINAPI*)(); + static PFNWGLGETCURRENTCONTEXTPROC p_wglGetCurrentContext = nullptr; + if (!p_wglGetCurrentContext) { + HMODULE gl = GetModuleHandleA("opengl32.dll"); + if (gl) { + p_wglGetCurrentContext = + reinterpret_cast( + GetProcAddress(gl, "wglGetCurrentContext")); + } + } + const bool have_wgl = + p_wglGetCurrentContext && p_wglGetCurrentContext() != nullptr; + auto bad_ptr = [](FARPROC p) { + return p == reinterpret_cast(1) || + p == reinterpret_cast(2) || + p == reinterpret_cast(3) || + p == reinterpret_cast(-1); + }; + if (have_wgl) { + HMODULE lib = GetModuleHandleA("opengl32.dll"); + FARPROC p = nullptr; + if (lib) { + using PFNWGLGETPROCADDRESSPROC = PROC(WINAPI*)(LPCSTR); + auto wglGetProcAddress_fn = + reinterpret_cast( + GetProcAddress(lib, "wglGetProcAddress")); + if (wglGetProcAddress_fn) { + p = reinterpret_cast(wglGetProcAddress_fn(name)); + if (bad_ptr(p)) + p = nullptr; + } + if (!p) + p = GetProcAddress(lib, name); + } + return p; + } + auto env_truthy = [](const char* v) { + if (!v) + return false; + std::string s(v); + for (auto& c : s) + c = (char)tolower((unsigned char)c); + return (s == "1" || s == "true" || s == "on" || s == "yes"); + }; + if (env_truthy(std::getenv("MLNS_FORCE_DESKTOP_GL"))) { + return nullptr; + } + using PFNEGLGETPROCADDRESSPROC = void*(WINAPI*)(const char*); + static PFNEGLGETPROCADDRESSPROC p_eglGetProcAddress = nullptr; + static bool egl_loaded = false; + if (!egl_loaded) { + HMODULE egl = GetModuleHandleA("libEGL.dll"); + if (!egl) + egl = GetModuleHandleA("EGL.dll"); + if (!egl) + egl = LoadLibraryA("libEGL.dll"); + if (!egl) + egl = LoadLibraryA("EGL.dll"); + if (egl) + p_eglGetProcAddress = reinterpret_cast( + GetProcAddress(egl, "eglGetProcAddress")); + egl_loaded = true; + } + void* vp = nullptr; + HMODULE gles = GetModuleHandleA("libGLESv2.dll"); + if (!gles) + gles = GetModuleHandleA("GLESv2.dll"); + if (!gles) + gles = LoadLibraryA("libGLESv2.dll"); + if (!gles) + gles = LoadLibraryA("GLESv2.dll"); + if (gles) + vp = reinterpret_cast(GetProcAddress(gles, name)); + if (!vp && p_eglGetProcAddress) + vp = p_eglGetProcAddress(name); + return reinterpret_cast(vp); +} + +// Ultra-safe mode stubs (duplicated with internal linkage) +typedef unsigned int GLbitfield; +typedef ptrdiff_t GLintptr; +typedef ptrdiff_t GLsizeiptr; +typedef char GLchar; +static const GLubyte* APIENTRY stub_glGetStringi(GLenum, GLuint) { + static const GLubyte empty[] = ""; + return empty; +} +static void APIENTRY stub_glBindFragDataLocation(GLuint, GLuint, + const GLchar*) { +} +static void* APIENTRY stub_glMapBufferRange(GLenum, GLintptr, GLsizeiptr, + GLbitfield) { + return nullptr; +} +static void APIENTRY stub_glFlushMappedBufferRange(GLenum, GLintptr, + GLsizeiptr) { +} +static void APIENTRY stub_glInvalidateFramebuffer(GLenum, GLsizei, + const GLenum*) { +} +static void APIENTRY stub_glInvalidateSubFramebuffer(GLenum, GLsizei, + const GLenum*, GLint, + GLint, GLsizei, GLsizei) { +} +#endif + +// Renderable that ensures our FBO/viewport are bound when MapLibre draws +class SlintGLRenderableResource final : public mbgl::gfx::RenderableResource { +public: + explicit SlintGLRenderableResource(class SlintGLBackend& backend_) + : backend(backend_) { + } + void bind() override; + +private: + class SlintGLBackend& backend; +}; + +SlintGLBackend::SlintGLBackend(const mbgl::gfx::ContextMode mode) + : mbgl::gl::RendererBackend(mode), + mbgl::gfx::Renderable( + {0, 0}, std::make_unique(*this)) { +} + +SlintGLBackend::~SlintGLBackend() = default; + +void SlintGLRenderableResource::bind() { + assert(mbgl::gfx::BackendScope::exists()); + auto& b = backend; +#ifdef _WIN32 + // Bind our FBO if the function is available; legacy headers don't declare + // it. + using PFNGLBINDFRAMEBUFFERPROC = void(APIENTRYP)(GLenum, GLuint); + static PFNGLBINDFRAMEBUFFERPROC p_glBindFramebuffer = nullptr; + if (!p_glBindFramebuffer) { + p_glBindFramebuffer = reinterpret_cast( + load_gl_proc("glBindFramebuffer")); + if (!p_glBindFramebuffer) { + p_glBindFramebuffer = reinterpret_cast( + load_gl_proc("glBindFramebufferEXT")); + } + } + if (p_glBindFramebuffer) { + p_glBindFramebuffer(GL_FRAMEBUFFER, b.currentFBO()); + } +#else + glBindFramebuffer(GL_FRAMEBUFFER, b.currentFBO()); +#endif + glViewport(0, 0, static_cast(b.getSize().width), + static_cast(b.getSize().height)); + b.setFramebufferBinding(static_cast(b.currentFBO())); + b.setViewport(0, 0, b.getSize()); +} + +void SlintGLBackend::updateAssumedState() { +#ifdef _WIN32 + GLint fbo = 0; + GLint vp[4] = {0, 0, static_cast(size.width), + static_cast(size.height)}; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo); + glGetIntegerv(GL_VIEWPORT, vp); + assumeFramebufferBinding(static_cast(fbo)); + assumeViewport( + vp[0], vp[1], + {static_cast(vp[2]), static_cast(vp[3])}); +#else + assumeFramebufferBinding(ImplicitFramebufferBinding); + assumeViewport(0, 0, size); +#endif +} + +void SlintGLBackend::updateFramebuffer(uint32_t fbo, + const mbgl::Size& newSize) { + fbo_ = fbo; + size = newSize; +} + +mbgl::gl::ProcAddress SlintGLBackend::getExtensionFunctionPointer( + const char* name) { +#ifdef _WIN32 + static int safe_mode_level = []() { + const char* v = std::getenv("MLNS_GL_SAFE_MODE"); + if (!v) + return 0; + std::string s(v); + for (auto& c : s) + c = static_cast(::tolower(static_cast(c))); + if (s == "2") + return 2; + if (s == "1" || s == "true" || s == "on" || s == "yes") + return 1; + return 0; + }(); + if (safe_mode_level >= 1) { + // Level 1: Disable only debug output related APIs + static const char* deny[] = {"glDebugMessageControl", + "glDebugMessageCallback", + "glDebugMessageControlARB", + "glDebugMessageCallbackARB", + "glPushDebugGroup", + "glPopDebugGroup", + nullptr}; + for (int i = 0; deny[i]; ++i) { + if (std::strcmp(name, deny[i]) == 0) { + std::cout << "[SlintGLBackend] getProcAddress('" << name + << "') => forced null (safe mode)" << std::endl; + return nullptr; + } + } + } + if (safe_mode_level >= 2) { + if (std::strcmp(name, "glGetStringi") == 0) + return reinterpret_cast(stub_glGetStringi); + if (std::strcmp(name, "glBindFragDataLocation") == 0) + return reinterpret_cast( + stub_glBindFragDataLocation); + if (std::strcmp(name, "glInvalidateFramebuffer") == 0) + return reinterpret_cast( + stub_glInvalidateFramebuffer); + if (std::strcmp(name, "glInvalidateSubFramebuffer") == 0) + return reinterpret_cast( + stub_glInvalidateSubFramebuffer); + if (std::strcmp(name, "glMapBufferRange") == 0) + return reinterpret_cast( + stub_glMapBufferRange); + if (std::strcmp(name, "glFlushMappedBufferRange") == 0) + return reinterpret_cast( + stub_glFlushMappedBufferRange); + } + // No unconditional deny beyond safe-mode handling + auto p = load_gl_proc(name); + return reinterpret_cast(p); +#else + return nullptr; +#endif +} + +// Frontend +SlintRendererFrontend::SlintRendererFrontend( + std::unique_ptr renderer_, + mbgl::gfx::RendererBackend& backend_) + : backend(backend_), renderer(std::move(renderer_)) { +} + +SlintRendererFrontend::~SlintRendererFrontend() = default; + +void SlintRendererFrontend::reset() { + renderer.reset(); +} + +void SlintRendererFrontend::setObserver(mbgl::RendererObserver& observer) { + if (renderer) + renderer->setObserver(&observer); +} + +void SlintRendererFrontend::update( + std::shared_ptr params) { + updateParameters = std::move(params); +} + +const mbgl::TaggedScheduler& SlintRendererFrontend::getThreadPool() const { + return backend.getThreadPool(); +} + +void SlintRendererFrontend::render() { + if (!renderer || !updateParameters) + return; + mbgl::gfx::BackendScope guard{backend, + mbgl::gfx::BackendScope::ScopeType::Implicit}; + auto params = updateParameters; + renderer->render(params); +} + +mbgl::Renderer* SlintRendererFrontend::getRenderer() { + return renderer.get(); +} + +} // namespace mlns diff --git a/vcpkg.json b/vcpkg.json index 319f848..a836a3b 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -20,6 +20,7 @@ "name": "pkgconf", "host": true }, - "egl-registry" + "egl-registry", + "skia" ] }