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
12 changes: 8 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,14 @@ not collected by CI.

Freeze and Zoom fail when the active output does not advertise a current `wl_output` mode.
Changes confined to another monitor preserve direct captures and installed active-output sources.
Portal captures discard stale desktop snapshots and retry once after complete geometry settles,
while preserving the original output identity and request. A second change during the retry or an
active-output switch is terminal. Board PDF desktop captures retain a full-desktop generation check,
including changes to other monitors even when the screenshot dimensions stay the same.
Capture preflight retries once on the selected backend after its required geometry settles,
while preserving the original request and keyboard policy. Direct backends require only active-output
geometry; portal captures require complete desktop geometry and also retry stale desktop snapshots.
Known output identities stay bound; requests started before
output discovery bind to the first output observed during the bounded wait. A second stale capture,
loss of a bound output, or an active-output switch is terminal. Board PDF desktop captures retain a
full-desktop generation check, including changes to other monitors even when the screenshot
dimensions stay the same.

`dotnet run tools/wayscriber.cs --no-build -- report code-health` reports navigational
maintainability metrics. Its CI artifact is
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,23 @@ ls -la ~/.config/wayscriber/config.toml
RUST_LOG=info wayscriber --active # watch for TOML errors
```

### Freeze and Zoom capture logs

`--active` and `--daemon` write dated logs to `$XDG_DATA_HOME/wayscriber/logs/`,
normally `~/.local/share/wayscriber/logs/`. Daemon-spawned overlays also write there;
forwarding their stdout or stderr is not required. `WAYSCRIBER_LOG_FILE` overrides
the log destination.

At the default `info` level, `capture.output` records surface enter/leave events with
previous and current output IDs and names. `capture.preflight` records Freeze/Zoom,
the barrier ID, saved and current output/layout tokens, and retry decisions. Monitor
metadata updates are available at `debug` level.

For full output metadata and before/after geometry, set
`RUST_LOG=info,wayscriber::capture_diagnostics=trace`
in the daemon's startup environment. Restart the daemon after changing its environment
so newly spawned overlays inherit the filter.

### Environment variables

Common toggles:
Expand Down
3 changes: 2 additions & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2683,7 +2683,8 @@ These override behavior at runtime. Bool-ish values treat anything except `0`, `
- `WAYSCRIBER_XDG_OUTPUT=...` forces GNOME fallback overlays onto a specific output (overrides `ui.preferred_output`)
- `WAYSCRIBER_XDG_FULLSCREEN=1` requests fullscreen GNOME fallback overlays (overrides `ui.xdg_fullscreen`)
- `WAYSCRIBER_XDG_FULLSCREEN_FORCE=1` bypasses the GNOME opacity safety check
- `RUST_LOG=info` enables Rust logging (default: unset; use `wayscriber=debug` for app-level logs)
- `RUST_LOG` sets the Rust log filter (default when unset: `info`; use `wayscriber=debug` for app-level logs or `info,wayscriber::capture_diagnostics=trace` for capture diagnostics)
- `WAYSCRIBER_LOG_FILE=/path/to/wayscriber.log` overrides the file log destination for `--active` and `--daemon` (default: dated files in `$XDG_DATA_HOME/wayscriber/logs/`, normally `~/.local/share/wayscriber/logs/`)

## Troubleshooting

Expand Down
3 changes: 2 additions & 1 deletion src/backend/wayland/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
- `handlers/` translates Wayland/Smithay callbacks into state and input calls.
- `state.rs` is the central live runtime state root; `state/` contains state helpers for boards, buffers, damage, clipboard paste, toolbar plumbing, zoom, onboarding, and export handoff.
- `toolbar/` owns runtime toolbar layout, hit testing, rows, surfaces, rendering, and widgets.
- `surface.rs` owns the main overlay surface, its buffers, and its shell role. `surface/output_membership.rs` selects the current output from compositor enter/leave, explicit output requests, and the provisional XDG configure guess that the first enter replaces; `surface/placement.rs` owns layer policy and bounded XDG frozen-fullscreen placement.
- `portal_raster.rs` validates complete portal desktop snapshots, crops the active logical rectangle, and resamples it to transformed native output pixels. `frozen_geometry.rs` keeps active viewport validity separate from full desktop validity.
- `capture_preflight.rs` retains backend-specific admission, shared active-output/full-desktop generation bookkeeping, typed preflight failures, and the bounded portal layout settling/retry policy. Board PDF desktop contexts use the full-desktop generation; direct capture and installed sources use the active-output generation.
- `capture_preflight.rs` retains backend-specific admission, shared active-output/full-desktop generation bookkeeping, and typed preflight failures. `capture_preflight/retry.rs` owns bounded layout settling and re-admission on the retained backend. Board PDF desktop contexts use the full-desktop generation; direct capture and installed sources use the active-output generation.
- `clipboard/`, `frozen/`, `zoom/`, and runtime session helpers integrate external compositor, clipboard, capture, and persistence behavior.

