shithub: qoistream

Download patch

ref: 55134acef2cf72c82556521348d2917939faee1b
parent: f4a3042aada0c8012de84907baae3ab193032a43
author: Sigrid Solveig Haflínudóttir <[email protected]>
date: Wed Jul 19 19:42:36 EDT 2023

qoisend

--- /dev/null
+++ b/Makefile
@@ -1,0 +1,41 @@
+TARG=qoirecv
+DESTDIR?=
+PREFIX?=/usr/local
+BIN=${DESTDIR}${PREFIX}/bin
+MAN=${DESTDIR}${PREFIX}/share/man/man1
+SDL2_CFLAGS=$$(pkg-config --cflags sdl2)
+SDL2_LDFLAGS=$$(pkg-config --libs sdl2)
+CFLAGS?=-O2 -pipe -g -Wall
+CFLAGS+=${SDL2_CFLAGS}
+LDFLAGS?=
+LDFLAGS+=${SDL2_LDFLAGS}
+
+OBJS=\
+	parg.o\
+	qoirecv.o\
+
+.PHONY: all default install uninstall clean
+
+all: default
+
+default: ${TARG}
+
+install: ${TARG} ${TARG}.1
+	install -d ${BIN}
+	install -m 755 ${TARG} ${BIN}
+	install -d ${MAN}
+	install -m 644 ${TARG}.1 ${MAN}
+
+uninstall:
+	rm -f ${BIN}/${TARG}
+	rm -f ${MAN}/${TARG}.1
+
+${TARG}: ${OBJS}
+	${CC} -o $@ ${OBJS} ${LDFLAGS}
+
+.SUFFIXES: .c .o
+.c.o:
+	${CC} -o $@ -c $< ${CFLAGS}
+
+clean:
+	rm -f ${TARG} ${OBJS}
--- a/README.md
+++ b/README.md
@@ -6,4 +6,10 @@
 like a cheap screen sharing.  Each frame is lossless but smaller than
 raw images (~620Kb vs ~8Mb for full hd, for example).
 
