From 44a526c5a191c79c2c4a54e2d53cd076614d8805 Mon Sep 17 00:00:00 2001 From: "kingstom.chen" Date: Mon, 8 Jun 2026 08:41:37 -0600 Subject: [PATCH 1/2] fix(compositor): export composite-write fence for cross-device frame ordering When a client did not bind wp_linux_drm_syncobj_v1 (basic GL/SW clients, many XWayland), the compositor exported each frame's DMA-BUF to the goggles Vulkan consumer with an empty sync_fd and published it anyway. The consumer's submit then had no primitive ordering the foreign compositor device's composite write before goggles sampled the buffer, so under GPU contention it could read a torn/partial/stale frame (P5-1). When a fence was exported it came from the root surface's acquire timeline -- the client's input point, not the composite output write -- and the comment claiming otherwise was inaccurate (P5-2). Have the compositor own a composite-OUTPUT signal timeline, pass it into the present buffer pass when renderer->features.timeline is supported, and export the materialized point as the frame's sync_fd. The consumer path is unchanged: the existing eSyncFd/eTemporary import now waits on the correct composite-write dma_fence regardless of whether the client uses explicit sync. When the renderer lacks timeline support, behavior degrades to the prior implicit-sync path (strictly no worse than before). Builds clean; full test suite passes 12/12. On AMD/radeonsi GLES2 the gate is active and the fence is exported on every frame (0 not-materialized over 29 frames). Tear-free-under-contention validation (RenderDoc/pixel-diff) remains for hardware testing. --- src/compositor/compositor_core.cpp | 23 +++++++++++++++ src/compositor/compositor_present.cpp | 42 +++++++++++++++++++++------ src/compositor/compositor_state.hpp | 5 ++++ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/compositor/compositor_core.cpp b/src/compositor/compositor_core.cpp index 49b43b7..97b5a2d 100644 --- a/src/compositor/compositor_core.cpp +++ b/src/compositor/compositor_core.cpp @@ -10,6 +10,7 @@ extern "C" { #include #include +#include #include #include #include @@ -197,6 +198,23 @@ auto CompositorState::create_compositor() -> Result { } } + // Own a composite-OUTPUT signal timeline so every exported frame carries a producer-write + // fence (P5), independent of whether the client bound wp_linux_drm_syncobj_v1. Best-effort: + // absence degrades to the status-quo implicit path (see compositor_present.cpp). + if (renderer->features.timeline && drm_fd >= 0) { + present_signal_timeline = wlr_drm_syncobj_timeline_create(drm_fd); + present_timeline_supported = (present_signal_timeline != nullptr); + if (present_timeline_supported) { + GOGGLES_LOG_INFO("Compositor: composite-write signal timeline enabled"); + } else { + GOGGLES_LOG_WARN( + "Compositor: signal timeline creation failed; implicit signal sync fallback"); + } + } else { + GOGGLES_LOG_WARN("Compositor: renderer lacks features.timeline; composite-write fence " + "export unavailable (implicit signal sync fallback)"); + } + return {}; } @@ -382,6 +400,11 @@ void CompositorState::teardown() { allocator = nullptr; } + if (present_signal_timeline) { + wlr_drm_syncobj_timeline_unref(present_signal_timeline); + present_signal_timeline = nullptr; + } + if (renderer) { wlr_renderer_destroy(renderer); renderer = nullptr; diff --git a/src/compositor/compositor_present.cpp b/src/compositor/compositor_present.cpp index ef6f1f6..05b5b24 100644 --- a/src/compositor/compositor_present.cpp +++ b/src/compositor/compositor_present.cpp @@ -8,6 +8,7 @@ #include extern "C" { +#include #include #include #include @@ -653,7 +654,17 @@ bool CompositorState::render_surface_to_frame(const InputTarget& target) { return false; } - wlr_render_pass* pass = wlr_renderer_begin_buffer_pass(renderer, buffer, nullptr); + uint64_t pass_point = 0; + wlr_render_pass* pass = nullptr; + if (present_timeline_supported) { + pass_point = ++present_signal_point; + wlr_buffer_pass_options opts{}; + opts.signal_timeline = present_signal_timeline; + opts.signal_point = pass_point; + pass = wlr_renderer_begin_buffer_pass(renderer, buffer, &opts); + } else { + pass = wlr_renderer_begin_buffer_pass(renderer, buffer, nullptr); + } if (!pass) { wlr_buffer_unlock(buffer); return false; @@ -719,18 +730,31 @@ bool CompositorState::render_surface_to_frame(const InputTarget& target) { frame.image.handle = std::move(dup_fd); frame.frame_number = ++presented_frame_number; - // Export the acquire fence from the root surface so Vulkan waits on compositor writes. - wlr_linux_drm_syncobj_surface_v1_state* syncobj_state = - wlr_linux_drm_syncobj_v1_get_surface_state(root_surface); - if (syncobj_state && syncobj_state->acquire_timeline) { - int sync_file = wlr_drm_syncobj_timeline_export_sync_file(syncobj_state->acquire_timeline, - syncobj_state->acquire_point); - if (sync_file >= 0) { - frame.sync_fd = util::UniqueFd{sync_file}; + // Export the composite OUTPUT write fence so goggles waits on the compositor's render + // completion. The signal timeline is the compositor's own, independent of whether the client + // bound wp_linux_drm_syncobj_v1, so every client gets a producer-write fence when supported. + if (present_timeline_supported) { + bool materialized = false; + if (wlr_drm_syncobj_timeline_check(present_signal_timeline, pass_point, + DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE, &materialized) && + materialized) { + int sync_file = + wlr_drm_syncobj_timeline_export_sync_file(present_signal_timeline, pass_point); + if (sync_file >= 0) { + frame.sync_fd = util::UniqueFd{sync_file}; + } else { + GOGGLES_LOG_WARN("Composite-write sync_file export failed (point {})", pass_point); + } + } else { + GOGGLES_LOG_WARN("Composite-write point {} not materialized at export; frame published " + "without producer fence", + pass_point); } } // Release stays tied to the exported buffer so wlroots can retire it after import completes. + wlr_linux_drm_syncobj_surface_v1_state* syncobj_state = + wlr_linux_drm_syncobj_v1_get_surface_state(root_surface); if (syncobj_state && syncobj_state->release_timeline) { wlr_linux_drm_syncobj_v1_state_signal_release_with_buffer(syncobj_state, buffer); } diff --git a/src/compositor/compositor_state.hpp b/src/compositor/compositor_state.hpp index 623a7b3..b463689 100644 --- a/src/compositor/compositor_state.hpp +++ b/src/compositor/compositor_state.hpp @@ -26,6 +26,7 @@ struct wlr_allocator; struct wlr_backend; struct wlr_buffer; struct wlr_compositor; +struct wlr_drm_syncobj_timeline; struct wlr_layer_shell_v1; struct wlr_linux_drm_syncobj_manager_v1; struct wlr_output; @@ -61,6 +62,7 @@ using ::wlr_allocator; using ::wlr_backend; using ::wlr_buffer; using ::wlr_compositor; +using ::wlr_drm_syncobj_timeline; using ::wlr_linux_drm_syncobj_manager_v1; using ::wlr_output; using ::wlr_output_layout; @@ -166,6 +168,9 @@ struct CompositorState { std::vector> layer_hooks; wlr_layer_shell_v1* layer_shell = nullptr; wlr_linux_drm_syncobj_manager_v1* syncobj_manager = nullptr; + wlr_drm_syncobj_timeline* present_signal_timeline = nullptr; // composite-OUTPUT write timeline + uint64_t present_signal_point = 0; // monotonic, compositor-thread only + bool present_timeline_supported = false; wlr_drm_format present_format{}; std::string wayland_socket_name; mutable std::mutex hooks_mutex; From 684283d0d7f11cf8c17e754d0ced8e67a7e2192a Mon Sep 17 00:00:00 2001 From: "kingstom.chen" Date: Mon, 8 Jun 2026 09:05:59 -0600 Subject: [PATCH 2/2] chore: sync pixi.lock with manifest (libxkbcommon 1.13.1 -> 1.13.2) The committed lock referenced libxkbcommon 1.13.1, which no longer matches the manifest resolution, so `pixi install --locked` (run by CI's Setup Pixi on a clean checkout) reports "lock-file not up-to-date with the workspace" and every CI job fails before building. This pre-exists on main and is unrelated to the code change; included here so this PR's CI can pass. Regenerated with the CI-pinned pixi v0.65.0; only a transitive patch bump. --- pixi.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pixi.lock b/pixi.lock index b2b1eaf..e595a0a 100644 --- a/pixi.lock +++ b/pixi.lock @@ -100,7 +100,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libvulkan-loader-1.4.328.1-h5279c79_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-hca5e8e5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.2-hf2a90c1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.2-h031cc0b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda @@ -1725,9 +1725,9 @@ packages: purls: [] size: 100393 timestamp: 1702724383534 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.1-hca5e8e5_0.conda - sha256: d2195b5fbcb0af1ff7b345efdf89290c279b8d1d74f325ae0ac98148c375863c - md5: 2bca1fbb221d9c3c8e3a155784bbc2e9 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.13.2-hca5e8e5_0.conda + sha256: 046f2ff4acebd8729fac03e99c8c307dfb48b6a32894ba8c11576e78f6e76e43 + md5: dc8b067e22b414172bedd8e3f03f3c95 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 @@ -1740,8 +1740,8 @@ packages: license: MIT/X11 Derivative license_family: MIT purls: [] - size: 837922 - timestamp: 1764794163823 + size: 851166 + timestamp: 1780213397575 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.2-h031cc0b_0.conda sha256: a9612f88139197b2777a00325c72d872507e70d4f4111021f65e55797f97de67 md5: 672c49f67192f0a7c2fa55986219d197 @@ -2657,7 +2657,7 @@ packages: - wayland - wayland >=1.25.0,<2.0a0 - libxkbcommon - - libxkbcommon >=1.13.1,<2.0a0 + - libxkbcommon >=1.13.2,<2.0a0 - pixman - pixman >=0.46.4,<1.0a0 license: MIT