338 lines
14 KiB
Diff
338 lines
14 KiB
Diff
--- a/sway/desktop/output.c
|
|
+++ b/sway/desktop/output.c
|
|
@@ -1,4 +1,9 @@
|
|
#include <assert.h>
|
|
+#include <drm_fourcc.h>
|
|
+#include <fcntl.h>
|
|
+#include <limits.h>
|
|
+#include <stdio.h>
|
|
+#include <unistd.h>
|
|
#include <inttypes.h>
|
|
#include <stdlib.h>
|
|
#include <strings.h>
|
|
@@ -8,6 +13,7 @@
|
|
#include <wlr/backend/headless.h>
|
|
#include <wlr/render/swapchain.h>
|
|
#include <wlr/render/wlr_renderer.h>
|
|
+#include <wlr/render/wlr_texture.h>
|
|
#include <wlr/types/wlr_buffer.h>
|
|
#include <wlr/types/wlr_alpha_modifier_v1.h>
|
|
#include <wlr/types/wlr_gamma_control_v1.h>
|
|
@@ -431,6 +437,39 @@
|
|
}
|
|
}
|
|
|
|
+static struct wlr_buffer *bi_expected_buffer;
|
|
+static uint64_t bi_expected_id;
|
|
+
|
|
+// WHY: a headless present may arrive after output_state_finish.
|
|
+// CONTRACT: one lab-owned reference at most; timeout and output teardown
|
|
+// release even when no present event arrives. The lock prevents slot reuse.
|
|
+// INVARIANT: bi_hold.buffer is non-NULL exactly while the probe owns a lock.
|
|
+static struct {
|
|
+ struct wlr_buffer *buffer;
|
|
+ struct wlr_output *output;
|
|
+ struct wl_event_source *timer;
|
|
+ uint32_t seq;
|
|
+ uint64_t id;
|
|
+ uint64_t locked_ns;
|
|
+ int scroll;
|
|
+} bi_hold;
|
|
+static unsigned bi_reread_phase;
|
|
+
|
|
+static void bi_release_hold(const char *reason) {
|
|
+ if (!bi_hold.buffer) return;
|
|
+ sway_log(SWAY_INFO, "BI_RELEASE id=%" PRIu64 " reason=%s age_ns=%" PRIu64,
|
|
+ bi_hold.id, reason, presentation_probe_now_ns() - bi_hold.locked_ns);
|
|
+ if (bi_hold.timer) wl_event_source_remove(bi_hold.timer);
|
|
+ wlr_buffer_unlock(bi_hold.buffer);
|
|
+ memset(&bi_hold, 0, sizeof(bi_hold));
|
|
+}
|
|
+
|
|
+static int bi_timeout_hold(void *data) {
|
|
+ bi_release_hold("timeout");
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+
|
|
static void presentation_probe_handle_commit(struct wl_listener *listener, void *data) {
|
|
struct sway_output *output = wl_container_of(listener, output, presentation_probe_commit);
|
|
const struct wlr_output_event_commit *event = data;
|
|
@@ -441,6 +480,145 @@
|
|
(uint64_t)event->when.tv_sec * UINT64_C(1000000000) + event->when.tv_nsec,
|
|
!!(event->state->committed & WLR_OUTPUT_STATE_BUFFER));
|
|
presentation_probe_scene(output);
|
|
+ if (bi_expected_buffer) {
|
|
+ sway_log(SWAY_INFO, "BI_EVENT_BUFFER id=%" PRIu64
|
|
+ " output=%s commit_seq=%" PRIu32 " same=%d has_buffer=%d",
|
|
+ bi_expected_id, output->wlr_output->name,
|
|
+ output->wlr_output->commit_seq,
|
|
+ event->state->buffer == bi_expected_buffer,
|
|
+ !!(event->state->committed & WLR_OUTPUT_STATE_BUFFER));
|
|
+ }
|
|
+}
|
|
+
|
|
+// WHY: identify bytes in the actual output-state buffer, not in a screenshot.
|
|
+// CONTRACT: only the private headless lab sets BI_OUTPUT_DIR; all copies and
|
|
+// accesses end before the state is finished. The caller owns one extra lock
|
|
+// until the commit result has been recorded.
|
|
+// INVARIANT: no data pointer or texture survives this function; snapshots
|
|
+// are limited to 80 * 1.92 MiB, and failed reads never become fingerprints.
|
|
+// The commit event is synchronous inside wlr_output_commit_state(); these
|
|
+// borrowed comparison fields are cleared before the repaint call returns.
|
|
+static uint64_t bi_next_id;
|
|
+static unsigned bi_snapshots;
|
|
+static struct wlr_buffer *bi_observe_buffer(struct sway_output *output,
|
|
+ const struct wlr_output_state *pending, uint64_t *id) {
|
|
+ const char *dir = getenv("BI_OUTPUT_DIR");
|
|
+ if (!dir || strcmp(output->wlr_output->name, "HEADLESS-1") != 0 ||
|
|
+ !(pending->committed & WLR_OUTPUT_STATE_BUFFER) || !pending->buffer) {
|
|
+ return NULL;
|
|
+ }
|
|
+ struct sway_workspace *ws = output->current.active_workspace;
|
|
+ if (!ws || ws->current.layout != L_MOSAIC || !ws->mosaic ||
|
|
+ !ws->mosaic->rows->length || bi_snapshots >= 80) {
|
|
+ return NULL;
|
|
+ }
|
|
+ struct mosaic_row *row = ws->mosaic->rows->items[0];
|
|
+ bool transient = getenv("BI_INCLUDE_TRANSIENT") &&
|
|
+ row->state.scroll_x == 1760 && ws->layers.tiling->node.x == -1120;
|
|
+ if (row->state.scroll_x != 400 && row->state.scroll_x != 1280 &&
|
|
+ !transient) {
|
|
+ return NULL;
|
|
+ }
|
|
+ struct wlr_buffer *buffer = pending->buffer;
|
|
+ if (buffer->width != 800 || buffer->height != 600) {
|
|
+ return NULL;
|
|
+ }
|
|
+ *id = ++bi_next_id;
|
|
+ bi_snapshots++;
|
|
+ wlr_buffer_lock(buffer);
|
|
+ uint64_t start = presentation_probe_now_ns();
|
|
+ struct wlr_dmabuf_attributes dmabuf = {0};
|
|
+ bool has_dmabuf = wlr_buffer_get_dmabuf(buffer, &dmabuf);
|
|
+ sway_log(SWAY_INFO, "BI_BUFFER id=%" PRIu64 " output=%s ns=%" PRIu64
|
|
+ " width=%d height=%d state=0x%x dmabuf=%d dmabuf_format=0x%x"
|
|
+ " src=%.1f,%.1f,%.1f,%.1f dst=%d,%d,%d,%d",
|
|
+ *id, output->wlr_output->name, start, buffer->width, buffer->height,
|
|
+ pending->committed, has_dmabuf, has_dmabuf ? dmabuf.format : 0,
|
|
+ pending->buffer_src_box.x, pending->buffer_src_box.y,
|
|
+ pending->buffer_src_box.width, pending->buffer_src_box.height,
|
|
+ pending->buffer_dst_box.x, pending->buffer_dst_box.y,
|
|
+ pending->buffer_dst_box.width, pending->buffer_dst_box.height);
|
|
+ void *direct = NULL;
|
|
+ uint32_t direct_fmt = 0;
|
|
+ size_t direct_stride = 0;
|
|
+ bool direct_ok = wlr_buffer_begin_data_ptr_access(buffer,
|
|
+ WLR_BUFFER_DATA_PTR_ACCESS_READ, &direct, &direct_fmt, &direct_stride);
|
|
+ sway_log(SWAY_INFO, "BI_DIRECT id=%" PRIu64 " available=%d format=0x%x stride=%zu",
|
|
+ *id, direct_ok, direct_fmt, direct_stride);
|
|
+ if (direct_ok) {
|
|
+ wlr_buffer_end_data_ptr_access(buffer);
|
|
+ }
|
|
+ const size_t bytes = 800u * 600u * 4u;
|
|
+ unsigned char *pixels = malloc(bytes);
|
|
+ if (!pixels) {
|
|
+ sway_log(SWAY_INFO, "BI_READ id=%" PRIu64 " status=ALLOC_FAILED", *id);
|
|
+ return buffer;
|
|
+ }
|
|
+ struct wlr_texture *texture = wlr_texture_from_buffer(output->wlr_output->renderer, buffer);
|
|
+ if (!texture) {
|
|
+ sway_log(SWAY_INFO, "BI_READ id=%" PRIu64 " status=TEXTURE_UNAVAILABLE", *id);
|
|
+ free(pixels);
|
|
+ return buffer;
|
|
+ }
|
|
+ struct wlr_texture_read_pixels_options opts = {
|
|
+ .data = pixels, .format = DRM_FORMAT_XRGB8888, .stride = 800u * 4u,
|
|
+ };
|
|
+ bool read_ok = wlr_texture_read_pixels(texture, &opts);
|
|
+ wlr_texture_destroy(texture);
|
|
+ if (!read_ok) {
|
|
+ sway_log(SWAY_INFO, "BI_READ id=%" PRIu64 " status=READ_FAILED", *id);
|
|
+ free(pixels);
|
|
+ return buffer;
|
|
+ }
|
|
+ char path[PATH_MAX];
|
|
+ int n = snprintf(path, sizeof(path), "%s/buffer-%" PRIu64 ".xrgb", dir, *id);
|
|
+ if (n < 0 || (size_t)n >= sizeof(path)) {
|
|
+ sway_log(SWAY_INFO, "BI_READ id=%" PRIu64 " status=PATH_TOO_LONG", *id);
|
|
+ free(pixels);
|
|
+ return buffer;
|
|
+ }
|
|
+ int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
|
|
+ size_t off = 0;
|
|
+ if (fd >= 0) {
|
|
+ while (off < bytes) {
|
|
+ ssize_t written = write(fd, pixels + off, bytes - off);
|
|
+ if (written <= 0) break;
|
|
+ off += (size_t)written;
|
|
+ }
|
|
+ close(fd);
|
|
+ }
|
|
+ if (off != bytes && fd >= 0) unlink(path);
|
|
+ sway_log(SWAY_INFO, "BI_READ id=%" PRIu64 " status=%s bytes=%zu"
|
|
+ " format=0x%x stride=%u duration_ns=%" PRIu64,
|
|
+ *id, off == bytes ? "READ" : "WRITE_FAILED", off,
|
|
+ DRM_FORMAT_XRGB8888, 800u * 4u, presentation_probe_now_ns() - start);
|
|
+ free(pixels);
|
|
+ return buffer;
|
|
+}
|
|
+
|
|
+// WHY: retain the scene state on each side of build and commit separately.
|
|
+// CONTRACT: these are ordered main-thread observations, not an atomic
|
|
+// transaction snapshot or proof that the later buffer contains these pixels.
|
|
+static void bi_stage(struct sway_output *output, const char *phase,
|
|
+ const struct wlr_output_state *state) {
|
|
+ struct sway_workspace *ws = output->current.active_workspace;
|
|
+ struct wlr_box clip = {0};
|
|
+ bool clip_valid = ws && wlr_scene_tree_get_clip(ws->layers.tiling, &clip);
|
|
+ double scroll = -1;
|
|
+ if (ws && ws->current.layout == L_MOSAIC && ws->mosaic &&
|
|
+ ws->mosaic->rows->length) {
|
|
+ struct mosaic_row *row = ws->mosaic->rows->items[0];
|
|
+ scroll = row->state.scroll_x;
|
|
+ }
|
|
+ sway_log(SWAY_INFO, "BI_STAGE phase=%s ns=%" PRIu64
|
|
+ " output=%s workspace=%zu layout=%d scroll=%.0f tile_scene=%d,%d"
|
|
+ " clip_valid=%d clip=%d,%d,%d,%d has_buffer=%d",
|
|
+ phase, presentation_probe_now_ns(), output->wlr_output->name,
|
|
+ ws ? ws->node.id : 0, ws ? (int)ws->current.layout : -1, scroll,
|
|
+ ws ? ws->layers.tiling->node.x : 0,
|
|
+ ws ? ws->layers.tiling->node.y : 0, clip_valid,
|
|
+ clip.x, clip.y, clip.width, clip.height,
|
|
+ state && (state->committed & WLR_OUTPUT_STATE_BUFFER) && state->buffer);
|
|
}
|
|
|
|
static int output_repaint_timer_handler(void *data) {
|
|
@@ -462,6 +640,7 @@
|
|
return 0;
|
|
}
|
|
|
|
+ bi_stage(output, "PRE_BUILD", NULL);
|
|
struct wlr_output_state pending;
|
|
wlr_output_state_init(&pending);
|
|
if (!wlr_scene_output_build_state(output->scene_output, &pending, &opts)) {
|
|
@@ -469,6 +648,7 @@
|
|
return 0;
|
|
}
|
|
|
|
+ bi_stage(output, "POST_BUILD", &pending);
|
|
if (output_can_tear(output)) {
|
|
pending.tearing_page_flip = true;
|
|
|
|
@@ -479,7 +659,47 @@
|
|
}
|
|
}
|
|
|
|
+ // INVARIANT: release an older probe lock before taking a new one.
|
|
+ bi_release_hold("superseded");
|
|
+ uint64_t bi_id = 0;
|
|
+ struct wlr_buffer *bi_locked = bi_observe_buffer(output, &pending, &bi_id);
|
|
+ bi_expected_buffer = bi_locked;
|
|
+ bi_expected_id = bi_id;
|
|
bool commit_ok = wlr_output_commit_state(output->wlr_output, &pending);
|
|
+ bi_expected_buffer = NULL;
|
|
+ bi_expected_id = 0;
|
|
+ if (commit_ok) bi_stage(output, "COMMIT_ACCEPTED", &pending);
|
|
+ if (bi_locked) {
|
|
+ sway_log(SWAY_INFO, "BI_COMMIT id=%" PRIu64 " accepted=%d output=%s"
|
|
+ " commit_seq=%" PRIu32 " ns=%" PRIu64, bi_id, commit_ok,
|
|
+ output->wlr_output->name, output->wlr_output->commit_seq,
|
|
+ presentation_probe_now_ns());
|
|
+ if (!commit_ok) {
|
|
+ wlr_buffer_unlock(bi_locked);
|
|
+ sway_log(SWAY_INFO, "BI_RELEASE id=%" PRIu64 " reason=commit_rejected", bi_id);
|
|
+ } else {
|
|
+ struct wl_event_source *timer = wl_event_loop_add_timer(
|
|
+ output->wlr_output->event_loop, bi_timeout_hold, NULL);
|
|
+ if (!timer || wl_event_source_timer_update(timer, 2000) < 0) {
|
|
+ if (timer) wl_event_source_remove(timer);
|
|
+ wlr_buffer_unlock(bi_locked);
|
|
+ sway_log(SWAY_INFO, "BI_RELEASE id=%" PRIu64 " reason=timer_unavailable", bi_id);
|
|
+ } else {
|
|
+ bi_hold.buffer = bi_locked;
|
|
+ bi_hold.output = output->wlr_output;
|
|
+ bi_hold.timer = timer;
|
|
+ bi_hold.seq = output->wlr_output->commit_seq;
|
|
+ bi_hold.id = bi_id;
|
|
+ bi_hold.locked_ns = presentation_probe_now_ns();
|
|
+ struct sway_workspace *ws = output->current.active_workspace;
|
|
+ struct mosaic_row *row = ws && ws->mosaic && ws->mosaic->rows->length ?
|
|
+ ws->mosaic->rows->items[0] : NULL;
|
|
+ bi_hold.scroll = row ? row->state.scroll_x : -1;
|
|
+ sway_log(SWAY_INFO, "BI_LOCKED id=%" PRIu64 " commit_seq=%" PRIu32,
|
|
+ bi_id, bi_hold.seq);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
struct timespec temporal_now;
|
|
clock_gettime(CLOCK_MONOTONIC, &temporal_now);
|
|
sway_log(SWAY_INFO, "TEMPORAL_OUTPUT_COMMIT ordinal=%" PRIu64
|
|
@@ -614,6 +834,9 @@
|
|
}
|
|
|
|
static void begin_destroy(struct sway_output *output) {
|
|
+ if (bi_hold.buffer && bi_hold.output == output->wlr_output) {
|
|
+ bi_release_hold("output_destroy");
|
|
+ }
|
|
|
|
wl_list_remove(&output->layout_destroy.link);
|
|
wl_list_remove(&output->destroy.link);
|
|
@@ -664,6 +887,57 @@
|
|
output_event->when.tv_nsec,
|
|
output_event->presented, output_event->seq,
|
|
output_event->refresh, output_event->flags);
|
|
+ if (bi_hold.buffer && bi_hold.output == output->wlr_output &&
|
|
+ bi_hold.seq == output_event->commit_seq) {
|
|
+ sway_log(SWAY_INFO, "BI_PRESENT_HOLD id=%" PRIu64
|
|
+ " commit_seq=%" PRIu32 " presented=%d valid=1",
|
|
+ bi_hold.id, bi_hold.seq, output_event->presented);
|
|
+ // CONTRACT: the owned lock is still held here. A second read is
|
|
+ // limited to visible, hidden, returned buffers that actually reach
|
|
+ // present while locked. A superseded buffer does not advance phase.
|
|
+ bool reread = (bi_reread_phase == 0 && bi_hold.id >= 8 && bi_hold.scroll == 400) ||
|
|
+ (bi_reread_phase == 1 && bi_hold.scroll == 1280) ||
|
|
+ (bi_reread_phase == 2 && bi_hold.scroll == 400);
|
|
+ if (reread) {
|
|
+ const char *dir = getenv("BI_OUTPUT_DIR");
|
|
+ const size_t bytes = 800u * 600u * 4u;
|
|
+ unsigned char *pixels = malloc(bytes);
|
|
+ struct wlr_texture *tex = pixels ? wlr_texture_from_buffer(
|
|
+ output->wlr_output->renderer, bi_hold.buffer) : NULL;
|
|
+ bool read_ok = false;
|
|
+ if (tex) {
|
|
+ struct wlr_texture_read_pixels_options opts = {
|
|
+ .data = pixels, .format = DRM_FORMAT_XRGB8888,
|
|
+ .stride = 800u * 4u,
|
|
+ };
|
|
+ read_ok = wlr_texture_read_pixels(tex, &opts);
|
|
+ wlr_texture_destroy(tex);
|
|
+ }
|
|
+ char path[PATH_MAX];
|
|
+ int n = dir ? snprintf(path, sizeof(path),
|
|
+ "%s/buffer-%" PRIu64 "-present.xrgb", dir, bi_hold.id) : -1;
|
|
+ size_t off = 0;
|
|
+ int fd = -1;
|
|
+ if (read_ok && n >= 0 && (size_t)n < sizeof(path)) {
|
|
+ fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
|
|
+ if (fd >= 0) {
|
|
+ while (off < bytes) {
|
|
+ ssize_t written = write(fd, pixels + off, bytes - off);
|
|
+ if (written <= 0) break;
|
|
+ off += (size_t)written;
|
|
+ }
|
|
+ close(fd);
|
|
+ if (off != bytes) unlink(path);
|
|
+ }
|
|
+ }
|
|
+ sway_log(SWAY_INFO, "BI_PRESENT_READ id=%" PRIu64
|
|
+ " status=%s bytes=%zu phase=%u", bi_hold.id,
|
|
+ off == bytes ? "READ" : "UNAVAILABLE", off, bi_reread_phase);
|
|
+ if (off == bytes) bi_reread_phase++;
|
|
+ free(pixels);
|
|
+ }
|
|
+ bi_release_hold("present");
|
|
+ }
|
|
|
|
if (!output->enabled || !output_event->presented) {
|
|
return;
|