-Work in progress.
+`qoisend` is used on 9front side, `qoirecv` on Unix side. Default port is 12345.
+
+	# linux
+	$ qoirecv
+
+	# 9front
+	% video/qoisend /dev/screen | aux/trampoline tcp!unixhost!12345
--- a/main.c
+++ /dev/null
@@ -1,278 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <thread.h>
-#include <tos.h>
-
-enum {
-	Opind = 0x00,
-	Opdif = 0x40,
-	Oplum = 0x80,
-	Oprun = 0xc0,
-	Oprgb = 0xfe,
-};
-
-#define Nsec 1000000000ULL
-
-typedef struct Img Img;
-typedef union Pix Pix;
-
-struct Img {
-	int w;
-	int h;
-	u8int bgrx[];
-};
-
-union Pix {
-	struct {
-		u8int r, g, b, a;
-	};
-	u32int v;
-};
-
-static Channel *frame, *done;
-int mainstacksize = 8192;
-
-static uvlong
-nanosec(void)
-{
-	static uvlong fasthz, xstart;
-	uvlong x, div;
-
-	if(fasthz == ~0ULL)
-		return nsec() - xstart;
-
-	if(fasthz == 0){
-		if(_tos->cyclefreq){
-			cycles(&xstart);
-			fasthz = _tos->cyclefreq;
-		} else {
-			xstart = nsec();
-			fasthz = ~0ULL;
-			fprint(2, "cyclefreq not available, falling back to nsec()\n");
-			fprint(2, "you might want to disable aux/timesync\n");
-			return 0;
-		}
-	}
-	cycles(&x);
-	x -= xstart;
-
-	/* this is ugly */
-	for(div = Nsec; x < 0x1999999999999999ULL && div > 1 ; div /= 10ULL, x *= 10ULL);
-
-	return x / (fasthz / div);
-}
-
-static void
-nsleep(uvlong ns)
-{
-	uvlong start, end;
-
-	start = nanosec();
-	end = start + ns;
-	ns = start;
-	do{
-		if(end - ns > 85*Nsec)
-			sleep(80);
-		else if (end - ns > 25*Nsec)
-			sleep(20);
-		else if (end - ns > 1*Nsec)
-			sleep(1);
-		else
-			break;
-		ns = nanosec();
-	}while(ns < end);
-}
-
-static Img *
-imgread(int f, int w, int h)
-{
-	int r, n, e;
-	Img *i;
-
-	e = w*h*4;
-	i = malloc(sizeof(*i) + e);
-	i->w = w;
-	i->h = h;
-	for(n = 0; n < e; n += r){
-		if((r = pread(f, i->bgrx+n, e-n, n+5*12)) <= 0){
-			free(i);
-			return nil;
-		}
-	}
-
-	return i;
-}
-
-static void
-encthread(void *)
-{
-	Pix p[64], pix, prevpix;
-	int i, j, run, indpos;
-	Img *img, *prev;
-	u8int *b, *x;
-
-	threadsetname("qoi/encthread");
-	memset(p, 0, sizeof(p));
-	memset(&prevpix, 0, sizeof(0));
-	prevpix.a = 255;
-	prev = nil;
-	b = nil;
-	for(;;){
-		if(recv(frame, &img) < 0)
-			break;
-		if(prev != nil && memcmp(img->bgrx, prev->bgrx, img->w*img->h*4) == 0){
-			free(img);
-			continue;
-		}else if(prev == nil){
-			b = malloc(14 + img->w*img->h*4); /* a lot of extra for the worst case */
-			b[0] = 'q';
-			b[1] = 'o';
-			b[2] = 'i';
-			b[3] = 'f';
-			b[4] = img->w>>24;
-			b[5] = img->w>>16;
-			b[6] = img->w>>8;
-			b[7] = img->w;
-			b[8] = img->h>>24;
-			b[9] = img->h>>16;
-			b[10] = img->h>>8;
-			b[11] = img->h;
-			b[12] = 3;
-			b[13] = 0;
-		}
-
-		x = img->bgrx;
-		run = 0;
-		j = 14;
-		for(i = 0; i < img->w*img->h; i++){
-			pix.b = *x++;
-			pix.g = *x++;
-			pix.r = *x++;
-			x++;
-
-			if(pix.v == prevpix.v){
-				run++;
-				if(run == 62 || i+1 == img->w*img->h){
-					b[j++] = Oprun | (run-1);
-					run = 0;
-				}
-			}else{
-				if(run > 0){
-					b[j++] = Oprun | (run-1);
-					run = 0;
-				}
-				indpos = (pix.r*3 + pix.g*5 + pix.b*7 + 255*11) % 64;
-				if(pix.v == p[indpos].v){
-					b[j++] = Opind | indpos;
-				}else{
-					signed char Δr, Δg, Δb, Δgr, Δgb;
-					p[indpos].v = pix.v;
-					Δr = pix.r - prevpix.r;
-					Δg = pix.g - prevpix.g;
-					Δb = pix.b - prevpix.b;
-					Δgr = Δr - Δg;
-					Δgb = Δb - Δg;
-					if(Δr > -3 && Δr < 2 && Δg > -3 && Δg < 2 && Δb > -3 && Δb < 2){
-						b[j++] = Opdif | (Δr + 2)<<4 | (Δg + 2)<<2 | (Δb + 2);
-					}else if(Δgr > -9 && Δgr < 8 && Δg > -33 && Δg < 32 && Δgb > -9 && Δgb < 8){
-						b[j++] = Oplum | (Δg + 32);
-						b[j++] = (Δgr + 8)<<4 | (Δgb + 8);
-					}else{
-						b[j++] = Oprgb;
-						b[j++] = pix.r;
-						b[j++] = pix.g;
-						b[j++] = pix.b;
-					}
-				}
-			}
-			prevpix.v = pix.v;
-		}
-		/* padding */
-		for(i = 0; i < 7; i++)
-			b[j++] = 0;
-		b[j++] = 1;
-
-		if(write(1, b, j) != j)
-			break;
-
-		free(prev);
-		prev = img;
-	}
-	sendul(done, 0);
-
-	threadexits(nil);
-}
-
-static void
-usage(void)
-{
-	fprint(2, "usage: %s [-f FPS] FILE\n", argv0);
-	threadexitsall("usage");
-}
-
-void
-threadmain(int argc, char **argv)
-{
-	uvlong fstart, fend, f₀, nframes;
-	int fps, in, w, h, one, debug;
-	char tmp[61], *f[5];
-	Img *img;
-
-	one = 0;
-	debug = 0;
-	fps = 30;
-	ARGBEGIN{
-	case '1':
-		one++;
-		break;
-	case 'd':
-		debug++;
-		break;
-	case 'f':
-		fps = atoi(EARGF(usage()));
-		break;
-	default:
-		usage();
-	}ARGEND
-
-	if(argc != 1)
-		usage();
-	if((in = open(*argv, OREAD)) < 0)
-		sysfatal("input: %r");
-
-	tmp[60] = 0;
-	if(readn(in, tmp, 60) != 60 || tokenize(tmp, f, 5) != 5)
-		sysfatal("invalid image");
-	if(strcmp(f[0]+1, "8r8g8b8") != 0)
-		sysfatal("only [ax]8r8g8b8 is supported");
-	w = atoi(f[3]) - atoi(f[1]);
-	h = atoi(f[4]) - atoi(f[2]);
-	frame = chancreate(sizeof(void*), 1);
-	done = chancreate(sizeof(ulong), 1);
-	proccreate(encthread, nil, mainstacksize);
-
-	f₀ = nanosec();
-	for(nframes = 0;; nframes++){
-		fstart = nanosec();
-		if((img = imgread(in, w, h)) == nil)
-			break;
-		if(sendp(frame, img) != 1)
-			break;
-		if(one){
-			chanclose(frame);
-			recvul(done);
-		}
-		fend = nanosec();
-
-		if(debug && nframes > 0 && (nframes % fps) == 0){
-			fprint(2, "avg fps: %llud\n", nframes/((fend - f₀)/Nsec));
-			f₀ = nanosec();
-			nframes = -1;
-		}
-
-		if(Nsec/fps > (fend - fstart))
-			nsleep(Nsec/fps - (fend - fstart));
-	}
-
-	threadexitsall(nil);
-}
--- a/mkfile
+++ b/mkfile
@@ -1,12 +1,12 @@
 </$objtype/mkfile
 
 BIN=/$objtype/bin/video
-TARG=qoi
+TARG=qoisend
 
 HFILES=\
 
 OFILES=\
-	main.$O\
+	qoisend.$O\
 
 default:V: all
 
