[compositor] golden-frame harness scaffolding

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 <joel.winarske@gmail.com>
This commit is contained in:
Joel Winarske
2026-04-14 20:20:05 -07:00
parent 9067cba848
commit 9a644a284e
5 changed files with 266 additions and 0 deletions
+125
View File
@@ -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: <Factory<...>>{},
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.
+1
View File
@@ -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)
@@ -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}
)
@@ -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 <chrono>
#include <stdexcept>
#include <thread>
#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<char**>(&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<int>(configs[0].view.width.value_or(1920));
const int height = static_cast<int>(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
}
+9
View File
@@ -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)