diff --git a/.emb/base.emb.yaml b/.emb/base.emb.yaml index 85e9e45ca..3faa63e30 100644 --- a/.emb/base.emb.yaml +++ b/.emb/base.emb.yaml @@ -53,7 +53,7 @@ cross: BUILD_BACKEND_SOFTWARE: 'ON' BUILD_BACKEND_WAYLAND_EGL: 'OFF' BUILD_BACKEND_WAYLAND_VULKAN: 'OFF' - headless-egl: - BUILD_BACKEND_HEADLESS_EGL: 'ON' + headless-software: + BUILD_BACKEND_HEADLESS_SOFTWARE: 'ON' BUILD_BACKEND_WAYLAND_EGL: 'OFF' BUILD_BACKEND_WAYLAND_VULKAN: 'OFF' diff --git a/.emb/raspberry-pi.emb.yaml b/.emb/raspberry-pi.emb.yaml index 5dd9aa5d7..81386ab1c 100644 --- a/.emb/raspberry-pi.emb.yaml +++ b/.emb/raspberry-pi.emb.yaml @@ -27,7 +27,7 @@ build: cross: defines: - BUILD_BACKEND_HEADLESS_EGL: 'OFF' + BUILD_BACKEND_HEADLESS_SOFTWARE: 'OFF' DISABLE_PLUGINS: 'OFF' PLUGINS_DIR: '../ivi-homescreen-plugins/plugins' diff --git a/CMakeLists.txt b/CMakeLists.txt index b9ae8713e..6b49388b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -72,10 +72,19 @@ endif () if (BUILD_BACKEND_SOFTWARE) if (BUILD_BACKEND_DRM_KMS_EGL OR BUILD_BACKEND_DRM_KMS_VULKAN OR BUILD_BACKEND_WAYLAND_EGL OR - BUILD_BACKEND_WAYLAND_VULKAN OR BUILD_BACKEND_HEADLESS_EGL) + BUILD_BACKEND_WAYLAND_VULKAN OR BUILD_BACKEND_HEADLESS_SOFTWARE) message(FATAL_ERROR "BUILD_BACKEND_SOFTWARE is mutually exclusive with the " - "DRM/Wayland/Headless EGL/Vulkan backends — pick exactly one") + "DRM/Wayland/Headless Software/Vulkan backends — pick exactly one") + endif () +endif () +if (BUILD_BACKEND_HEADLESS_SOFTWARE) + if (BUILD_BACKEND_DRM_KMS_EGL OR BUILD_BACKEND_DRM_KMS_VULKAN OR + BUILD_BACKEND_WAYLAND_EGL OR + BUILD_BACKEND_WAYLAND_VULKAN OR BUILD_BACKEND_SOFTWARE) + message(FATAL_ERROR + "BUILD_BACKEND_HEADLESS_SOFTWARE is mutually exclusive with the " + "DRM/Wayland/Software/Vulkan backends — pick exactly one") endif () endif () diff --git a/README.md b/README.md index bfdc75db4..f96abcf78 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ All other parameters get assigned using the following ordering: `BUILD_BACKEND_WAYLAND_VULKAN` - Build Backed for Vulkan. Defaults to OFF -`BUILD_BACKEND_HEADLESS_EGL` - Build Headless backend for EGL (OSMesa). Defaults to OFF +`BUILD_BACKEND_HEADLESS_SOFTWARE` - Build headless backend using the kSoftware CPU renderer (no EGL or GPU required). Defaults to OFF `BUILD_COMPOSITOR` - Enable the `FlutterCompositor` backing-store API so platform-view layers can be interleaved with Flutter UI. See [Platform View Plugins](#platform-view-plugins). Defaults to OFF. diff --git a/cmake/config_common.h.in b/cmake/config_common.h.in index 6e0dac277..fdb2a03a8 100644 --- a/cmake/config_common.h.in +++ b/cmake/config_common.h.in @@ -39,8 +39,8 @@ #ifndef BUILD_BACKEND_DRM_KMS_VULKAN #cmakedefine01 BUILD_BACKEND_DRM_KMS_VULKAN #endif -#ifndef BUILD_BACKEND_HEADLESS_EGL -#cmakedefine01 BUILD_BACKEND_HEADLESS_EGL +#ifndef BUILD_BACKEND_HEADLESS_SOFTWARE +#cmakedefine01 BUILD_BACKEND_HEADLESS_SOFTWARE #endif #ifndef BUILD_BACKEND_SOFTWARE #cmakedefine01 BUILD_BACKEND_SOFTWARE @@ -58,8 +58,7 @@ // EGL is needed only by the EGL-based backends. Including here // unconditionally dragged EGL/GL headers into the DRM-Vulkan and software // builds, which must stay EGL/GL-free. -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_DRM_KMS_EGL || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_DRM_KMS_EGL #define IVI_HAVE_EGL 1 #include #else @@ -73,8 +72,7 @@ // libwayland-cursor runtime deps. This condition MUST match the waypp build // gate in third_party/CMakeLists.txt: waypp's own sources include this header // (via logging.h), so a mismatch breaks waypp's build. -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN #include #endif diff --git a/cmake/options.cmake b/cmake/options.cmake index 1e1301577..6717026d0 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -137,12 +137,17 @@ if (USE_DRM_SCENE AND NOT (BUILD_BACKEND_DRM_KMS_EGL OR BUILD_BACKEND_DRM_KMS_VU endif () # -# Headless +# Headless (software / CPU renderer — no GPU or Wayland) # -option(BUILD_BACKEND_HEADLESS_EGL "Build Headless EGL Backend" OFF) -if (BUILD_BACKEND_HEADLESS_EGL) - find_package(PkgConfig) - pkg_check_modules(OSMESA osmesa glesv2 egl IMPORTED_TARGET REQUIRED) +option(BUILD_BACKEND_HEADLESS_SOFTWARE + "Build headless backend using kSoftware renderer — no GPU or Wayland required" + OFF) +if (BUILD_BACKEND_HEADLESS_SOFTWARE) + # Force all hardware sinks and libinput off: headless has no display + # device to scan out to and no /dev/input/event* to listen on. + set(BUILD_SOFTWARE_SINK_DRM OFF CACHE BOOL "" FORCE) + set(BUILD_SOFTWARE_SINK_FBDEV OFF CACHE BOOL "" FORCE) + set(BUILD_SOFTWARE_INPUT_LIBINPUT OFF CACHE BOOL "" FORCE) endif () # diff --git a/docs/COMPOSITOR_GOLDENS.md b/docs/COMPOSITOR_GOLDENS.md index 296838368..214bf5b67 100644 --- a/docs/COMPOSITOR_GOLDENS.md +++ b/docs/COMPOSITOR_GOLDENS.md @@ -9,21 +9,9 @@ Both run against software rasterizers so they're CI-friendly. ## Prerequisites -### OSMesa (headless OpenGL) +### Headless software backend -Required for any EGL-side golden. The host's existing `BUILD_BACKEND_HEADLESS_EGL` already targets OSMesa. - -Fedora: - -```bash -sudo dnf install mesa-compat-libOSMesa mesa-compat-libOSMesa-devel -``` - -Ubuntu/Debian: - -```bash -sudo apt install libosmesa6 libosmesa6-dev -``` +`BUILD_BACKEND_HEADLESS_SOFTWARE` uses Flutter's `kSoftware` CPU renderer — no EGL or GPU required. No additional packages are needed beyond a standard build environment. ### lavapipe (headless Vulkan) @@ -73,7 +61,7 @@ Configure with both flags on, point at the bundle: ```bash cmake -S . -B build-goldens \ -DBUILD_UNIT_TESTS=ON \ - -DBUILD_BACKEND_HEADLESS_EGL=ON \ + -DBUILD_BACKEND_HEADLESS_SOFTWARE=ON \ -DBUILD_COMPOSITOR=ON \ -DUNIT_TEST_APP_BUNDLE=/path/to/flutter/build/linux/x64/release/bundle \ -DBUILD_NUMBER=1 @@ -114,11 +102,10 @@ VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json \ homescreen -b "$BUNDLE" --window-type=BG ``` -There is no native headless-Vulkan backend in this repo today (the existing `HeadlessBackend` is OSMesa/EGL only). Adding one would mean a fourth backend implementation along the lines of `WaylandVulkanBackend` but using `VK_EXT_headless_surface` + a memory-image swapchain — substantial, not on this branch. Until then, Vulkan goldens go through the weston-headless pairing above. +There is no native headless-Vulkan backend in this repo today (the existing `HeadlessBackend` is Software-only). Adding one would mean a fourth backend implementation along the lines of `WaylandVulkanBackend` but using `VK_EXT_headless_surface` + a memory-image swapchain — substantial, not on this branch. Until then, Vulkan goldens go through the weston-headless pairing above. ## What's deferred -- **HeadlessBackend compositor wiring.** `HeadlessBackend::GetCompositorConfig()` returns null callbacks today. `BUILD_COMPOSITOR=ON` with the headless backend currently falls back to direct rendering — i.e. the parity test verifies "compositor build doesn't break Flutter-only frames" but doesn't yet exercise the create/collect/present callbacks. Wiring requires either porting `GlCompositor` / `EglFboBackingStore` to desktop GL headers (OSMesa is desktop GL, not GLES) or templating those primitives over the GL header set. - **Native headless Vulkan backend.** See above. - **Multi-layer goldens.** Need a Dart bundle that emits a `PlatformViewLayer`; the renderer will then exercise `WaylandEglBackend::PresentLayers`'s general path and the new GL-texture composite step from the plugin migrations. diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 35c65c7ab..d881aefa7 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -325,15 +325,6 @@ key_event_handler.h :private-members: :undoc-members: -osmesa.h --------- - -.. doxygenclass:: OSMesaHeadless - :project: ivi-homescreen - :members: - :private-members: - :undoc-members: - display.cc ---------- diff --git a/scripts/build_beagleplay.sh b/scripts/build_beagleplay.sh index 82aff700f..e922f79c7 100755 --- a/scripts/build_beagleplay.sh +++ b/scripts/build_beagleplay.sh @@ -653,7 +653,6 @@ phase4_build() { -S "$REPO_DIR" -B "$BUILD_DIR" -DCMAKE_TOOLCHAIN_FILE="$BUILD_DIR/.xc-toolchain.cmake" -DCMAKE_BUILD_TYPE=Release - -DBUILD_BACKEND_HEADLESS_EGL=OFF -DBUILD_BACKEND_DRM_KMS_VULKAN=OFF ) case "$be" in diff --git a/scripts/build_nitrogen8mm.sh b/scripts/build_nitrogen8mm.sh index 0dfed2eba..ad95cee7d 100755 --- a/scripts/build_nitrogen8mm.sh +++ b/scripts/build_nitrogen8mm.sh @@ -286,7 +286,6 @@ phase4_build() { -DCMAKE_BUILD_TYPE=Release -DBUILD_BACKEND_WAYLAND_EGL=OFF -DBUILD_BACKEND_WAYLAND_VULKAN=OFF -DBUILD_BACKEND_DRM_KMS_EGL=ON -DBUILD_BACKEND_SOFTWARE=OFF - -DBUILD_BACKEND_HEADLESS_EGL=OFF ) [[ "$WITH_SCENE" -eq 1 ]] && args+=(-DBUILD_COMPOSITOR=ON -DUSE_DRM_SCENE=ON) if [[ "$NO_PLUGINS" -eq 1 ]]; then diff --git a/scripts/build_radxa_zero3.sh b/scripts/build_radxa_zero3.sh index 827340370..198d12c4a 100755 --- a/scripts/build_radxa_zero3.sh +++ b/scripts/build_radxa_zero3.sh @@ -908,7 +908,6 @@ phase4_build() { -S "$REPO_DIR" -B "$BUILD_DIR" -DCMAKE_TOOLCHAIN_FILE="$BUILD_DIR/.xc-toolchain.cmake" -DCMAKE_BUILD_TYPE=Release - -DBUILD_BACKEND_HEADLESS_EGL=OFF -DBUILD_BACKEND_DRM_KMS_VULKAN=OFF ) case "$be" in diff --git a/scripts/build_unoq.sh b/scripts/build_unoq.sh index e357b6d1e..eb0524e77 100755 --- a/scripts/build_unoq.sh +++ b/scripts/build_unoq.sh @@ -890,7 +890,6 @@ phase5_build() { -S "$REPO_DIR" -B "$BUILD_DIR" -DCMAKE_TOOLCHAIN_FILE="$BUILD_DIR/.xc-toolchain.cmake" -DCMAKE_BUILD_TYPE=Release - -DBUILD_BACKEND_HEADLESS_EGL=OFF ) case "$be" in wayland-egl) diff --git a/scripts/clang_tidy.sh b/scripts/clang_tidy.sh index 48dd6a0d5..4f1e8863e 100755 --- a/scripts/clang_tidy.sh +++ b/scripts/clang_tidy.sh @@ -40,7 +40,7 @@ echo "Using: $(command -v "${CLANG_TIDY}")" # Only lint files actually compiled in this build configuration (those # present in compile_commands.json). Files belonging to optional backends -# (osmesa, vulkan, dlt, ...) or plugin-only sources are skipped when the +# (software, vulkan, dlt, ...) or plugin-only sources are skipped when the # corresponding feature is off, avoiding spurious errors from unavailable # headers and mis-resolved compilation flags. # diff --git a/shell/CMakeLists.txt b/shell/CMakeLists.txt index f02908c09..9881c078c 100644 --- a/shell/CMakeLists.txt +++ b/shell/CMakeLists.txt @@ -46,8 +46,7 @@ endif () # #if-gated on a Wayland backend. Skipping these for non-Wayland # binaries drops the libwayland-client + libwayland-cursor link/runtime # deps; drm-kms-egl and software builds become truly Wayland-free. -if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN OR - BUILD_BACKEND_HEADLESS_EGL) +if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN) target_sources(${PROJECT_NAME} PRIVATE wayland/display.cc wayland/window.cc @@ -83,22 +82,22 @@ if (BUILD_BACKEND_WAYLAND_EGL) endif () endif () -if (BUILD_BACKEND_HEADLESS_EGL) - target_sources(${PROJECT_NAME} PRIVATE - backend/headless/headless.cc - backend/headless/osmesa.cc - backend/gl_process_resolver.cc - ) -endif () - -if (BUILD_BACKEND_SOFTWARE) +if (BUILD_BACKEND_HEADLESS_SOFTWARE OR BUILD_BACKEND_SOFTWARE) target_sources(${PROJECT_NAME} PRIVATE backend/software/file_sink.cc backend/software/sink_factory.cc backend/software/software_backend.cc backend/software/software_cursor.cc + ) +endif () + +if (BUILD_BACKEND_HEADLESS_SOFTWARE OR BUILD_BACKEND_SOFTWARE) + target_sources(${PROJECT_NAME} PRIVATE display/software_display.cc ) +endif () + +if (BUILD_BACKEND_SOFTWARE) if (BUILD_SOFTWARE_SINK_DRM) target_sources(${PROJECT_NAME} PRIVATE backend/software/drm_dumb_sink.cc @@ -121,10 +120,10 @@ endif () # Shared libinput keyboard core (xkb translation + key-repeat) used by every # libinput-backed seat — SoftwareSeat and (DRM) DrmSeat — so keyboard behavior # is identical across backends. Depends only on libxkbcommon, which those seats -# already link. Added once here to avoid duplicate sources in multi-backend -# builds. +# already link. Also compiled for headless software so platform_homescreen's +# key event handler can resolve xkb symbols without the Wayland/waypp stack. if (BUILD_SOFTWARE_INPUT_LIBINPUT OR BUILD_BACKEND_DRM_KMS_EGL OR - BUILD_BACKEND_DRM_KMS_VULKAN) + BUILD_BACKEND_DRM_KMS_VULKAN OR BUILD_BACKEND_HEADLESS_SOFTWARE) target_sources(${PROJECT_NAME} PRIVATE input/xkb_keyboard.cc input/key_repeater.cc @@ -316,9 +315,14 @@ endif () if (BUILD_BACKEND_WAYLAND_VULKAN) target_link_libraries(${PROJECT_NAME} PRIVATE vulkan_headers) endif () -if (BUILD_BACKEND_HEADLESS_EGL) - target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::OSMESA) +if (BUILD_BACKEND_HEADLESS_SOFTWARE) + # xkb_keyboard.cc (compiled above) calls libxkbcommon directly; link it + # explicitly since neither waypp nor SW_LIBINPUT bring it in for this path. + find_package(PkgConfig REQUIRED) + pkg_check_modules(HEADLESS_XKBCOMMON xkbcommon IMPORTED_TARGET REQUIRED) + target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::HEADLESS_XKBCOMMON) endif () + if (BUILD_CRASH_HANDLER) target_link_libraries(${PROJECT_NAME} PRIVATE sentry::sentry) endif () @@ -341,8 +345,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE # the wayland-gen protocol-header include dirs and the static library are both # needed only there. Non-Wayland binaries (DRM EGL/Vulkan, software) get # neither the headers nor the libwayland-client / libwayland-cursor deps. -if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN OR - BUILD_BACKEND_HEADLESS_EGL) +if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN) target_include_directories(${PROJECT_NAME} PRIVATE $ ) diff --git a/shell/app.cc b/shell/app.cc index 1d7be35d6..d64b67eb8 100644 --- a/shell/app.cc +++ b/shell/app.cc @@ -25,8 +25,7 @@ #include "timer.h" #include "view/flutter_view.h" -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN #include "wayland/display.h" #include "wayland/window.h" #endif @@ -34,7 +33,7 @@ #include "display/drm_display.h" #endif -#if BUILD_BACKEND_SOFTWARE +#if BUILD_BACKEND_SOFTWARE || BUILD_BACKEND_HEADLESS_SOFTWARE #include "backend/software/software_cursor.h" #include "display/software_display.h" #if BUILD_SOFTWARE_INPUT_LIBINPUT @@ -42,8 +41,9 @@ #endif #endif -#if BUILD_BACKEND_HEADLESS_EGL -#include "backend/headless/headless.h" +#if BUILD_BACKEND_HEADLESS_SOFTWARE +#include "backend/software/memory_sink.h" +#include "backend/software/software_backend.h" #endif namespace { @@ -65,7 +65,7 @@ std::shared_ptr MakeDisplay( const bool no_seat = configs[0].view.drm_no_seat.value_or(false); return std::make_shared(static_cast(w), static_cast(h), 60.0, no_seat); -#elif BUILD_BACKEND_SOFTWARE +#elif BUILD_BACKEND_SOFTWARE || BUILD_BACKEND_HEADLESS_SOFTWARE // No compositor, no Wayland, no DRM — just an IDisplay that owns // (a) a refresh-rate denominator for App::Loop and (b) an optional // libinput-backed seat. 60 Hz default; a sink with a real vblank @@ -118,9 +118,8 @@ App::App(const std::vector& configs) // usage is meaningless on DRM / software. ENABLE_AGL_SHELL_CLIENT // defaults ON in waypp's CMake regardless of backend, so combine // with a Wayland-backend gate here. -#if ENABLE_AGL_SHELL_CLIENT && \ - (BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL) +#if ENABLE_AGL_SHELL_CLIENT && \ + (BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN) bool found_view_with_bg = false; #endif @@ -136,9 +135,8 @@ App::App(const std::vector& configs) // usage is meaningless on DRM / software. ENABLE_AGL_SHELL_CLIENT // defaults ON in waypp's CMake regardless of backend, so combine // with a Wayland-backend gate here. -#if ENABLE_AGL_SHELL_CLIENT && \ - (BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL) +#if ENABLE_AGL_SHELL_CLIENT && \ + (BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN) if (WaylandWindow::get_window_type(cfg.view.window_type) == WaylandWindow::WINDOW_BG) { found_view_with_bg = true; @@ -150,9 +148,8 @@ App::App(const std::vector& configs) // usage is meaningless on DRM / software. ENABLE_AGL_SHELL_CLIENT // defaults ON in waypp's CMake regardless of backend, so combine // with a Wayland-backend gate here. -#if ENABLE_AGL_SHELL_CLIENT && \ - (BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL) +#if ENABLE_AGL_SHELL_CLIENT && \ + (BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN) // check that if we had a BG type and issue a ready() request for it, // otherwise we're going to assume that this is a NORMAL/REGULAR application. if (found_view_with_bg) @@ -233,12 +230,33 @@ int App::Loop() const { return 0; } -#if BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_HEADLESS_SOFTWARE -GLubyte* App::getViewRenderBuf(const int i) const { - return reinterpret_cast( - m_views[static_cast(i)]->GetBackend()) - ->getHeadlessBuffer(); +std::vector App::getViewRenderBuf(const int i, + size_t* row_bytes, + size_t* height) const { + auto* sw_backend = dynamic_cast( + m_views[static_cast(i)]->GetBackend()); + if (sw_backend == nullptr) { + if (row_bytes != nullptr) + *row_bytes = 0; + if (height != nullptr) + *height = 0; + return {}; + } + // Obtain the MemorySink from the backend. The SoftwareBackend was + // constructed with is_headless=true so IVI_SW_SINK defaults to + // "memory" in headless test fixtures; callers that want pixel access + // must ensure IVI_SW_SINK=memory at startup. + auto* sink = dynamic_cast(sw_backend->GetSink()); + if (sink == nullptr) { + if (row_bytes != nullptr) + *row_bytes = 0; + if (height != nullptr) + *height = 0; + return {}; + } + return sink->SnapshotLatest(row_bytes, height); } #endif diff --git a/shell/app.h b/shell/app.h index a251edc1b..ce8c99313 100644 --- a/shell/app.h +++ b/shell/app.h @@ -16,8 +16,10 @@ #pragma once -#include +#include +#include #include +#include #include "configuration/configuration.h" #include "view/flutter_view.h" @@ -45,8 +47,13 @@ class App final { */ [[nodiscard]] int Loop() const; -#if BUILD_BACKEND_HEADLESS_EGL - uint8_t* getViewRenderBuf(int i) const; +#if BUILD_BACKEND_HEADLESS_SOFTWARE + // Returns a snapshot of the most recent rendered frame from the MemorySink. + // row_bytes and height receive the buffer geometry; both are 0 before the + // first frame presents. The returned vector owns its copy of the pixels. + std::vector getViewRenderBuf(int i, + size_t* row_bytes, + size_t* height) const; #endif private: diff --git a/shell/backend/gl_process_resolver.cc b/shell/backend/gl_process_resolver.cc index c9b708f5e..ba56a82a8 100644 --- a/shell/backend/gl_process_resolver.cc +++ b/shell/backend/gl_process_resolver.cc @@ -24,10 +24,6 @@ #include "logging.h" -#if BUILD_BACKEND_HEADLESS_EGL -#include -#endif - std::shared_ptr GlProcessResolver::sInstance = nullptr; EglProcessResolver::~EglProcessResolver() { @@ -83,13 +79,8 @@ void* EglProcessResolver::process_resolver(const char* name) const { return address; } } -#if BUILD_BACKEND_HEADLESS_EGL - SPDLOG_TRACE("** OSMesaGetProcAddress({})", name); - address = reinterpret_cast(OSMesaGetProcAddress(name)); -#else SPDLOG_TRACE("** eglGetProcAddress({})", name); address = reinterpret_cast(eglGetProcAddress(name)); -#endif if (address) { return address; diff --git a/shell/backend/gl_process_resolver.h b/shell/backend/gl_process_resolver.h index 286e7f12a..6f10cc565 100644 --- a/shell/backend/gl_process_resolver.h +++ b/shell/backend/gl_process_resolver.h @@ -21,14 +21,8 @@ class EglProcessResolver { public: -#if BUILD_BACKEND_HEADLESS_EGL - static constexpr char kGlSoNames[3UL][15UL] = {{"libOSMesa.so.8"}, - {"libGLESv2.so.2"}, - {"libEGL.so.1"}}; -#else static constexpr char kGlSoNames[2UL][15UL] = {{"libGLESv2.so.2"}, {"libEGL.so.1"}}; -#endif ~EglProcessResolver(); /** diff --git a/shell/backend/headless/headless.cc b/shell/backend/headless/headless.cc deleted file mode 100644 index bc6b57d67..000000000 --- a/shell/backend/headless/headless.cc +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2021-2024 Toyota Connected North America - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "headless.h" -#include "engine.h" -#include "logging/logging.h" -#include "osmesa.h" -#include "shell/platform/homescreen/flutter_desktop_engine_state.h" -#include "shell/platform/homescreen/flutter_desktop_texture_registrar.h" - -struct FlutterDesktopEngineState; - -HeadlessBackend::HeadlessBackend(const uint32_t initial_width, - const uint32_t initial_height, - const bool /* debug_backend */, - const int /* buffer_size */) - : OSMesaHeadless(static_cast(initial_width), - static_cast(initial_height)), - Backend(), - m_prev_width(initial_width), - m_width(initial_width), - m_prev_height(initial_height), - m_height(initial_height) {} - -void HeadlessBackend::Resize(size_t /* index */, - Engine* flutter_engine, - const int32_t width, - const int32_t height) { - m_prev_width = m_width; - m_prev_height = m_height; - m_width = static_cast(width); - m_height = static_cast(height); - free_buffer(); - m_buf = create_osmesa_buffer(static_cast(m_width), - static_cast(m_height)); - (void)MakeCurrent(); - if (flutter_engine) { - if (const auto result = flutter_engine->SetWindowSize(m_height, m_width); - result != kSuccess) { - spdlog::error("Failed to set Flutter Engine Window Size"); - } - } -} - -void HeadlessBackend::CreateSurface(size_t /* index */, - wl_surface* /*surface*/, - int32_t /* width */, - int32_t /* height */) {} - -FlutterRendererConfig HeadlessBackend::GetRenderConfig() { - return {.type = kOpenGL, - .open_gl = { - .struct_size = sizeof(FlutterOpenGLRendererConfig), - .make_current = [](void* user_data) -> bool { - const auto state = - static_cast(user_data); - return reinterpret_cast( - state->view_controller->engine->GetBackend()) - ->MakeCurrent(); - }, - .clear_current = [](void* userdata) -> bool { - const auto state = - static_cast(userdata); - return reinterpret_cast( - state->view_controller->engine->GetBackend()) - ->ClearCurrent(); - }, - .present = [](void* /* userdata */) -> bool { - Finish(); - return true; - }, - .fbo_callback = [](void*) -> uint32_t { - return 0; // FBO0 - }, - .make_resource_current = [](void* userdata) -> bool { - const auto state = - static_cast(userdata); - return reinterpret_cast( - state->view_controller->engine->GetBackend()) - ->MakeResourceCurrent(); - }, - .fbo_reset_after_present = false, - .surface_transformation = nullptr, - .gl_proc_resolver = [](void* /* userdata */, - const char* name) -> void* { - return reinterpret_cast(OSMesaGetProcAddress(name)); - }, - .gl_external_texture_frame_callback = - [](void* userdata, const int64_t texture_id, - const size_t width, const size_t height, - FlutterOpenGLTexture* texture_out) -> bool { - const auto state = - static_cast(userdata); - return PopulateExternalGlTextureFrame( - state->texture_registrar.get(), texture_id, width, height, - texture_out); - }, - .fbo_with_frame_info_callback = nullptr, - .present_with_info = nullptr, - .populate_existing_damage = nullptr, - }}; -} - -bool HeadlessBackend::TextureMakeCurrent() { - return MakeTextureCurrent(); -} - -bool HeadlessBackend::TextureClearCurrent() { - return ClearCurrent(); -} - -FlutterCompositor HeadlessBackend::GetCompositorConfig() { - return { - .struct_size = sizeof(FlutterCompositor), - .user_data = this, - .create_backing_store_callback = nullptr, - .collect_backing_store_callback = nullptr, - .present_layers_callback = nullptr, - .avoid_backing_store_cache = true, - .present_view_callback = nullptr, - }; -} - -GLubyte* HeadlessBackend::getHeadlessBuffer() const { - return m_buf; -} diff --git a/shell/backend/headless/headless.h b/shell/backend/headless/headless.h deleted file mode 100644 index 0e93241e3..000000000 --- a/shell/backend/headless/headless.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2021-2024 Toyota Connected North America - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -#include "../backend.h" -#include "osmesa.h" - -class Backend; - -class Engine; - -class HeadlessBackend final : public OSMesaHeadless, public Backend { - public: - HeadlessBackend(uint32_t initial_width, - uint32_t initial_height, - bool debug_backend, - int buffer_size); - - /** - * @brief Resize Flutter engine Window size - * @param[in] index No use - * @param[in] flutter_engine Pointer to Flutter engine - * @param[in] width Set window width - * @param[in] height Set window height - * @return void - * @relation - * wayland - */ - void Resize(size_t index, - Engine* flutter_engine, - int32_t width, - int32_t height) override; - - /** - * @brief Create EGL surface - * @param[in] index No use - * @param[in] surface Pointer to surface - * @param[in] width Set surface width - * @param[in] height Set surface height - * @return void - * @relation - * wayland - */ - void CreateSurface(size_t index, - wl_surface* surface, - int32_t width, - int32_t height) override; - - bool TextureMakeCurrent() override; - - bool TextureClearCurrent() override; - - /** - * @brief Get FlutterRendererConfig - * @return FlutterRendererConfig - * @retval Pointer to FlutterRendererConfig - * @relation - * wayland - */ - FlutterRendererConfig GetRenderConfig() override; - - /** - * @brief Get FlutterCompositor - * @return FlutterCompositor - * @retval Pointer to FlutterCompositor - * @relation - * wayland - */ - FlutterCompositor GetCompositorConfig() override; - - GLubyte* getHeadlessBuffer() const; - - private: - uint32_t m_prev_width, m_width; - uint32_t m_prev_height, m_height; -}; diff --git a/shell/backend/headless/osmesa.cc b/shell/backend/headless/osmesa.cc deleted file mode 100644 index 7887a012d..000000000 --- a/shell/backend/headless/osmesa.cc +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2021-2024 Toyota Connected North America - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "osmesa.h" -#include "logging/logging.h" - -#include - -#include - -#include "logging.h" - -OSMesaHeadless::OSMesaHeadless(const int32_t initial_width, - const int32_t initial_height) - : m_height(initial_height), m_width(initial_width) { - if (initial_width == 0) { - m_width = kDefaultViewWidth; - } - if (initial_height == 0) { - m_height = kDefaultViewHeight; - } - - m_context = OSMesaCreateContextExt(OSMESA_RGBA, 16, 0, 0, nullptr); - assert(m_context); - spdlog::trace("Context Created"); - - m_resource_context = OSMesaCreateContextExt(OSMESA_RGBA, 16, 0, 0, m_context); - assert(m_resource_context); - spdlog::trace("Resource Context Created"); - - m_texture_context = OSMesaCreateContextExt(OSMESA_RGBA, 16, 0, 0, m_context); - assert(m_texture_context); - spdlog::trace("Texture Context Created"); - - m_buf = create_osmesa_buffer(m_width, m_height); - - (void)MakeCurrent(); -} - -OSMesaHeadless::~OSMesaHeadless() { - // The original only destroyed m_context, leaking the resource and texture - // contexts and the malloc'd framebuffer. OSMesa contexts have no external - // surface binding (unlike the EGL/Wayland backend), so they can be destroyed - // directly; free the backing buffer last. - if (m_texture_context != nullptr) { - OSMesaDestroyContext(m_texture_context); - m_texture_context = nullptr; - } - if (m_resource_context != nullptr) { - OSMesaDestroyContext(m_resource_context); - m_resource_context = nullptr; - } - if (m_context != nullptr) { - OSMesaDestroyContext(m_context); - m_context = nullptr; - } - free_buffer(); -} - -bool OSMesaHeadless::MakeCurrent() const { - spdlog::trace("+MakeCurrent(), thread_id=0x{:x}", pthread_self()); - const bool ret = - OSMesaMakeCurrent(m_context, m_buf, GL_UNSIGNED_BYTE, m_width, m_height); - assert(ret); - spdlog::trace("-MakeCurrent()"); - return true; -} - -bool OSMesaHeadless::ClearCurrent() const { - spdlog::trace("+ClearCurrent(), thread_id=0x{:x}", pthread_self()); - const bool ret = - OSMesaMakeCurrent(nullptr, nullptr, GL_UNSIGNED_BYTE, m_width, m_height); - assert(ret); - spdlog::trace("-ClearCurrent()"); - return true; -} - -bool OSMesaHeadless::MakeResourceCurrent() const { - spdlog::trace("+MakeResourceCurrent(), thread_id=0x{:x}", pthread_self()); - const bool ret = OSMesaMakeCurrent(m_resource_context, m_buf, - GL_UNSIGNED_BYTE, m_width, m_height); - assert(ret); - spdlog::trace("-MakeResourceCurrent()"); - return true; -} - -bool OSMesaHeadless::MakeTextureCurrent() const { - spdlog::trace("+MakeTextureCurrent(), thread_id=0x{:x}", pthread_self()); - const bool ret = OSMesaMakeCurrent(m_texture_context, m_buf, GL_UNSIGNED_BYTE, - m_width, m_height); - assert(ret); - spdlog::trace("-MakeTextureCurrent()"); - return true; -} - -void OSMesaHeadless::Finish() { - glFinish(); -} - -GLubyte* OSMesaHeadless::create_osmesa_buffer(const int32_t width, - const int32_t height) { - return static_cast( - malloc(static_cast(height * width * 4) * sizeof(GLubyte))); -} - -void OSMesaHeadless::free_buffer() const { - free(m_buf); -} diff --git a/shell/backend/headless/osmesa.h b/shell/backend/headless/osmesa.h deleted file mode 100644 index 7a8a6cc68..000000000 --- a/shell/backend/headless/osmesa.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2021-2024 Toyota Connected North America - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include - -class OSMesaHeadless { - public: - OSMesaHeadless(int32_t initial_width, int32_t initial_height); - - ~OSMesaHeadless(); - - /** - * @brief Clear an EGL rendering context - * @return bool - * @retval true Normal end - * @retval false Abnormal end - * @relation - * wayland - */ - [[nodiscard]] bool ClearCurrent() const; - - /** - * @brief Attach an EGL rendering context to EGL surface - * @return bool - * @retval true Normal end - * @retval false Abnormal end - * @relation - * wayland - */ - bool MakeCurrent() const; - bool MakeResourceCurrent() const; - bool MakeTextureCurrent() const; - - static void Finish(); - - /** - * @brief Create a GLUbyte buffer to bind to an OSMesa Context - * @param[in] width - * @param[in] height - * @return GLubyte* - * @retval GLUbyte buffer - */ - static GLubyte* create_osmesa_buffer(int32_t width, int32_t height); - void free_buffer() const; - - protected: - uint8_t* m_buf{}; - - private: - OSMesaContext m_context{}; - OSMesaContext m_resource_context{}; - OSMesaContext m_texture_context{}; - int32_t m_height{}; - int32_t m_width{}; -}; diff --git a/shell/backend/software/software_backend.cc b/shell/backend/software/software_backend.cc index f0fc0e3ed..aef06f537 100644 --- a/shell/backend/software/software_backend.cc +++ b/shell/backend/software/software_backend.cc @@ -17,6 +17,8 @@ #include "backend/software/software_backend.h" #include "logging/logging.h" +#include "backend/software/none_sink.h" + #include #include #include @@ -30,11 +32,29 @@ SoftwareBackend::SoftwareBackend(const uint32_t initial_width, const uint32_t initial_height, - std::unique_ptr sink) + std::unique_ptr sink, + const bool is_headless) : Backend(), + is_headless_(is_headless), width_(initial_width), height_(initial_height), sink_(std::move(sink)) { + if (is_headless_) { + // In headless mode hardware sinks (DRM dumb / fbdev) are meaningless — + // there is no display to scan out to. If the caller passed one (e.g. via + // a stale IVI_SW_SINK env var), swap it for a NoneSink so the process + // never tries to open /dev/dri/* or /dev/fb*. + if (sink_ && sink_->NativeSize().has_value()) { + spdlog::warn( + "[SoftwareBackend] headless mode: hardware sink ignored; " + "falling back to NoneSink"); + sink_ = std::make_unique(); + } + spdlog::info( + "[SoftwareBackend] headless mode (kSoftware renderer, no GPU/Wayland)"); + } else { + spdlog::info("[SoftwareBackend] software mode"); + } if (sink_) { // Adopt the sink's native extent (the DRM mode / fbdev virtual size) so // Flutter renders at the panel's resolution — the frame fills and centers, @@ -97,6 +117,9 @@ void SoftwareBackend::CreateSurface(size_t /* index */, wl_surface* /* unused */, const int32_t width, const int32_t height) { + if (is_headless_) { + SPDLOG_TRACE("[SoftwareBackend] CreateSurface no-op (headless mode)"); + } width_ = static_cast(width); height_ = static_cast(height); if (sink_) { @@ -114,6 +137,11 @@ FlutterRendererConfig SoftwareBackend::GetRenderConfig() { } VsyncCallback SoftwareBackend::GetVsyncCallback() const { + // Headless mode has no display vblank source; always use the wall-clock + // Flutter scheduler regardless of sink or env-var setting. + if (is_headless_) { + return nullptr; + } static const bool env_disabled = []() { const char* env = std::getenv("IVI_SW_VSYNC"); return env != nullptr && std::string_view(env) == "0"; diff --git a/shell/backend/software/software_backend.h b/shell/backend/software/software_backend.h index 6331893ef..1efe307d5 100644 --- a/shell/backend/software/software_backend.h +++ b/shell/backend/software/software_backend.h @@ -32,7 +32,8 @@ class SoftwareBackend final : public Backend { public: SoftwareBackend(uint32_t initial_width, uint32_t initial_height, - std::unique_ptr sink); + std::unique_ptr sink, + bool is_headless = false); // dtor emits the IVI_SW_PROFILE session summary when the env var is // set. Order matters: backend destructs before the sink so the @@ -65,13 +66,19 @@ class SoftwareBackend final : public Backend { [[nodiscard]] uint32_t width() const { return width_; } [[nodiscard]] uint32_t height() const { return height_; } + // Exposes the active sink for in-process inspection (e.g. MemorySink pixel + // capture in headless test fixtures). Returns nullptr when no sink is set. + [[nodiscard]] ISurfaceSink* GetSink() const { return sink_.get(); } + // Forward the shared software cursor to the sink (the dumb sink composites - // it). Called by FlutterView with the SoftwareDisplay-owned cursor. No-op for - // headless sinks. (SoftwareCursor is forward-declared via surface_sink.h.) + // it). Called by FlutterView with the SoftwareDisplay-owned cursor. No-op in + // headless mode (no display, no cursor compositing) and for headless sinks. + // (SoftwareCursor is forward-declared via surface_sink.h.) void SetCursor(std::shared_ptr cursor) { - if (sink_) { - sink_->SetCursor(std::move(cursor)); + if (is_headless_ || !sink_) { + return; } + sink_->SetCursor(std::move(cursor)); } // Vsync wiring is gated on the sink advertising a real vblank source @@ -121,6 +128,7 @@ class SoftwareBackend final : public Backend { // is disabled. Window log every 60 frames, session summary emitted from dtor. void ProfilePresent(bool ok); + const bool is_headless_; uint32_t width_; uint32_t height_; std::unique_ptr sink_; diff --git a/shell/display/software_display.cc b/shell/display/software_display.cc index 53ad7cc9c..a9a0bb041 100644 --- a/shell/display/software_display.cc +++ b/shell/display/software_display.cc @@ -18,8 +18,10 @@ #include -#include "backend/software/input/software_seat.h" #include "input/iseat.h" +#if BUILD_SOFTWARE_INPUT_LIBINPUT +#include "backend/software/input/software_seat.h" +#endif SoftwareDisplay::SoftwareDisplay(const int32_t width, const int32_t height, @@ -37,17 +39,22 @@ void SoftwareDisplay::SetViewportSize(const int32_t width, width_ = width; height_ = height; // SetViewport is SoftwareSeat-specific (not on ISeat), so downcast — the - // only seat type a SoftwareDisplay ever holds. + // only seat type a SoftwareDisplay ever holds. No-op when libinput is + // disabled (no seat is ever set in that configuration). +#if BUILD_SOFTWARE_INPUT_LIBINPUT if (auto* sw_seat = dynamic_cast(seat_.get())) { sw_seat->SetViewport(width, height); } +#endif } void SoftwareDisplay::SetCursor(std::shared_ptr cursor) { cursor_ = std::move(cursor); +#if BUILD_SOFTWARE_INPUT_LIBINPUT if (auto* sw_seat = dynamic_cast(seat_.get())) { sw_seat->SetCursor(cursor_); } +#endif } void SoftwareDisplay::StartEvents() { diff --git a/shell/engine.cc b/shell/engine.cc index 08c49d900..0a618a219 100644 --- a/shell/engine.cc +++ b/shell/engine.cc @@ -45,8 +45,7 @@ // forward-declares WaylandWindow unconditionally so the header // compiles without Wayland; the .cc needs the real definition only // when Wayland is selected. -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN #include "wayland/window.h" #endif @@ -682,8 +681,7 @@ bool Engine::ActivateSystemCursor(const int32_t device, if (!m_egl_window) { return true; } -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN return m_egl_window->ActivateSystemCursor(device, kind); #else // Forward-declaration only on non-Wayland builds; m_egl_window is diff --git a/shell/platform/homescreen/CMakeLists.txt b/shell/platform/homescreen/CMakeLists.txt index 46b7f2d0d..fccf523be 100644 --- a/shell/platform/homescreen/CMakeLists.txt +++ b/shell/platform/homescreen/CMakeLists.txt @@ -44,8 +44,7 @@ target_link_libraries(platform_homescreen PUBLIC # the wayland-gen protocol-header include dirs and the static library are both # scoped to those builds. Non-Wayland backends get neither the headers nor the # libwayland-client / libwayland-cursor runtime deps. -if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN OR - BUILD_BACKEND_HEADLESS_EGL) +if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN) target_include_directories(platform_homescreen PUBLIC $ ) diff --git a/shell/platform/homescreen/flutter_desktop.cc b/shell/platform/homescreen/flutter_desktop.cc index 7466e78c1..b6c867b3c 100644 --- a/shell/platform/homescreen/flutter_desktop.cc +++ b/shell/platform/homescreen/flutter_desktop.cc @@ -448,13 +448,12 @@ void FlutterDesktopTextureRegistrarUnregisterExternalTexture( // in case the engine is mid-teardown. // // GLESv2 is only linked into the final binary when a GL-based backend - // is enabled (Wayland EGL, DRM KMS EGL, Headless EGL). Wayland Vulkan + // is enabled (Wayland EGL, DRM KMS EGL). Wayland Vulkan // builds don't link it and don't produce GL pixel-buffer textures at // runtime — but the linker still needs the glDeleteTextures symbol // unless we compile this branch out. if (removed->pixel_buffer_callback && removed->name != 0) { -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_DRM_KMS_EGL || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_DRM_KMS_EGL Backend* backend = nullptr; if (texture_registrar->engine && texture_registrar->engine->view_controller && diff --git a/shell/platform/homescreen/mouse_cursor_handler.cc b/shell/platform/homescreen/mouse_cursor_handler.cc index 786d9a92b..108a4ed4d 100644 --- a/shell/platform/homescreen/mouse_cursor_handler.cc +++ b/shell/platform/homescreen/mouse_cursor_handler.cc @@ -23,8 +23,7 @@ // window->ActivateSystemCursor() call. Forward-decl in flutter_view.h // is enough for everything else (GetWindow's return type, the // `if (!window)` null check). -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN #include "wayland/window.h" #endif @@ -77,8 +76,7 @@ void MouseCursorHandler::HandleMethodCall( result->Success(flutter::EncodableValue(true)); return; } -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN auto res = window->ActivateSystemCursor(device, kind); result->Success(flutter::EncodableValue(res)); #else diff --git a/shell/platform/homescreen/public/flutter_homescreen.h b/shell/platform/homescreen/public/flutter_homescreen.h index e9ca024b7..4d23c4a60 100644 --- a/shell/platform/homescreen/public/flutter_homescreen.h +++ b/shell/platform/homescreen/public/flutter_homescreen.h @@ -76,7 +76,7 @@ typedef struct { // |kFlutterDesktopGpuSurfaceTypeGlTexture2D| external textures. // // Returns false if the embedder's current backend is not EGL-based -// (e.g. headless OSMesa, Vulkan) or the handles are not yet initialized. +// (e.g. headless Software, Vulkan) or the handles are not yet initialized. FLUTTER_EXPORT bool FlutterDesktopPluginRegistrarGetEglContext( FlutterDesktopPluginRegistrarRef registrar, FlutterDesktopEglContext* out); diff --git a/shell/view/flutter_view.cc b/shell/view/flutter_view.cc index 135020bc6..fdefa17f2 100644 --- a/shell/view/flutter_view.cc +++ b/shell/view/flutter_view.cc @@ -22,8 +22,11 @@ #include "app.h" -#if BUILD_BACKEND_HEADLESS_EGL -#include "backend/headless/headless.h" +#if BUILD_BACKEND_HEADLESS_SOFTWARE +#include "backend/software/memory_sink.h" +#include "backend/software/none_sink.h" +#include "backend/software/sink_factory.h" +#include "backend/software/software_backend.h" #elif BUILD_BACKEND_DRM_KMS_EGL #include "backend/drm_kms_egl/drm_backend.h" #include "display/drm_display.h" @@ -60,9 +63,9 @@ extern void PluginsApiRegisterPlugins(FlutterDesktopEngineRef engine); #endif -#if !BUILD_BACKEND_DRM_KMS_EGL && !BUILD_BACKEND_SOFTWARE -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if !BUILD_BACKEND_DRM_KMS_EGL && !BUILD_BACKEND_SOFTWARE && \ + !BUILD_BACKEND_HEADLESS_SOFTWARE +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN #include "wayland/display.h" #include "wayland/window.h" #endif @@ -75,12 +78,7 @@ FlutterView::FlutterView(Configuration::Config config, const size_t index, const std::shared_ptr& display) : m_display(display), m_config(std::move(config)), m_index(index) { -#if BUILD_BACKEND_HEADLESS_EGL - m_backend = std::make_shared( - m_config.view.width.value_or(kDefaultViewWidth), - m_config.view.height.value_or(kDefaultViewHeight), - m_config.debug_backend.value_or(false), kEglBufferSize); -#elif BUILD_BACKEND_DRM_KMS_EGL +#if BUILD_BACKEND_DRM_KMS_EGL { auto parse_tri = [](const std::optional& s, drm_config::TriState def = @@ -313,6 +311,17 @@ FlutterView::FlutterView(Configuration::Config config, m_config.view.height.value_or(kDefaultViewHeight), MakeSinkFromEnv(m_config.view.drm_device.value_or(std::string{}))); } +#elif BUILD_BACKEND_HEADLESS_SOFTWARE + { + // In headless mode the default sink is NoneSink (frames discarded). + // Test fixtures that need pixel access should set IVI_SW_SINK=memory + // at launch; MakeSinkFromEnv() will pick it up. + m_backend = std::make_shared( + m_config.view.width.value_or(kDefaultViewWidth), + m_config.view.height.value_or(kDefaultViewHeight), + MakeSinkFromEnv(m_config.view.drm_device.value_or(std::string{})), + /*is_headless=*/true); + } #endif SPDLOG_DEBUG("Width: {}, Height: {}", @@ -320,7 +329,7 @@ FlutterView::FlutterView(Configuration::Config config, m_config.view.height.value_or(kDefaultViewHeight)); #if !BUILD_BACKEND_DRM_KMS_EGL && !BUILD_BACKEND_DRM_KMS_VULKAN && \ - !BUILD_BACKEND_SOFTWARE + !BUILD_BACKEND_SOFTWARE && !BUILD_BACKEND_HEADLESS_SOFTWARE auto* wl = dynamic_cast(display.get()); m_wayland_window = std::make_shared( m_index, std::dynamic_pointer_cast(display), @@ -439,7 +448,7 @@ FlutterView::~FlutterView() { } #if !BUILD_BACKEND_DRM_KMS_EGL && !BUILD_BACKEND_DRM_KMS_VULKAN && \ - !BUILD_BACKEND_SOFTWARE + !BUILD_BACKEND_SOFTWARE && !BUILD_BACKEND_HEADLESS_SOFTWARE Display* FlutterView::GetDisplay() const { return dynamic_cast(m_display.get()); } @@ -500,11 +509,12 @@ void FlutterView::Initialize() { // fullscreen actually gets mode dims here instead of a stale 1024x768 etc. const auto width = static_cast(m_backend->width()); const auto height = static_cast(m_backend->height()); -#elif BUILD_BACKEND_SOFTWARE +#elif BUILD_BACKEND_SOFTWARE || BUILD_BACKEND_HEADLESS_SOFTWARE // The SoftwareBackend adopts the sink's native mode (DRM / fbdev) as its // resolved extent; render the engine at that so the frame fills + centers on // the panel instead of being top-left cropped from a config-sized view. - // Falls back to the config dims for sinks with no native size (file/memory). + // Falls back to the config dims for sinks with no native size + // (file/memory/none). auto* sw_backend = dynamic_cast(m_backend.get()); const auto width = static_cast( sw_backend ? sw_backend->width() @@ -563,10 +573,19 @@ void FlutterView::Initialize() { // and hand the display's cursor to the sink (which composites it). if (auto* sw_display = dynamic_cast(m_display.get())) { sw_display->SetViewportSize(width, height); - if (auto* sw_backend = dynamic_cast(m_backend.get())) { - sw_backend->SetCursor(sw_display->cursor()); + if (auto* sw_backend_ptr = + dynamic_cast(m_backend.get())) { + sw_backend_ptr->SetCursor(sw_display->cursor()); } } +#elif BUILD_BACKEND_HEADLESS_SOFTWARE + // No WaylandWindow, no SoftwareDisplay: send window metrics explicitly. + { + const auto result = m_flutter_engine->SetWindowSize( + static_cast(height), static_cast(width)); + spdlog::info("[SoftwareBackend/headless] SendWindowMetrics {}x{} result={}", + width, height, static_cast(result)); + } #else // Engine events are decoded by surface pointer dynamic_cast(m_display.get()) diff --git a/shell/view/flutter_view.h b/shell/view/flutter_view.h index 3de1ce49d..699372aa7 100644 --- a/shell/view/flutter_view.h +++ b/shell/view/flutter_view.h @@ -25,8 +25,7 @@ #if BUILD_ACCESSIBILITY #include "shell/accessibility/accessibility_tree.h" #endif -#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN || \ - BUILD_BACKEND_HEADLESS_EGL +#if BUILD_BACKEND_WAYLAND_EGL || BUILD_BACKEND_WAYLAND_VULKAN #include "wayland/window.h" #endif @@ -62,23 +61,21 @@ class PlatformChannel; namespace flutter { class KeyEventHandler; } -#if BUILD_BACKEND_HEADLESS_EGL -class HeadlessBackend; +#if BUILD_BACKEND_HEADLESS_SOFTWARE || BUILD_BACKEND_SOFTWARE +class SoftwareBackend; #elif BUILD_BACKEND_DRM_KMS_EGL class DrmBackend; #elif BUILD_BACKEND_DRM_KMS_VULKAN class VulkanDrmBackend; -#elif BUILD_BACKEND_SOFTWARE -class SoftwareBackend; #elif BUILD_BACKEND_WAYLAND_EGL class WaylandEglBackend; #elif BUILD_BACKEND_WAYLAND_VULKAN class WaylandVulkanBackend; #else #error \ - "no Flutter backend selected: define one of BUILD_BACKEND_HEADLESS_EGL, " \ - "BUILD_BACKEND_DRM_KMS_EGL, BUILD_BACKEND_DRM_KMS_VULKAN, " \ - "BUILD_BACKEND_SOFTWARE, BUILD_BACKEND_WAYLAND_EGL, " \ + "no Flutter backend selected: define one of BUILD_BACKEND_HEADLESS_SOFTWARE, " \ + "BUILD_BACKEND_SOFTWARE, BUILD_BACKEND_DRM_KMS_EGL, " \ + "BUILD_BACKEND_DRM_KMS_VULKAN, BUILD_BACKEND_WAYLAND_EGL, " \ "BUILD_BACKEND_WAYLAND_VULKAN" #endif #ifdef ENABLE_PLUGIN_COMP_SURF @@ -263,14 +260,12 @@ class FlutterView { FML_DISALLOW_COPY_AND_ASSIGN(FlutterView); private: -#if BUILD_BACKEND_HEADLESS_EGL - std::shared_ptr m_backend; +#if BUILD_BACKEND_HEADLESS_SOFTWARE || BUILD_BACKEND_SOFTWARE + std::shared_ptr m_backend; #elif BUILD_BACKEND_DRM_KMS_EGL std::shared_ptr m_backend{}; #elif BUILD_BACKEND_DRM_KMS_VULKAN std::shared_ptr m_backend{}; -#elif BUILD_BACKEND_SOFTWARE - std::shared_ptr m_backend; #elif BUILD_BACKEND_WAYLAND_EGL std::shared_ptr m_backend; #elif BUILD_BACKEND_WAYLAND_VULKAN diff --git a/test/unit_test/CMakeLists.txt b/test/unit_test/CMakeLists.txt index fb4ef6a74..4d805e0e3 100644 --- a/test/unit_test/CMakeLists.txt +++ b/test/unit_test/CMakeLists.txt @@ -11,7 +11,7 @@ list(APPEND TYPICAL_TEST_SOURCES ${PROJECT_SOURCE_DIR}/test/unit_test/unit_test_ # When tweaking TYPICAL_TEST_DEFINITIONS in CMakeLists.txt for each test case, it is better to always set TYPICAL_TEST_DEFINITIONS the same. # Therefore, TYPICAL_TEST_DEFINITIONS is set one by one without including options.cmake and plugins.cmake. list(APPEND TYPICAL_TEST_DEFINITIONS ENABLE_XDG_CLIENT) -list(APPEND TYPICAL_TEST_DEFINITIONS BUILD_BACKEND_HEADLESS_EGL) +list(APPEND TYPICAL_TEST_DEFINITIONS BUILD_BACKEND_HEADLESS_SOFTWARE) list(APPEND TYPICAL_TEST_DEFINITIONS ENABLE_EGL) list(APPEND TYPICAL_TEST_DEFINITIONS BUILD_EXAMPLES) list(APPEND TYPICAL_TEST_DEFINITIONS HAVE_STRCHRNUL) diff --git a/test/unit_test/README.md b/test/unit_test/README.md index 923a5d414..253642132 100644 --- a/test/unit_test/README.md +++ b/test/unit_test/README.md @@ -32,7 +32,7 @@ build$ firefox lcovHtml/index.html # Writing tests ## Headless Backend -Testing requires BUILD_BACKEND_HEADLESS_EGL compile option, which uses OSMesa to render to an offscreen buffer. This buffer can then be written to a file for comparison to a known-good result. +Testing requires BUILD_BACKEND_HEADLESS_SOFTWARE compile option, which uses the kSoftware (CPU) renderer with a MemorySink to capture frames to an in-process buffer. This buffer can then be written to a file for comparison to a known-good result. ## Flutter App For unit tests that generate/compare images, a flutter app bundle must be provided via the UNIT_TEST_APP_BUNDLE option. @@ -62,7 +62,7 @@ or `-DSANITIZE_THREAD=ON` for TSan. All three drivers are sanitizer-clean. Full golden-frame tests for the compositor path (single-layer parity vs. the non-compositor baseline, two-layer interleave on EGL and Vulkan, Filament plugin composition) require: - A Flutter bundle that produces a `PlatformViewLayer` in its frame graph. -- A headless EGL or Vulkan context (OSMesa or `lavapipe`) to run without a display. +- No GPU or display required: BUILD_BACKEND_HEADLESS_SOFTWARE uses the kSoftware CPU renderer (no Mesa, no EGL). The groundwork — `ImageType`, `utils_write_targa`, `utils_images_are_equal`, `UNIT_TEST_SAVE_GOLDENS` — is in place, but the compositor tests aren't written yet. They land when the headless-compositor harness does. diff --git a/test/unit_test/app-headless-test/test_case_app_headless.cc b/test/unit_test/app-headless-test/test_case_app_headless.cc index 5fa901bc1..13eb796c3 100644 --- a/test/unit_test/app-headless-test/test_case_app_headless.cc +++ b/test/unit_test/app-headless-test/test_case_app_headless.cc @@ -4,10 +4,9 @@ #include #include -#include - #include "app.h" -#include "backend/headless/headless.h" +#include "backend/software/memory_sink.h" +#include "backend/software/software_backend.h" #include "configuration/configuration.h" #include "logging.h" #include "unit_test_utils.h" @@ -39,20 +38,18 @@ TEST(HomescreenAppHeadless, Lv1Normal001) { const auto test_filename = utils_get_image_filename(TEST, "1"); const auto golden_filename = utils_get_image_filename(GOLDEN, "1"); +#if BUILD_BACKEND_HEADLESS_SOFTWARE + size_t row_bytes = 0; + size_t frame_height = 0; + const auto buf = app.getViewRenderBuf(0, &row_bytes, &frame_height); + const auto w = static_cast(configs[0].view.width.value_or(1920)); + const auto h = static_cast(configs[0].view.height.value_or(1080)); #if SAVE_IMAGE_FOR_COMPARISON - utils_write_targa(app.getViewRenderBuf(0), golden_filename, - static_cast(configs[0].view.width.value_or(1920)), - static_cast(configs[0].view.height.value_or(1080))); + utils_write_targa(buf.data(), golden_filename, w, h); #endif -#if BUILD_BACKEND_HEADLESS_EGL - utils_write_targa(app.getViewRenderBuf(0), test_filename, - static_cast(configs[0].view.width.value_or(1920)), - static_cast(configs[0].view.height.value_or(1080))); - const int images_are_equal = utils_images_are_equal( - test_filename, golden_filename, - static_cast(configs[0].view.width.value_or(1920)), - static_cast(configs[0].view.height.value_or(1080))); - + utils_write_targa(buf.data(), test_filename, w, h); + const int images_are_equal = + utils_images_are_equal(test_filename, golden_filename, w, h); EXPECT_EQ(images_are_equal, 1); #endif } diff --git a/test/unit_test/compositor_headless_golden-test/CMakeLists.txt b/test/unit_test/compositor_headless_golden-test/CMakeLists.txt index e6983c933..7314ae5da 100644 --- a/test/unit_test/compositor_headless_golden-test/CMakeLists.txt +++ b/test/unit_test/compositor_headless_golden-test/CMakeLists.txt @@ -1,10 +1,10 @@ -# Compositor-mode parity gate against the OSMesa headless backend. +# Compositor-mode parity gate against the headless software backend. # Builds only when both flags are on; the existing test infrastructure -# already requires BUILD_BACKEND_HEADLESS_EGL. +# already requires BUILD_BACKEND_HEADLESS_SOFTWARE. if (NOT BUILD_COMPOSITOR) return() endif () -if (NOT BUILD_BACKEND_HEADLESS_EGL) +if (NOT BUILD_BACKEND_HEADLESS_SOFTWARE) return() endif () diff --git a/test/unit_test/compositor_headless_golden-test/test_case_compositor_headless_golden.cc b/test/unit_test/compositor_headless_golden-test/test_case_compositor_headless_golden.cc index 997ee7d0a..62cfb607e 100644 --- a/test/unit_test/compositor_headless_golden-test/test_case_compositor_headless_golden.cc +++ b/test/unit_test/compositor_headless_golden-test/test_case_compositor_headless_golden.cc @@ -14,7 +14,7 @@ * limitations under the License. */ -// Compositor-mode golden frame test running against the OSMesa headless +// Compositor-mode golden frame test running against the headless software // backend. Same harness as test_case_app_headless.cc but compiled with // BUILD_COMPOSITOR on. Establishes a parity gate: the single-layer fast // path through the compositor must produce the same pixels as the diff --git a/test/unit_test/configuration-test-case/CMakeLists.txt b/test/unit_test/configuration-test-case/CMakeLists.txt index a8f310934..83cd26bd8 100644 --- a/test/unit_test/configuration-test-case/CMakeLists.txt +++ b/test/unit_test/configuration-test-case/CMakeLists.txt @@ -5,7 +5,7 @@ set(TESTCASE_CC test_case_configuration.cc; test_case_configuration_getCliOverrides.cc; test_case_configuration_PrintConfig.cc;) list(REMOVE_ITEM TYPICAL_TEST_DEFINITIONS "ENABLE_PLUGIN_URL_LAUNCHER") -list(REMOVE_ITEM TYPICAL_TEST_DEFINITIONS "BUILD_BACKEND_HEADLESS_EGL") +list(REMOVE_ITEM TYPICAL_TEST_DEFINITIONS "BUILD_BACKEND_HEADLESS_SOFTWARE") # Basically, the following statements need not be modified add_executable( diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 477c993fa..6435cf2f1 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -70,8 +70,7 @@ add_subdirectory(flutter) # backends would drag Wayland headers + libwayland into builds that must stay # Wayland-free, so gate the whole subproject. # -if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN OR - BUILD_BACKEND_HEADLESS_EGL) +if (BUILD_BACKEND_WAYLAND_EGL OR BUILD_BACKEND_WAYLAND_VULKAN) if (BUILD_BACKEND_WAYLAND_EGL) set(ENABLE_EGL ON) else ()