From 9a644a284e38ac5688ca8afa2e6a808eeb01459f Mon Sep 17 00:00:00 2001 From: Joel Winarske Date: Tue, 14 Apr 2026 20:20:05 -0700 Subject: [PATCH] [compositor] golden-frame harness scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establish the headless golden-frame test infrastructure for the compositor path. Single-layer parity gate is wired and runs; multi- layer (PlatformViewLayer) coverage drops in alongside it once a Dart bundle that emits one is staged. - compositor_headless_golden-test: SingleLayerFastPathParity boots the homescreen app via the OSMesa headless backend with BUILD_COMPOSITOR=ON, captures getViewRenderBuf(0), writes a .tga, and pixel-diffs against a stored golden. UNIT_TEST_SAVE_GOLDENS=ON generates the golden then fails with a clear regenerate-then-rerun message. Sanitizer support inherited from the typical-test scaffold. - docs/COMPOSITOR_GOLDENS.md: operator's guide. Prerequisites with Fedora and Ubuntu install lines (mesa-compat-libOSMesa, mesa-vulkan-drivers/libosmesa6), bundle requirements with a minimal Dart Stack+PlatformViewSurface example, run commands for parity and save-then-compare flows, sanitizer combos, weston-headless + lavapipe recipe for Vulkan goldens, and a "what's deferred" section listing HeadlessBackend compositor wiring (OSMesa is desktop GL not GLES — needs parallel implementation of GlCompositor/EglFboBackingStore), the absent native headless Vulkan backend, and bundle staging. - third_party/CMakeLists.txt: gtest and gtest_main get -Wno-error=sign-conversion / -Wno-error=conversion. GCC 15.2 promotes a sign-conversion warning inside googletest's gtest.cc (kSize template parameter) to an error under the project's -Werror toolchain settings, blocking any unit test build on this host. Project-side -Werror untouched — only the third-party gtest target gets the relaxation. All four pre-existing compositor unit tests (backing_store_pool, present_layer_sequencer, compositor_registry, mutation_stack) pass under ctest in this configuration. The new test driver runs to the bundle-load step against a fake bundle path; a real Flutter bundle is needed to complete the parity comparison. Default EGL and Vulkan+compositor non-test builds still link clean. Signed-off-by: Joel Winarske --- docs/COMPOSITOR_GOLDENS.md | 125 ++++++++++++++++++ test/unit_test/CMakeLists.txt | 1 + .../CMakeLists.txt | 52 ++++++++ .../test_case_compositor_headless_golden.cc | 79 +++++++++++ third_party/CMakeLists.txt | 9 ++ 5 files changed, 266 insertions(+) create mode 100644 docs/COMPOSITOR_GOLDENS.md create mode 100644 test/unit_test/compositor_headless_golden-test/CMakeLists.txt create mode 100644 test/unit_test/compositor_headless_golden-test/test_case_compositor_headless_golden.cc diff --git a/docs/COMPOSITOR_GOLDENS.md b/docs/COMPOSITOR_GOLDENS.md new file mode 100644 index 00000000..29683836 --- /dev/null +++ b/docs/COMPOSITOR_GOLDENS.md @@ -0,0 +1,125 @@ +# Compositor golden-frame tests + +Pixel-diff regression gates for the compositor path. Two layers of coverage: + +1. **Single-layer parity** — running the same Flutter app under `BUILD_COMPOSITOR=ON` should produce the same image as under `BUILD_COMPOSITOR=OFF`. Captured by `compositor_headless_golden-test`. This gate flips the moment a compositor change subtly perturbs a Flutter-only frame. +2. **Multi-layer interleave** — Flutter content above and below a `PlatformViewSurface`, expected pixel patterns committed as goldens. Requires a Flutter bundle that emits the platform view; see [Bundle requirements](#bundle-requirements). + +Both run against software rasterizers so they're CI-friendly. + +## Prerequisites + +### OSMesa (headless OpenGL) + +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 +``` + +### lavapipe (headless Vulkan) + +Mesa's software Vulkan ICD. Used to drive `WaylandVulkanBackend` without GPU hardware. Already shipped with `mesa-vulkan-drivers` on most distros — verify with: + +```bash +ls /usr/share/vulkan/icd.d/lvp_icd.*.json +VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json vulkaninfo --summary +``` + +Driver name should report `llvmpipe`. + +### Bundle requirements + +`UNIT_TEST_APP_BUNDLE` must point to a built Flutter bundle. For the parity test any bundle works (it just renders Flutter UI). + +For multi-layer goldens the bundle must construct a `PlatformViewSurface` with one of the registered view types (`layer_playground_view` or `nav_render_view`). Minimal example: + +```dart +class GoldenScene extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Stack(children: [ + Container(color: Colors.green), // layer 0 + const SizedBox( + width: 400, height: 300, + child: PlatformViewSurface( + viewType: 'layer_playground_view', + controller: ..., // layer 1 + gestureRecognizers: >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ), + ), + Positioned( + top: 100, left: 100, + child: Text('above', style: TextStyle(fontSize: 48)), // layer 2 + ), + ]); + } +} +``` + +## Running the parity gate + +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_COMPOSITOR=ON \ + -DUNIT_TEST_APP_BUNDLE=/path/to/flutter/build/linux/x64/release/bundle \ + -DBUILD_NUMBER=1 +cmake --build build-goldens --target homescreen_compositor_headless_golden_ut_test_driver -j$(nproc) +ctest --test-dir build-goldens -R compositor_headless_golden -V +``` + +First run with no committed golden: regenerate. + +```bash +cmake -B build-goldens -DUNIT_TEST_SAVE_GOLDENS=ON . +ctest --test-dir build-goldens -R compositor_headless_golden -V # writes the golden then fails +# Inspect build-goldens/test/unit_test/test_images_golden/*.tga manually. +cmake -B build-goldens -DUNIT_TEST_SAVE_GOLDENS=OFF . +ctest --test-dir build-goldens -R compositor_headless_golden -V # now passes if pixels match +``` + +The compare tolerance is per-channel `<= 2` (driver rounding); see `utils_images_are_equal`. + +## Running with sanitizers + +```bash +cmake -B build-goldens -DSANITIZE_ADDRESS=ON . +cmake --build build-goldens --target homescreen_compositor_headless_golden_ut_test_driver -j$(nproc) +ctest --test-dir build-goldens -R compositor_headless_golden +``` + +`-DSANITIZE_THREAD=ON` for TSan. + +## Vulkan goldens with lavapipe + +The `WaylandVulkanBackend` requires a real Wayland compositor (it uses `VK_KHR_wayland_surface` + a swapchain). Lavapipe alone isn't enough — pair it with weston-headless: + +```bash +weston --backend=headless-backend.so --width=1920 --height=1080 & +WAYLAND_DISPLAY=wayland-1 \ +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. + +## 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. + +When those land, the additional test cases drop into `compositor_headless_golden-test/` next to the existing parity test. diff --git a/test/unit_test/CMakeLists.txt b/test/unit_test/CMakeLists.txt index 5123ec82..0109b694 100644 --- a/test/unit_test/CMakeLists.txt +++ b/test/unit_test/CMakeLists.txt @@ -72,4 +72,5 @@ add_subdirectory(backing_store_pool-test) add_subdirectory(present_layer_sequencer-test) add_subdirectory(compositor_registry-test) add_subdirectory(mutation_stack-test) +add_subdirectory(compositor_headless_golden-test) #add_subdirectory(texture-test) diff --git a/test/unit_test/compositor_headless_golden-test/CMakeLists.txt b/test/unit_test/compositor_headless_golden-test/CMakeLists.txt new file mode 100644 index 00000000..e6983c93 --- /dev/null +++ b/test/unit_test/compositor_headless_golden-test/CMakeLists.txt @@ -0,0 +1,52 @@ +# Compositor-mode parity gate against the OSMesa headless backend. +# Builds only when both flags are on; the existing test infrastructure +# already requires BUILD_BACKEND_HEADLESS_EGL. +if (NOT BUILD_COMPOSITOR) + return() +endif () +if (NOT BUILD_BACKEND_HEADLESS_EGL) + return() +endif () + +set(TESTCASE_NAME "homescreen_compositor_headless_golden_ut_test_driver") +set(TESTCASE_CC test_case_compositor_headless_golden.cc) +list(REMOVE_ITEM TYPICAL_TEST_DEFINITIONS "ENABLE_PLUGIN_URL_LAUNCHER") + +if (UNIT_TEST_SAVE_GOLDENS) + list(APPEND TYPICAL_TEST_DEFINITIONS SAVE_IMAGE_FOR_COMPARISON=1) +else () + list(APPEND TYPICAL_TEST_DEFINITIONS SAVE_IMAGE_FOR_COMPARISON=0) +endif () + +add_executable( + ${TESTCASE_NAME} + ${TYPICAL_TEST_SOURCES} + ${TESTCASE_CC} +) + +add_sanitizers(${TESTCASE_NAME}) + +target_compile_definitions( + ${TESTCASE_NAME} + PRIVATE + ${TYPICAL_TEST_DEFINITIONS} +) + +target_include_directories( + ${TESTCASE_NAME} + PRIVATE + ${TYPICAL_TEST_INC_DIRS} +) + +target_link_libraries( + ${TESTCASE_NAME} + PRIVATE + gtest_main + ${TYPICAL_TEST_LINK_LIBS} + toolchain::toolchain +) + +add_test( + NAME ${TESTCASE_NAME} + COMMAND ${TESTCASE_NAME} +) 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 new file mode 100644 index 00000000..83f04e14 --- /dev/null +++ b/test/unit_test/compositor_headless_golden-test/test_case_compositor_headless_golden.cc @@ -0,0 +1,79 @@ +/* + * Copyright 2026 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. + */ + +// Compositor-mode golden frame test running against the OSMesa headless +// 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 +// non-compositor path. Once HeadlessBackend grows full compositor wiring +// (currently dormant — see docs/COMPOSITOR_GOLDENS.md) the test will +// exercise the create/collect/present callbacks instead of falling +// through to the engine's direct render path. +// +// Multi-layer goldens (PlatformViewLayer interleaving) require a Flutter +// bundle that emits PlatformViewSurface widgets — see the doc for the +// staging recipe. + +#include +#include +#include + +#include "app.h" +#include "configuration/configuration.h" +#include "gtest/gtest.h" +#include "logging.h" +#include "unit_test_utils.h" + +TEST(CompositorHeadlessGolden, SingleLayerFastPathParity) { + constexpr int argc = 3; + const char* argv[3] = {"homescreen", "-b", kBundlePath}; + const auto argv_p = reinterpret_cast(&argv); + + const auto configs = Configuration::ParseArgcArgv(argc, argv_p); + + App app(configs); + int ret = app.Loop(); + + using namespace std::chrono_literals; + std::this_thread::sleep_for(2s); + + do { + ret = app.Loop(); + } while (ret > 0); + + const auto test_filename = + utils_get_image_filename(TEST, "compositor_single_layer"); + const auto golden_filename = + utils_get_image_filename(GOLDEN, "compositor_single_layer"); + + const int width = static_cast(configs[0].view.width.value_or(1920)); + const int height = static_cast(configs[0].view.height.value_or(1080)); + +#if SAVE_IMAGE_FOR_COMPARISON + // First-time generation. Inspect the image manually before committing. + utils_write_targa(app.getViewRenderBuf(0), golden_filename, width, height); + GTEST_FAIL() << "UNIT_TEST_SAVE_GOLDENS is on; this run only generates the " + "golden. Re-run with the option off to compare."; +#else + utils_write_targa(app.getViewRenderBuf(0), test_filename, width, height); + const int images_are_equal = utils_images_are_equal( + test_filename, golden_filename, width, height); + EXPECT_EQ(images_are_equal, 1) + << "Compositor single-layer output diverged from baseline. " + "If the divergence is intentional, regenerate the golden with " + "-DUNIT_TEST_SAVE_GOLDENS=ON."; +#endif +} diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 4441ee63..31879ef5 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -86,6 +86,12 @@ if (BUILD_UNIT_TESTS) if (TOOLCHAIN_COMPILE_OPTIONS) target_compile_options(gtest PRIVATE ${TOOLCHAIN_COMPILE_OPTIONS}) endif () + # GCC 15 promotes a signed-conversion warning inside googletest itself + # (gtest.cc: kSize template parameter). Demote to a warning for + # third_party only — the project's own Werror settings stay intact. + target_compile_options(gtest PRIVATE + -Wno-error=sign-conversion + -Wno-error=conversion) target_compile_options(gmock PRIVATE -Wno-error=deprecated-copy) target_compile_options(gtest_main PRIVATE -include cstdint) @@ -93,6 +99,9 @@ if (BUILD_UNIT_TESTS) if (TOOLCHAIN_COMPILE_OPTIONS) target_compile_options(gtest_main PRIVATE ${TOOLCHAIN_COMPILE_OPTIONS}) endif () + target_compile_options(gtest_main PRIVATE + -Wno-error=sign-conversion + -Wno-error=conversion) target_compile_options(gmock PRIVATE -Wno-error=deprecated-copy)