--- /dev/null
+++ b/parg.c
@@ -1,0 +1,354 @@
+/*
+ * parg - parse argv
+ *
+ * Written in 2015-2016 by Joergen Ibsen
+ *
+ * To the extent possible under law, the author(s) have dedicated all
+ * copyright and related and neighboring rights to this software to the
+ * public domain worldwide. This software is distributed without any
+ * warranty. <http://creativecommons.org/publicdomain/zero/1.0/>
+ */
+
+#include "parg.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * Check if state is at end of argv.
+ */
+static int
+is_argv_end(const struct parg_state *ps, int argc, char *const argv[])
+{
+	return ps->optind >= argc || argv[ps->optind] == NULL;
+}
+
+/*
+ * Match nextchar against optstring.
+ */
+static int
+match_short(struct parg_state *ps, int argc, char *const argv[],
+            const char *optstring)
+{
+	const char *p = strchr(optstring, *ps->nextchar);
+
+	if (p == NULL) {
+		ps->optopt = *ps->nextchar++;
+		return '?';
+	}
+
+	/* If no option argument, return option */
+	if (p[1] != ':') {
+		return *ps->nextchar++;
+	}
+
+	/* If more characters, return as option argument */
+	if (ps->nextchar[1] != '\0') {
+		ps->optarg = &ps->nextchar[1];
+		ps->nextchar = NULL;
+		return *p;
+	}
+
+	/* If option argument is optional, return option */
+	if (p[2] == ':') {
+		return *ps->nextchar++;
+	}
+
+	/* Option argument required, so return next argv element */
+	if (is_argv_end(ps, argc, argv)) {
+		ps->optopt = *ps->nextchar++;
+		return optstring[0] == ':' ? ':' : '?';
+	}
+
+	ps->optarg = argv[ps->optind++];
+	ps->nextchar = NULL;
+	return *p;
+}
+
+/*
+ * Match string at nextchar against longopts.
+ */
+static int
+match_long(struct parg_state *ps, int argc, char *const argv[],
+           const char *optstring,
+           const struct parg_option *longopts, int *longindex)
+{
+	size_t len;
+	int num_match = 0;
+	int match = -1;
+	int i;
+
+	len = strcspn(ps->nextchar, "=");
+
+	for (i = 0; longopts[i].name; ++i) {
+		if (strncmp(ps->nextchar, longopts[i].name, len) == 0) {
+			match = i;
+			num_match++;
+			/* Take if exact match */
+			if (longopts[i].name[len] == '\0') {
+				num_match = 1;
+				break;
+			}
+		}
+	}
+
+	/* Return '?' on no or ambiguous match */
+	if (num_match != 1) {
+		ps->optopt = 0;
+		ps->nextchar = NULL;
+		return '?';
+	}
+
+	assert(match != -1);
+
+	if (longindex) {
+		*longindex = match;
+	}
+
+	if (ps->nextchar[len] == '=') {
+		/* Option argument present, check if extraneous */
+		if (longopts[match].has_arg == PARG_NOARG) {
+			ps->optopt = longopts[match].flag ? 0 : longopts[match].val;
+			ps->nextchar = NULL;
+			return optstring[0] == ':' ? ':' : '?';
+		}
+		else {
+			ps->optarg = &ps->nextchar[len + 1];
+		}
+	}
+	else if (longopts[match].has_arg == PARG_REQARG) {
+		/* Option argument required, so return next argv element */
+		if (is_argv_end(ps, argc, argv)) {
+			ps->optopt = longopts[match].flag ? 0 : longopts[match].val;
+			ps->nextchar = NULL;
+			return optstring[0] == ':' ? ':' : '?';
+		}
+
+		ps->optarg = argv[ps->optind++];
+	}
+
+	ps->nextchar = NULL;
+
+	if (longopts[match].flag != NULL) {
+		*longopts[match].flag = longopts[match].val;
+		return 0;
+	}
+
+	return longopts[match].val;
+}
+
+void
+parg_init(struct parg_state *ps)
+{
+	ps->optarg = NULL;
+	ps->optind = 1;
+	ps->optopt = '?';
+	ps->nextchar = NULL;
+}
+
+int
+parg_getopt(struct parg_state *ps, int argc, char *const argv[],
+            const char *optstring)
+{
+	return parg_getopt_long(ps, argc, argv, optstring, NULL, NULL);
+}
+
+int
+parg_getopt_long(struct parg_state *ps, int argc, char *const argv[],
+                 const char *optstring,
+                 const struct parg_option *longopts, int *longindex)
+{
+	assert(ps != NULL);
+	assert(argv != NULL);
+	assert(optstring != NULL);
+
+	ps->optarg = NULL;
+
+	if (argc < 2) {
+		return -1;
+	}
+
+	/* Advance to next element if needed */
+	if (ps->nextchar == NULL || *ps->nextchar == '\0') {
+		if (is_argv_end(ps, argc, argv)) {
+			return -1;
+		}
+
+		ps->nextchar = argv[ps->optind++];
+
+		/* Check for nonoption element (including '-') */
+		if (ps->nextchar[0] != '-' || ps->nextchar[1] == '\0') {
+			ps->optarg = ps->nextchar;
+			ps->nextchar = NULL;
+			return 1;
+		}
+
+		/* Check for '--' */
+		if (ps->nextchar[1] == '-') {
+			if (ps->nextchar[2] == '\0') {
+				ps->nextchar = NULL;
+				return -1;
+			}
+
+			if (longopts != NULL) {
+				ps->nextchar += 2;
+
+				return match_long(ps, argc, argv, optstring,
+				                  longopts, longindex);
+			}
+		}
+
+		ps->nextchar++;
+	}
+
+	/* Match nextchar */
+	return match_short(ps, argc, argv, optstring);
+}
+
+/*
+ * Reverse elements of `v` from `i` to `j`.
+ */
+static void
+reverse(char *v[], int i, int j)
+{
+	while (j - i > 1) {
+		char *tmp = v[i];
+		v[i] = v[j - 1];
+		v[j - 1] = tmp;
+		++i;
+		--j;
+	}
+}
+
+/*
+ * Reorder elements of `argv` with no special cases.
+ *
+ * This function assumes there is no `--` element, and the last element
+ * is not an option missing a required argument.
+ *
+ * The algorithm is described here:
+ * http://hardtoc.com/2016/11/07/reordering-arguments.html
+ */
+static int
+parg_reorder_simple(int argc, char *argv[],
+                    const char *optstring,
+                    const struct parg_option *longopts)
+{
+	struct parg_state ps;
+	int change;
+	int l = 0;
+	int m = 0;
+	int r = 0;
+
+	if (argc < 2) {
+		return argc;
+	}
+
+	do {
+		int nextind;
+		int c;
+
+		parg_init(&ps);
+
+		nextind = ps.optind;
+
+		/* Parse until end of argument */
+		do {
+			c = parg_getopt_long(&ps, argc, argv, optstring, longopts, NULL);
+		} while (ps.nextchar != NULL && *ps.nextchar != '\0');
+
+		change = 0;
+
+		do {
+			/* Find next non-option */
+			for (l = nextind; c != 1 && c != -1;) {
+				l = ps.optind;
+
+				do {
+					c = parg_getopt_long(&ps, argc, argv, optstring, longopts, NULL);
+				} while (ps.nextchar != NULL && *ps.nextchar != '\0');
+			}
+
+			/* Find next option */
+			for (m = l; c == 1;) {
+				m = ps.optind;
+
+				do {
+					c = parg_getopt_long(&ps, argc, argv, optstring, longopts, NULL);
+				} while (ps.nextchar != NULL && *ps.nextchar != '\0');
+			}
+
+			/* Find next non-option */
+			for (r = m; c != 1 && c != -1;) {
+				r = ps.optind;
+
+				do {
+					c = parg_getopt_long(&ps, argc, argv, optstring, longopts, NULL);
+				} while (ps.nextchar != NULL && *ps.nextchar != '\0');
+			}
+
+			/* Find next option */
+			for (nextind = r; c == 1;) {
+				nextind = ps.optind;
+
+				do {
+					c = parg_getopt_long(&ps, argc, argv, optstring, longopts, NULL);
+				} while (ps.nextchar != NULL && *ps.nextchar != '\0');
+			}
+
+			if (m < r) {
+				change = 1;
+				reverse(argv, l, m);
+				reverse(argv, m, r);
+				reverse(argv, l, r);
+			}
+		} while (c != -1);
+	} while (change != 0);
+
+	return l + (r - m);
+}
+
+int
+parg_reorder(int argc, char *argv[],
+             const char *optstring,
+             const struct parg_option *longopts)
+{
+	struct parg_state ps;
+	int lastind;
+	int optend;
+	int c;
+
+	assert(argv != NULL);
+	assert(optstring != NULL);
+
+	if (argc < 2) {
+		return argc;
+	}
+
+	parg_init(&ps);
+
+	/* Find end of normal arguments */
+	do {
+		lastind = ps.optind;
+
+		c = parg_getopt_long(&ps, argc, argv, optstring, longopts, NULL);
+
+		/* Check for trailing option with error */
+		if ((c == '?' || c == ':') && is_argv_end(&ps, argc, argv)) {
+			lastind = ps.optind - 1;
+			break;
+		}
+	} while (c != -1);
+
+	optend = parg_reorder_simple(lastind, argv, optstring, longopts);
+
+	/* Rotate `--` or trailing option with error into position */
+	if (lastind < argc) {
+		reverse(argv, optend, lastind);
+		reverse(argv, optend, lastind + 1);
+		++optend;
+	}
+
+	return optend;
+}
--- /dev/null
+++ b/parg.h
@@ -1,0 +1,192 @@
+/*
+ * parg - parse argv
+ *
+ * Written in 2015-2016 by Joergen Ibsen
+ *
+ * To the extent possible under law, the author(s) have dedicated all
+ * copyright and related and neighboring rights to this software to the
+ * public domain worldwide. This software is distributed without any
+ * warranty. <http://creativecommons.org/publicdomain/zero/1.0/>
+ */
+
+#ifndef PARG_H_INCLUDED
+#define PARG_H_INCLUDED
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define PARG_VER_MAJOR 1        /**< Major version number */
+#define PARG_VER_MINOR 0        /**< Minor version number */
+#define PARG_VER_PATCH 2        /**< Patch version number */
+#define PARG_VER_STRING "1.0.2" /**< Version number as a string */
+
+/**
+ * Structure containing state between calls to parser.
+ *
+ * @see parg_init
+ */
+struct parg_state {
+	const char *optarg;   /**< Pointer to option argument, if any */
+	int optind;           /**< Next index in argv to process */
+	int optopt;           /**< Option value resulting in error, if any */
+	const char *nextchar; /**< Next character to process */
+};
+
+/**
+ * Structure for supplying long options to `parg_getopt_long()`.
+ *
+ * @see parg_getopt_long
+ */
+struct parg_option {
+	const char *name; /**< Name of option */
+	int has_arg;      /**< Option argument status */
+	int *flag;        /**< Pointer to flag variable */
+	int val;          /**< Value of option */
+};
+
+/**
+ * Values for `has_arg` flag in `parg_option`.
+ *
+ * @see parg_option
+ */
+typedef enum {
+	PARG_NOARG,  /**< No argument */
+	PARG_REQARG, /**< Required argument */
+	PARG_OPTARG  /**< Optional argument */
+} parg_arg_num;
+
+/**
+ * Initialize `ps`.
+ *
+ * Must be called before using state with a parser.
+ *
+ * @see parg_state
+ *
+ * @param ps pointer to state
+ */
+void
+parg_init(struct parg_state *ps);
+
+/**
+ * Parse next short option in `argv`.
+ *
+ * Elements in `argv` that contain short options start with a single dash
+ * followed by one or more option characters, and optionally an option
+ * argument for the last option character. Examples are '`-d`', '`-ofile`',
+ * and '`-dofile`'.
+ *
+ * Consecutive calls to this function match the command-line arguments in
+ * `argv` against the short option characters in `optstring`.
+ *
+ * If an option character in `optstring` is followed by a colon, '`:`', the
+ * option requires an argument. If it is followed by two colons, the option
+ * may take an optional argument.
+ *
+ * If a match is found, `optarg` points to the option argument, if any, and
+ * the value of the option character is returned.
+ *
+ * If a match is found, but is missing a required option argument, `optopt`
+ * is set to the option character. If the first character in `optstring` is
+ * '`:`', then '`:`' is returned, otherwise '`?`' is returned.
+ *
+ * If no option character in `optstring` matches a short option, `optopt`
+ * is set to the option character, and '`?`' is returned.
+ *
+ * If an element of argv does not contain options (a nonoption element),
+ * `optarg` points to the element, and `1` is returned.
+ *
+ * An element consisting of a single dash, '`-`', is returned as a nonoption.
+ *
+ * Parsing stops and `-1` is returned, when the end of `argv` is reached, or
+ * if an element contains '`--`'.
+ *
+ * Works similarly to `getopt`, if `optstring` were prefixed by '`-`'.
+ *
+ * @param ps pointer to state
+ * @param argc number of elements in `argv`
+ * @param argv array of pointers to command-line arguments
+ * @param optstring string containing option characters
+ * @return option value on match, `1` on nonoption element, `-1` on end of
+ * arguments, '`?`' on unmatched option, '`?`' or '`:`' on option argument
+ * error
+ */
+int
+parg_getopt(struct parg_state *ps, int argc, char *const argv[],
+            const char *optstring);
+
+/**
+ * Parse next long or short option in `argv`.
+ *
+ * Elements in `argv` that contain a long option start with two dashes
+ * followed by a string, and optionally an equal sign and an option argument.
+ * Examples are '`--help`' and '`--size=5`'.
+ *
+ * If no exact match is found, an unambiguous prefix of a long option will
+ * match. For example, if '`foo`' and '`foobar`' are valid long options, then
+ * '`--fo`' is ambiguous and will not match, '`--foo`' matches exactly, and
+ * '`--foob`' is an unambiguous prefix and will match.
+ *
+ * If a long option match is found, and `flag` is `NULL`, `val` is returned.
+ *
+ * If a long option match is found, and `flag` is not `NULL`, `val` is stored
+ * in the variable `flag` points to, and `0` is returned.
+ *
+ * If a long option match is found, but is missing a required option argument,
+ * or has an option argument even though it takes none, `optopt` is set to
+ * `val` if `flag` is `NULL`, and `0` otherwise. If the first character in
+ * `optstring` is '`:`', then '`:`' is returned, otherwise '`?`' is returned.
+ *
+ * If `longindex` is not `NULL`, the index of the entry in `longopts` that
+ * matched is stored there.
+ *
+ * If no long option in `longopts` matches a long option, '`?`' is returned.
+ *
+ * Handling of nonoptions and short options is like `parg_getopt()`.
+ *
+ * If no short options are required, an empty string, `""`, should be passed
+ * as `optstring`.
+ *
+ * Works similarly to `getopt_long`, if `optstring` were prefixed by '`-`'.
+ *
+ * @see parg_getopt
+ *
+ * @param ps pointer to state
+ * @param argc number of elements in `argv`
+ * @param argv array of pointers to command-line arguments
+ * @param optstring string containing option characters
+ * @param longopts array of `parg_option` structures
+ * @param longindex pointer to variable to store index of matching option in
+ * @return option value on match, `0` for flag option, `1` on nonoption
+ * element, `-1` on end of arguments, '`?`' on unmatched or ambiguous option,
+ * '`?`' or '`:`' on option argument error
+ */
+int
+parg_getopt_long(struct parg_state *ps, int argc, char *const argv[],
+                 const char *optstring,
+                 const struct parg_option *longopts, int *longindex);
+
+/**
+ * Reorder elements of `argv` so options appear first.
+ *
+ * If there are no long options, `longopts` may be `NULL`.
+ *
+ * The return value can be used as `argc` parameter for `parg_getopt()` and
+ * `parg_getopt_long()`.
+ *
+ * @param argc number of elements in `argv`
+ * @param argv array of pointers to command-line arguments
+ * @param optstring string containing option characters
+ * @param longopts array of `parg_option` structures
+ * @return index of first nonoption in `argv` on success, `-1` on error
+ */
+int
+parg_reorder(int argc, char *argv[],
+             const char *optstring,
+             const struct parg_option *longopts);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* PARG_H_INCLUDED */
--- /dev/null
+++ b/qoirecv.c
@@ -1,0 +1,235 @@
+#define _DEFAULT_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <SDL.h>
+#include "parg.h"
+
+enum {
+	Opind = 0x00,
+	Opdif = 0x40,
+	Oplum = 0x80,
+	Oprun = 0xc0,
+	Oprgb = 0xfe,
+};
+
+typedef struct Pix Pix;
+
+struct Pix {
+	uint8_t r, g, b, a;
+};
+
+int
+main(int argc, char **argv)
+{
+	int port, c, ls, s, n, w, h, m, ow, oh;
+	uint8_t hdr[14], t[8], *o, *b;
+	Pix p[64], prevpix, pix;
+	struct sockaddr_in addr;
+	struct parg_state ps;
+	SDL_Renderer *rend;
+	SDL_Texture *tex;
+	SDL_Window *win;
+	socklen_t alen;
+	SDL_Event e;
+	FILE *f;
+
+	parg_init(&ps);
+	port = 12345;
+	while((c = parg_getopt(&ps, argc, argv, "hp:")) >= 0){
+		switch(c){
+		case 'p':
+			port = atoi(ps.optarg);
+			break;
+		default:
+		case 'h':
+			fprintf(stderr, "usage: qoirecv [-p PORT]\n");
+			return 0;
+			break;
+		case '?':
+			fprintf(stderr, "unknown option -%c\n", ps.optopt);
+			return 1;
+			break;
+		}
+	}
+
+	if((ls = socket(AF_INET, SOCK_STREAM, 0)) < 0){
+		perror("socket");
+		return 1;
+	}
+	setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));
+	setsockopt(ls, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int));
+	memset(&addr, 0, sizeof(addr));
+	addr.sin_family = AF_INET;
+	addr.sin_addr.s_addr = INADDR_ANY;
+	addr.sin_port = htons(port);
+	if(bind(ls, (struct sockaddr*)&addr, sizeof(addr)) < 0){
+		perror("bind");
+		return 1;
+	}
+	if(listen(ls, 0) < 0){
+		perror("listen");
+		return 1;
+	}
+	if(SDL_Init(SDL_INIT_VIDEO) < 0){
+		fprintf(stderr, "SDL_Init: %s\n", SDL_GetError());
+		return 1;
+	}
+	win = SDL_CreateWindow("qoirecv", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 256, 256, SDL_WINDOW_HIDDEN | SDL_WINDOW_ALLOW_HIGHDPI);
+	if(win == NULL){
+		fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError());
+		return 1;
+	}
+	if((rend = SDL_CreateRenderer(win, -1, 0)) == NULL){
+		fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError());
+		return 1;
+	}
+	SDL_SetRenderDrawColor(rend, 0, 0, 0, 255);
+
+	tex = NULL;
+	b = NULL;
+	ow = oh = 0;
+	for(;;){
+		alen = sizeof(addr);
+		if((s = accept(ls, (struct sockaddr*)&addr, &alen)) < 0){
+			perror("accept");
+			return 1;
+		}
+		if((f = fdopen(s, "rb")) == NULL){
+			perror("fdopen");
+			goto err;
+		}
+		for(;;){
+			while(SDL_PollEvent(&e) > 0){
+				if(e.type == SDL_QUIT){
+					close(s);
+					close(ls);
+					SDL_Quit();
+					return 0;
+				}
+			}
+			if(fread(hdr, 1, sizeof(hdr), f) != sizeof(hdr)){
+				perror("header");
+				goto err;
+			}
+			if(memcmp(hdr, "qoif", 4) != 0 || hdr[12] != 3 || hdr[13] != 0){
+				fprintf(stderr, "invalid image format (magic=%c%c%c%c channels=%d colorspace=%d)\n",
+					hdr[0], hdr[1], hdr[2], hdr[3], hdr[12], hdr[13]
+				);
+				goto err;
+			}
+			w = hdr[4]<<24 | hdr[5]<<16 | hdr[6]<<8 | hdr[7];
+			h = hdr[8]<<24 | hdr[9]<<16 | hdr[10]<<8 | hdr[11];
+			if(w < 1 || h < 1 || w > 16384 || h > 16384){
+				fprintf(stderr, "sketchy image dimensions (%dx%d)\n", w, h);
+				goto err;
+			}
+			if(tex == NULL || ow != w || oh != h){
+				if((o = realloc(b, w*h*3)) == NULL){
+					perror("memory");
+					goto err;
+				}
+				b = o;
+				if(tex != NULL)
+					SDL_DestroyTexture(tex);
+				tex = SDL_CreateTexture(rend, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STATIC, w, h);
+				if(tex == NULL){
+					fprintf(stderr, "SDL_CreateTexture: %s\n", SDL_GetError());
+					goto err;
+				}
+				SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_NONE);
+				SDL_RenderSetLogicalSize(rend, w, h);
+				SDL_RenderClear(rend);
+				SDL_RenderPresent(rend);
+				SDL_SetWindowSize(win, w, h);
+				SDL_SetWindowPosition(win, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+				SDL_ShowWindow(win);
+				ow = w;
+				oh = h;
+			}
+
+			memset(p, 0, sizeof(p));
+			prevpix.r = 0;
+			prevpix.g = 0;
+			prevpix.b = 0;
+			prevpix.a = 255;
+			pix.a = 255;
+			o = b;
+			for(m = w*h; m > 0;){
+				if((c = fgetc(f)) == EOF){
+					fprintf(stderr, "unexpected EOF\n");
+					goto err;
+				}
+				if(c == Oprgb){
+					if(fread(t, 1, 3, f) != 3){
+						perror("Oprgb");
+						goto err;
+					}
+					pix.r = t[0];
+					pix.g = t[1];
+					pix.b = t[2];
+				}else if((c & 0xc0) == Oplum){
+					if((t[0] = fgetc(f)) == EOF){
+						perror("Oplum");
+						goto err;
+					}
+					pix.g = -32 + (c & 0x3f);
+					pix.r = pix.g - 8 + (t[0] >> 4) + prevpix.r;
+					pix.b = pix.g - 8 + (t[0] & 0x0f) + prevpix.b;
+					pix.g += prevpix.g;
+				}else if((c & 0xc0) == Opdif){
+					pix.b = prevpix.b - 2 + ((c>>0) & 3);
+					pix.g = prevpix.g - 2 + ((c>>2) & 3);
+					pix.r = prevpix.r - 2 + ((c>>4) & 3);
+				}else if((c & 0xc0) == Opind){
+					pix = p[c];
+				}else if(c == 0xff){ /* rgba */
+					fprintf(stderr, "unexpected Oprgba\n");
+					goto err;
+				}else{ /* run */
+					if((n = (c&0x3f)+1) > m){
+						fprintf(stderr, "run out of bounds: %d > %d", n, m);
+						goto err;
+					}
+					do{
+						*o++ = pix.r;
+						*o++ = pix.g;
+						*o++ = pix.b;
+						m--;
+					}while(--n > 0);
+					continue;
+				}
+				prevpix = pix;
+				p[(pix.r*3 + pix.g*5 + pix.b*7 + 255*11) & 63] = pix;
+				*o++ = pix.r;
+				*o++ = pix.g;
+				*o++ = pix.b;
+				m--;
+			}
+			if(fread(t, 1, 8, f) != 8){
+				perror("padding");
+				goto err;
+			}
+			if(memcmp(t, "\0\0\0\0\0\0\0\1", 7) != 0){
+				fprintf(stderr, "invalid padding: %02x%02x%02x%02x%02x%02x%02x%02x\n",
+					t[0], t[1], t[2], t[3], t[4], t[5], t[6], t[7]
+				);
+				goto err;
+			}
+			if(SDL_UpdateTexture(tex, NULL, b, w*3) != 0){
+				fprintf(stderr, "SDL_UpdateTexture: %s\n", SDL_GetError());
+				goto err;
+			}
+			SDL_RenderCopy(rend, tex, NULL, NULL);
+			SDL_RenderPresent(rend);
+			continue;
+err:
+			close(s);
+			break;
+		}
+	}
+
+	return 0;
+}
--- /dev/null
+++ b/qoisend.c
@@ -1,0 +1,282 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <tos.h>
+
+enum {
+	Opind = 0x00,
+	Opdif = 0x40,
+	Oplum = 0x80,
+	Oprun = 0xc0,
+	Oprgb = 0xfe,
+};
+
+#define Nsec 1000000000ULL
+
+typedef struct Img Img;
+typedef union Pix Pix;
+
+struct Img {
+	int w;
+	int h;
+	u8int bgrx[];
+};
+
+union Pix {
+	struct {
+		u8int r, g, b, a;
+	};
+	u32int v;
+};
+
+static Channel *frame, *done;
+int mainstacksize = 8192;
+
+static uvlong
+nanosec(void)
+{
+	static uvlong fasthz, xstart;
+	uvlong x, div;
+
+	if(fasthz == ~0ULL)
+		return nsec() - xstart;
+
+	if(fasthz == 0){
+		if(_tos->cyclefreq){
+			cycles(&xstart);
+			fasthz = _tos->cyclefreq;
+		} else {
+			xstart = nsec();
+			fasthz = ~0ULL;
+			fprint(2, "cyclefreq not available, falling back to nsec()\n");
+			fprint(2, "you might want to disable aux/timesync\n");
+			return 0;
+		}
+	}
+	cycles(&x);
+	x -= xstart;
+
+	/* this is ugly */
+	for(div = Nsec; x < 0x1999999999999999ULL && div > 1 ; div /= 10ULL, x *= 10ULL);
+
+	return x / (fasthz / div);
+}
+
+static void
+nsleep(uvlong ns)
+{
+	uvlong start, end;
+
+	start = nanosec();
+	end = start + ns;
+	ns = start;
+	do{
+		if(end - ns > 85*Nsec)
+			sleep(80);
+		else if (end - ns > 25*Nsec)
+			sleep(20);
+		else if (end - ns > 1*Nsec)
+			sleep(1);
+		else
+			break;
+		ns = nanosec();
+	}while(ns < end);
+}
+
+static Img *
+imgread(int in)
+{
+	char tmp[61], *f[5];
+	int r, n, e, w, h;
+	Img *i;
+
+	tmp[60] = 0;
+	if(pread(in, tmp, 60, 0) != 60 || tokenize(tmp, f, 5) != 5)
+		sysfatal("invalid image");
+	if(strcmp(f[0]+1, "8r8g8b8") != 0)
+		sysfatal("only [ax]8r8g8b8 is supported");
+	w = atoi(f[3]) - atoi(f[1]);
+	h = atoi(f[4]) - atoi(f[2]);
+
+	e = w*h*4;
+	i = malloc(sizeof(*i) + e);
+	i->w = w;
+	i->h = h;
+	for(n = 0; n < e; n += r){
+		if((r = pread(in, i->bgrx+n, e-n, n+5*12)) <= 0){
+			free(i);
+			return nil;
+		}
+	}
+
+	return i;
+}
+
+static void
+encthread(void *)
+{
+	int i, j, bsz, run, indpos;
+	Pix p[64], pix, prevpix;
+	Img *img, *prev;
+	u8int *b, *x;
+
+	threadsetname("qoi/encthread");
+	prev = nil;
+	b = nil;
+	bsz = 0;
+	for(;;){
+		if(recv(frame, &img) < 0)
+			break;
+		if(prev != nil && memcmp(img->bgrx, prev->bgrx, img->w*img->h*4) == 0){
+			free(img);
+			continue;
+		}else if(bsz < (i = 14+img->w*img->h*4)){
+			b = realloc(b, bsz = i); /* a lot of extra for the worst case */
+			if(b == nil)
+				sysfatal("memory");
+			b[0] = 'q';
+			b[1] = 'o';
+			b[2] = 'i';
+			b[3] = 'f';
+			b[12] = 3;
+			b[13] = 0;
+		}
+		b[4] = img->w>>24;
+		b[5] = img->w>>16;
+		b[6] = img->w>>8;
+		b[7] = img->w;
+		b[8] = img->h>>24;
+		b[9] = img->h>>16;
+		b[10] = img->h>>8;
+		b[11] = img->h;
+		memset(p, 0, sizeof(p));
+		memset(&prevpix, 0, sizeof(0));
+		prevpix.a = 255;
+
+		x = img->bgrx;
+		run = 0;
+		j = 14;
+		for(i = 0; i < img->w*img->h; i++){
+			pix.b = *x++;
+			pix.g = *x++;
+			pix.r = *x++;
+			x++;
+
+			if(pix.v == prevpix.v){
+				run++;
+				if(run == 62 || i+1 == img->w*img->h){
+					b[j++] = Oprun | (run-1);
+					run = 0;
+				}
+			}else{
+				if(run > 0){
+					b[j++] = Oprun | (run-1);
+					run = 0;
+				}
+				indpos = (pix.r*3 + pix.g*5 + pix.b*7 + 255*11) % 64;
+				if(pix.v == p[indpos].v){
+					b[j++] = Opind | indpos;
+				}else{
+					signed char Δr, Δg, Δb, Δgr, Δgb;
+					p[indpos].v = pix.v;
+					Δr = pix.r - prevpix.r;
+					Δg = pix.g - prevpix.g;
+					Δb = pix.b - prevpix.b;
+					Δgr = Δr - Δg;
+					Δgb = Δb - Δg;
+					if(Δr > -3 && Δr < 2 && Δg > -3 && Δg < 2 && Δb > -3 && Δb < 2){
+						b[j++] = Opdif | (Δr + 2)<<4 | (Δg + 2)<<2 | (Δb + 2);
+					}else if(Δgr > -9 && Δgr < 8 && Δg > -33 && Δg < 32 && Δgb > -9 && Δgb < 8){
+						b[j++] = Oplum | (Δg + 32);
+						b[j++] = (Δgr + 8)<<4 | (Δgb + 8);
+					}else{
+						b[j++] = Oprgb;
+						b[j++] = pix.r;
+						b[j++] = pix.g;
+						b[j++] = pix.b;
+					}
+				}
+			}
+			prevpix.v = pix.v;
+		}
+		/* padding */
+		for(i = 0; i < 7; i++)
+			b[j++] = 0;
+		b[j++] = 1;
+
+		if(write(1, b, j) != j)
+			break;
+
+		free(prev);
+		prev = img;
+	}
+	sendul(done, 0);
+
+	threadexits(nil);
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-f FPS] FILE\n", argv0);
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	uvlong fstart, fend, f₀, nframes;
+	int fps, in, one, debug;
+	Img *img;
+
+	one = 0;
+	debug = 0;
+	fps = 60;
+	ARGBEGIN{
+	case '1':
+		one++;
+		break;
+	case 'd':
+		debug++;
+		break;
+	case 'f':
+		fps = atoi(EARGF(usage()));
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+	if((in = open(*argv, OREAD)) < 0)
+		sysfatal("input: %r");
+
+	frame = chancreate(sizeof(void*), 1);
+	done = chancreate(sizeof(ulong), 1);
+	proccreate(encthread, nil, mainstacksize);
+
+	f₀ = nanosec();
+	for(nframes = 0;; nframes++){
+		fstart = nanosec();
+		if((img = imgread(in)) == nil)
+			continue;
+		if(sendp(frame, img) != 1)
+			break;
+		if(one){
+			chanclose(frame);
+			recvul(done);
+		}
+		fend = nanosec();
+
+		if(debug && nframes > 0 && (nframes % fps) == 0){
+			fprint(2, "avg fps: %llud\n", nframes/((fend - f₀)/Nsec));
+			f₀ = nanosec();
+			nframes = -1;
+		}
+
+		if(Nsec/fps > (fend - fstart))
+			nsleep(Nsec/fps - (fend - fstart));
+	}
+
+	threadexitsall(nil);
+}