## Invariants
Expand Down
136 changes: 133 additions & 3 deletions src/backend/wayland/backend/event_loop/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(super) fn poll_capture_deadlines(
) {
// Re-admit retries after dispatch, before GTK synchronization and rendering.
// This lets a fresh GTK generation be published in the same iteration.
state.poll_portal_layout_retries(now);
state.poll_layout_retries(now);
state.poll_overlay_capture_barrier_timeout(now);
if let Some(backend) = state.frozen.take_timed_out_direct_capture(now) {
warn!("{backend:?} frozen capture timed out; trying the next backend");
Expand All @@ -53,8 +53,8 @@ pub(super) fn capture_timeout(
render_delay: Duration,
) -> Option<Duration> {
[
state.frozen.portal_layout_retry_timeout(now),
state.zoom.portal_layout_retry_timeout(now),
state.frozen.layout_retry_timeout(now),
state.zoom.layout_retry_timeout(now),
state.overlay_capture_barrier_timeout(now, render_delay),
state.frozen.direct_capture_timeout(now),
state.frozen.portal_timeout(now),
Expand Down Expand Up @@ -716,6 +716,136 @@ fn handle_capture_manager_failure(
mod tests {
use super::*;
use crate::backend::wayland::acquisition::ScreenAcquisitionRegistry;
use crate::backend::wayland::handlers::test_support::{CaptureFixtureBackend, HandlerFixture};

#[test]
fn toolbar_capture_retries_output_discovery_through_both_hidden_frames() {
use crate::ui::toolbar::ToolbarEvent;

for (event, backend) in [
(ToolbarEvent::ToggleFreeze, CaptureFixtureBackend::Portal),
(ToolbarEvent::ZoomIn, CaptureFixtureBackend::Portal),
(
ToolbarEvent::ToggleFreeze,
CaptureFixtureBackend::ExtImageCopy,
),
(
ToolbarEvent::ToggleFreeze,
CaptureFixtureBackend::WlrScreencopy,
),
(ToolbarEvent::ZoomIn, CaptureFixtureBackend::WlrScreencopy),
] {
for enter_before_frame in [false, true] {
toolbar_output_discovery(event.clone(), enter_before_frame, backend);
}
}
}

fn toolbar_output_discovery(
event: crate::ui::toolbar::ToolbarEvent,
enter_before_frame: bool,
backend: CaptureFixtureBackend,
) {
use smithay_client_toolkit::compositor::CompositorHandler;

let mut fixture =
HandlerFixture::with_capture_output(crate::config::Config::default(), backend);
let output = fixture.complete_output_metadata("DP-3");
let qh = fixture.queue.handle();
let surface = fixture.state.surface.wl_surface().unwrap().clone();

fixture
.state
.handle_toolbar_event(event.clone(), Some(&fixture.conn), Some(&qh));
handle_pending_actions(&mut fixture.state, &qh);

let acquisition = fixture.state.acquisition.slot();
let zoom_id = fixture.state.zoom.current_capture_id();
let reason = fixture.state.suppression.reason();
let keyboard_passthrough = fixture
.state
.suppression
.keyboard_passthrough_requested(false);
let freezing = matches!(event, crate::ui::toolbar::ToolbarEvent::ToggleFreeze);
assert_eq!(
reason,
if freezing {
OverlaySuppression::Frozen
} else {
OverlaySuppression::Zoom
}
);
assert_eq!(acquisition.is_some(), freezing);
assert_eq!(zoom_id.is_some(), !freezing);

if enter_before_frame {
fixture
.state
.surface_enter(&fixture.conn, &qh, &surface, &output);
}
complete_hidden_frame(&mut fixture, &qh);
assert_eq!(fixture.state.frozen.has_layout_retry(), freezing);
assert_eq!(fixture.state.zoom.has_layout_retry(), !freezing);

if !enter_before_frame {
fixture
.state
.surface_enter(&fixture.conn, &qh, &surface, &output);
}

// Repeated KWin enter notifications during settling must retain ownership.
fixture
.state
.surface_enter(&fixture.conn, &qh, &surface, &output);
assert_eq!(fixture.state.acquisition.slot(), acquisition);
assert_eq!(fixture.state.zoom.current_capture_id(), zoom_id);

let now = Instant::now();
poll_capture_deadlines(&mut fixture.state, &qh, now);
poll_capture_deadlines(&mut fixture.state, &qh, now + Duration::from_millis(150));
assert!(!fixture.state.frozen.has_layout_retry());
assert!(!fixture.state.zoom.has_layout_retry());
assert_eq!(fixture.state.suppression.reason(), reason);
assert_eq!(
fixture
.state
.suppression
.keyboard_passthrough_requested(false),
keyboard_passthrough
);
complete_hidden_frame(&mut fixture, &qh);

// Both barriers completed and the worker was admitted with the original request.
// The fixture's current-thread Tokio runtime is never polled, so no host portal runs.
assert_eq!(fixture.state.acquisition.slot(), acquisition);
assert_eq!(fixture.state.zoom.current_capture_id(), zoom_id);
assert_eq!(fixture.state.suppression.reason(), reason);
assert_capture_started(&fixture.state, freezing);
fixture.assert_direct_capture_started();

fixture.state.frozen.cancel(&mut fixture.state.input_state);
fixture.state.zoom.abort_capture();
}

fn assert_capture_started(state: &WaylandState, freezing: bool) {
assert!(!state.frozen.preflight_pending());
assert!(!state.zoom.preflight_pending());
assert_eq!(state.frozen.is_in_progress(), freezing);
assert_eq!(state.zoom.is_in_progress(), !freezing);
assert!(!state.frozen.has_layout_retry());
assert!(!state.zoom.has_layout_retry());
}

fn complete_hidden_frame(
fixture: &mut crate::backend::wayland::handlers::test_support::HandlerFixture,
qh: &wayland_client::QueueHandle<WaylandState>,
) {
assert!(matches!(
fixture.state.render(qh).unwrap(),
crate::backend::wayland::state::RenderOutcome::Committed { .. }
));
fixture.complete_main_frame();
}

fn record(
owner: ScreenAcquisitionOwner,
Expand Down
2 changes: 1 addition & 1 deletion src/backend/wayland/backend/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub(super) fn create_overlay_surface(
// Commit the surface
layer_surface.commit();

state.surface.set_layer_surface(layer_surface);
state.surface.set_layer_surface(layer_surface, None);
state
.focus
.set_keyboard_interactivity(Some(desired_keyboard_mode));
Expand Down
Loading
Loading