main neuswc / extra / swcsnap.c
  1#define _POSIX_C_SOURCE 200809L
  2
  3#include <errno.h>
  4#include <fcntl.h>
  5#include <stdbool.h>
  6#include <stdint.h>
  7#include <stdio.h>
  8#include <stdlib.h>
  9#include <string.h>
 10#include <sys/mman.h>
 11#include <time.h>
 12#include <unistd.h>
 13
 14#include <wayland-client.h>
 15
 16#include "swc_snap-client-protocol.h"
 17
 18enum { CAPTURE_FLAGS = SWC_SNAP_FLAGS_OVERLAY_CURSOR };
 19
 20struct shm_buffer {
 21	struct wl_shm_pool *pool;
 22	struct wl_buffer *buffer;
 23	void *data;
 24	size_t size;
 25	int fd;
 26	int32_t width;
 27	int32_t height;
 28	int32_t stride;
 29};
 30
 31struct app {
 32	struct wl_display *display;
 33	struct wl_registry *registry;
 34	struct wl_shm *shm;
 35	struct swc_snap *snap;
 36	uint32_t width;
 37	uint32_t height;
 38	bool have_buffer_info;
 39	bool capture_ready;
 40	bool capture_failed;
 41	uint32_t failure_reason;
 42};
 43
 44static void
 45usage(FILE *stream, const char *argv0)
 46{
 47	fprintf(stream,
 48	        "usage:\n"
 49	        "  %s image [output.ppm]\n"
 50	        "  %s record [-o output.raw|-] [-n frames] [-r fps]\n",
 51	        argv0, argv0);
 52}
 53
 54static ssize_t
 55write_all(int fd, const void *data, size_t size)
 56{
 57	const uint8_t *p = data;
 58	size_t written = 0;
 59
 60	while (written < size) {
 61		ssize_t rc = write(fd, p + written, size - written);
 62		if (rc < 0) {
 63			if (errno == EINTR) {
 64				continue;
 65			}
 66			return -1;
 67		}
 68		written += (size_t)rc;
 69	}
 70
 71	return (ssize_t)written;
 72}
 73
 74static int
 75create_shm_file(size_t size)
 76{
 77	char path[] = "/tmp/swcsnap-XXXXXX";
 78	int fd = mkstemp(path);
 79
 80	if (fd < 0) {
 81		return -1;
 82	}
 83
 84	unlink(path);
 85
 86	if (ftruncate(fd, (off_t)size) < 0) {
 87		close(fd);
 88		return -1;
 89	}
 90
 91	return fd;
 92}
 93
 94static bool
 95shm_buffer_create(struct shm_buffer *buffer, struct wl_shm *shm,
 96                  int32_t width, int32_t height, uint32_t format)
 97{
 98	size_t size;
 99
100	memset(buffer, 0, sizeof(*buffer));
101	buffer->fd = -1;
102	buffer->width = width;
103	buffer->height = height;
104	buffer->stride = width * 4;
105	size = (size_t)buffer->stride * buffer->height;
106	buffer->size = size;
107
108	buffer->fd = create_shm_file(size);
109	if (buffer->fd < 0) {
110		return false;
111	}
112
113	buffer->data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,
114	                    buffer->fd, 0);
115	if (buffer->data == MAP_FAILED) {
116		close(buffer->fd);
117		buffer->fd = -1;
118		buffer->data = NULL;
119		return false;
120	}
121
122	buffer->pool = wl_shm_create_pool(shm, buffer->fd, (int32_t)size);
123	if (!buffer->pool) {
124		munmap(buffer->data, size);
125		close(buffer->fd);
126		buffer->fd = -1;
127		buffer->data = NULL;
128		return false;
129	}
130
131	buffer->buffer = wl_shm_pool_create_buffer(
132	    buffer->pool, 0, width, height, buffer->stride, format);
133	if (!buffer->buffer) {
134		wl_shm_pool_destroy(buffer->pool);
135		munmap(buffer->data, size);
136		close(buffer->fd);
137		buffer->pool = NULL;
138		buffer->fd = -1;
139		buffer->data = NULL;
140		return false;
141	}
142
143	return true;
144}
145
146static void
147shm_buffer_destroy(struct shm_buffer *buffer)
148{
149	if (buffer->buffer) {
150		wl_buffer_destroy(buffer->buffer);
151	}
152	if (buffer->pool) {
153		wl_shm_pool_destroy(buffer->pool);
154	}
155	if (buffer->data) {
156		munmap(buffer->data, buffer->size);
157	}
158	if (buffer->fd >= 0) {
159		close(buffer->fd);
160	}
161	memset(buffer, 0, sizeof(*buffer));
162	buffer->fd = -1;
163}
164
165static bool
166write_ppm(const char *path, const struct shm_buffer *buffer)
167{
168	FILE *f;
169	uint8_t rgb[3];
170
171	f = fopen(path, "wb");
172	if (!f) {
173		perror("fopen");
174		return false;
175	}
176
177	if (fprintf(f, "P6\n%d %d\n255\n", buffer->width, buffer->height) < 0) {
178		fclose(f);
179		return false;
180	}
181
182	for (int32_t y = 0; y < buffer->height; y++) {
183		const uint8_t *row =
184		    (const uint8_t *)buffer->data + (size_t)y * buffer->stride;
185		for (int32_t x = 0; x < buffer->width; x++) {
186			const uint8_t *px = row + (size_t)x * 4;
187
188			rgb[0] = px[2];
189			rgb[1] = px[1];
190			rgb[2] = px[0];
191			if (fwrite(rgb, 1, sizeof(rgb), f) != sizeof(rgb)) {
192				fclose(f);
193				return false;
194			}
195		}
196	}
197
198	return fclose(f) == 0;
199}
200
201static void
202handle_snap_buffer(void *data, struct swc_snap *snap, uint32_t width,
203                   uint32_t height, uint32_t format)
204{
205	struct app *app = data;
206	(void)snap;
207
208	app->width = width;
209	app->height = height;
210	app->have_buffer_info = true;
211}
212
213static void
214handle_snap_ready(void *data, struct swc_snap *snap, uint32_t tv_sec,
215                  uint32_t tv_nsec)
216{
217	struct app *app = data;
218	(void)snap;
219	(void)tv_sec;
220	(void)tv_nsec;
221
222	app->capture_ready = true;
223}
224
225static void
226handle_snap_failed(void *data, struct swc_snap *snap, uint32_t reason)
227{
228	struct app *app = data;
229	(void)snap;
230
231	app->capture_failed = true;
232	app->failure_reason = reason;
233}
234
235static const struct swc_snap_listener snap_listener = {
236    .buffer = handle_snap_buffer,
237    .ready = handle_snap_ready,
238    .failed = handle_snap_failed,
239};
240
241static void
242handle_global(void *data, struct wl_registry *registry, uint32_t name,
243              const char *interface, uint32_t version)
244{
245	struct app *app = data;
246
247	if (strcmp(interface, wl_shm_interface.name) == 0) {
248		app->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
249		return;
250	}
251
252	if (strcmp(interface, "swc_snap") == 0) {
253		uint32_t bind_version = version < 2 ? version : 2;
254
255		app->snap =
256		    wl_registry_bind(registry, name, &swc_snap_interface, bind_version);
257		if (app->snap) {
258			swc_snap_add_listener(app->snap, &snap_listener, app);
259		}
260	}
261}
262
263static void
264handle_global_remove(void *data, struct wl_registry *registry, uint32_t name)
265{
266	(void)data;
267	(void)registry;
268	(void)name;
269}
270
271static const struct wl_registry_listener registry_listener = {
272    .global = handle_global,
273    .global_remove = handle_global_remove,
274};
275
276static bool
277app_connect(struct app *app)
278{
279	memset(app, 0, sizeof(*app));
280
281	app->display = wl_display_connect(NULL);
282	if (!app->display) {
283		fprintf(stderr, "swcsnap: failed to connect to Wayland display\n");
284		return false;
285	}
286
287	app->registry = wl_display_get_registry(app->display);
288	wl_registry_add_listener(app->registry, &registry_listener, app);
289
290	if (wl_display_roundtrip(app->display) < 0 ||
291	    wl_display_roundtrip(app->display) < 0) {
292		fprintf(stderr, "swcsnap: roundtrip failed\n");
293		return false;
294	}
295
296	if (!app->shm || !app->snap || !app->have_buffer_info) {
297		fprintf(stderr, "swcsnap: missing wl_shm/swc_snap metadata\n");
298		return false;
299	}
300
301	return true;
302}
303
304static void
305app_disconnect(struct app *app)
306{
307	if (app->snap) {
308		swc_snap_destroy(app->snap);
309	}
310	if (app->shm) {
311		wl_shm_destroy(app->shm);
312	}
313	if (app->registry) {
314		wl_registry_destroy(app->registry);
315	}
316	if (app->display) {
317		wl_display_disconnect(app->display);
318	}
319	memset(app, 0, sizeof(*app));
320}
321
322static bool
323capture_once(struct app *app, struct shm_buffer *buffer)
324{
325	app->capture_ready = false;
326	app->capture_failed = false;
327	app->failure_reason = 0;
328
329	swc_snap_capture(app->snap, buffer->buffer, CAPTURE_FLAGS);
330	wl_display_flush(app->display);
331
332	while (!app->capture_ready && !app->capture_failed) {
333		if (wl_display_dispatch(app->display) < 0) {
334			return false;
335		}
336	}
337
338	return !app->capture_failed;
339}
340
341static void
342timespec_add_ns(struct timespec *ts, long ns)
343{
344	ts->tv_nsec += ns;
345	while (ts->tv_nsec >= 1000000000L) {
346		ts->tv_nsec -= 1000000000L;
347		ts->tv_sec += 1;
348	}
349}
350
351static void
352sleep_until(const struct timespec *deadline)
353{
354	int rc;
355
356	while ((rc = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, deadline,
357	                             NULL)) != 0) {
358		if (rc != EINTR) {
359			return;
360		}
361	}
362}
363
364static int
365cmd_image(const char *prog, int argc, char **argv)
366{
367	struct app app;
368	struct shm_buffer buffer = {.fd = -1};
369	const char *output = "shot.ppm";
370	int ret = 1;
371
372	if (argc > 2) {
373		fprintf(stderr, "usage: %s image [output.ppm]\n", prog);
374		return 1;
375	}
376	if (argc == 2) {
377		output = argv[1];
378	}
379
380	if (!app_connect(&app)) {
381		goto out0;
382	}
383
384	if (!shm_buffer_create(&buffer, app.shm, (int32_t)app.width,
385	                       (int32_t)app.height, WL_SHM_FORMAT_ARGB8888)) {
386		fprintf(stderr, "swcsnap: failed to allocate shm buffer\n");
387		goto out1;
388	}
389
390	if (!capture_once(&app, &buffer)) {
391		fprintf(stderr, "swcsnap: capture failed (%u)\n", app.failure_reason);
392		goto out2;
393	}
394
395	if (!write_ppm(output, &buffer)) {
396		fprintf(stderr, "swcsnap: failed to write %s\n", output);
397		goto out2;
398	}
399
400	ret = 0;
401
402out2:
403	shm_buffer_destroy(&buffer);
404out1:
405	app_disconnect(&app);
406out0:
407	return ret;
408}
409
410static int
411cmd_record(const char *prog, int argc, char **argv)
412{
413	struct app app;
414	struct shm_buffer buffer = {.fd = -1};
415	unsigned fps = 60;
416	unsigned frames = 600;
417	const char *output_path = "-";
418	int out_fd = STDOUT_FILENO;
419	struct timespec deadline;
420	long frame_ns;
421	int ret = 1;
422	int opt;
423
424	optind = 1;
425	while ((opt = getopt(argc, argv, "o:n:r:")) != -1) {
426		switch (opt) {
427		case 'o':
428			output_path = optarg;
429			break;
430		case 'n':
431			frames = (unsigned)strtoul(optarg, NULL, 10);
432			break;
433		case 'r':
434			fps = (unsigned)strtoul(optarg, NULL, 10);
435			break;
436		default:
437			fprintf(stderr,
438			        "usage: %s record [-o output.raw|-] [-n frames] [-r fps]\n",
439			        prog);
440			return 1;
441		}
442	}
443
444	if (optind != argc) {
445		fprintf(stderr,
446		        "usage: %s record [-o output.raw|-] [-n frames] [-r fps]\n",
447		        prog);
448		return 1;
449	}
450
451	if (fps == 0) {
452		fprintf(stderr, "swcsnap: fps must be > 0\n");
453		return 1;
454	}
455
456	if (strcmp(output_path, "-") != 0) {
457		out_fd = open(output_path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
458		if (out_fd < 0) {
459			perror("open");
460			return 1;
461		}
462	}
463
464	if (!app_connect(&app)) {
465		goto out0;
466	}
467
468	if (!shm_buffer_create(&buffer, app.shm, (int32_t)app.width,
469	                       (int32_t)app.height, WL_SHM_FORMAT_ARGB8888)) {
470		fprintf(stderr, "swcsnap: failed to allocate shm buffer\n");
471		goto out1;
472	}
473
474	fprintf(stderr, "swcsnap: rawvideo stream %ux%u bgra @ %u fps (%u frames)\n",
475	        app.width, app.height, fps, frames);
476
477	frame_ns = 1000000000L / (long)fps;
478	clock_gettime(CLOCK_MONOTONIC, &deadline);
479
480	for (unsigned i = 0; i < frames;) {
481		if ((uint32_t)buffer.width != app.width ||
482		    (uint32_t)buffer.height != app.height) {
483			shm_buffer_destroy(&buffer);
484			if (!shm_buffer_create(&buffer, app.shm, (int32_t)app.width,
485			                       (int32_t)app.height,
486			                       WL_SHM_FORMAT_ARGB8888)) {
487				fprintf(stderr, "swcsnap: failed to reallocate shm buffer\n");
488				goto out2;
489			}
490			fprintf(stderr, "swcsnap: resized to %ux%u\n", app.width,
491			        app.height);
492		}
493
494		if (!capture_once(&app, &buffer)) {
495			if (app.capture_failed &&
496			    app.failure_reason == SWC_SNAP_FAILURE_REASON_SIZE_MISMATCH) {
497				continue;
498			}
499			fprintf(stderr, "swcsnap: capture failed (%u)\n",
500			        app.failure_reason);
501			goto out2;
502		}
503
504		if (write_all(out_fd, buffer.data, buffer.size) < 0) {
505			perror("write");
506			goto out2;
507		}
508
509		i++;
510		timespec_add_ns(&deadline, frame_ns);
511		sleep_until(&deadline);
512	}
513
514	ret = 0;
515
516out2:
517	shm_buffer_destroy(&buffer);
518out1:
519	app_disconnect(&app);
520out0:
521	if (out_fd != STDOUT_FILENO && out_fd >= 0) {
522		close(out_fd);
523	}
524	return ret;
525}
526
527int
528main(int argc, char **argv)
529{
530	if (argc < 2) {
531		usage(stderr, argv[0]);
532		return 1;
533	}
534
535	if (strcmp(argv[1], "image") == 0) {
536		return cmd_image(argv[0], argc - 1, argv + 1);
537	}
538	if (strcmp(argv[1], "record") == 0) {
539		return cmd_record(argv[0], argc - 1, argv + 1);
540	}
541
542	usage(stderr, argv[0]);
543	return 1;
544}