Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/compositor/compositor_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
extern "C" {
#include <wlr/backend/headless.h>
#include <wlr/render/allocator.h>
#include <wlr/render/drm_syncobj.h>
#include <wlr/render/swapchain.h>
#include <wlr/render/wlr_renderer.h>
#include <wlr/types/wlr_compositor.h>
Expand Down Expand Up @@ -197,6 +198,23 @@ auto CompositorState::create_compositor() -> Result<void> {
}
}

// 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)");
}
Comment on lines +201 to +216

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Stale timeline support flag 🐞 Bug ☼ Reliability

CompositorState::create_compositor() only updates present_timeline_supported when
(renderer->features.timeline && drm_fd >= 0) is true, while teardown() clears
present_signal_timeline without resetting the flag. If the compositor is stopped/started and the
second init can’t create a timeline, render_surface_to_frame() may treat timeline support as
enabled and pass/use a null present_signal_timeline, risking a crash.
Agent Prompt
### Issue description
`present_timeline_supported` can become stale across compositor restarts: `teardown()` nulls `present_signal_timeline` but does not reset `present_timeline_supported`, and `create_compositor()` only assigns `present_timeline_supported` inside one branch. This can leave `present_timeline_supported == true` while `present_signal_timeline == nullptr`, which is then used in `render_surface_to_frame()`.

### Issue Context
`CompositorServer` exposes public `start()`/`stop()` and `start()` re-runs the initialization sequence on the same `CompositorState` instance.

### Fix Focus Areas
- src/compositor/compositor_core.cpp[186-216]
- src/compositor/compositor_core.cpp[393-411]
- src/compositor/compositor_present.cpp[657-667]

### Implementation notes
- In `create_compositor()`, proactively reset the composite-write timeline state before feature checks:
  - `present_timeline_supported = false;`
  - `present_signal_timeline = nullptr;` (and/or unref any existing timeline defensively)
  - optionally reset `present_signal_point = 0` when creating a new timeline.
- In the `else` path (no support), explicitly set `present_timeline_supported = false`.
- In `teardown()`, also set `present_timeline_supported = false` (and optionally reset `present_signal_point`).
- In `render_surface_to_frame()`, harden the condition to avoid null use (e.g., require both `present_timeline_supported` and `present_signal_timeline != nullptr`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


return {};
}

Expand Down Expand Up @@ -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;
Expand Down
42 changes: 33 additions & 9 deletions src/compositor/compositor_present.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <numeric>

extern "C" {
#include <libdrm/drm.h>
#include <wlr/render/allocator.h>
#include <wlr/render/drm_syncobj.h>
#include <wlr/render/pass.h>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions src/compositor/compositor_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -166,6 +168,9 @@ struct CompositorState {
std::vector<std::unique_ptr<LayerSurfaceHooks>> 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;
Expand Down
Loading