~amavect/whiteboardfs

384e1cdef71735b8c3c628427841cab564b48003 — amavect 1 year, 10 months ago
Import from https://bitbucket.org/amavect/whiteboardfs
6 files changed, 1309 insertions(+), 0 deletions(-)

A README
A dryerase.c
A imageload.c
A mkfile
A whiteboardfs.4.man
A whiteboardfs.c
A  => README +21 -0
@@ 1,21 @@
Whiteboardfs is a collaborative drawing file system.
It consists of 2 files in the tree.
canvas is the image to draw to. 
	On read, it returns a RGB24 image.
	To write, write a RGBA32 image, which will be composited SoverD.
update notifies of any changes.
	On read, it returns 1 character and then blocks.
	If a write completes, another character is sent.

Dryerase is a program meant to interface with whiteboardfs.
It will, by default, search in /mnt/whiteboard for the control files.
Mouse 1 for black, mouse 3 for white.
The '[' and ']' keys decrease and increase pen size, respectively.
Delete to quit.

TODO:
make whiteboardfs understand multiple image types
make whiteboardfs be able to use sub-Rectangles
make whiteboardfs send compressed images
add different sizes to dryerase
add different colors to dryerase

A  => dryerase.c +441 -0
@@ 1,441 @@
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <keyboard.h>
#include <mouse.h>

Image *canvas;
Image *in;
Image *out; /* add a write queue (delete comment when outbuf works) */
Image *outbuf; /* double buffering */
Image *clear;
Image *grey;
Image *pencol;
Point drawpt;
int pensize;
int writing;
int reading;

QLock ql;

Image *
getcanvasimage(int fd)
{
	Image *i;
	
	qlock(&ql);
	seek(fd, 0, 0);
	i = readimage(display, fd, 1);
	qunlock(&ql);
	if(i == nil)
		sysfatal("get: %r");
	if(i->r.min.x != 0 || i->r.min.y != 0 || i->r.max.x <= 0 || i->r.max.y <= 0)
		sysfatal("Bad image size.");
	if(i->r.max.x > 4096 || i->r.max.y > 2160)
		sysfatal("Image too large to be useful on most screens.");
	return i;
}

void
usage(void)
{
	fprint(2, "usage: %s [-c] [-d dir]\n", argv0);
	threadexitsall("usage");
}

void
dogetwindow(void)
{
	lockdisplay(display);
	if(getwindow(display, Refnone) < 0)
		sysfatal("Cannot reconnect to display: %r");
	unlockdisplay(display);
}

void
redraw(void)
{
	Point ss, lu, mid;
	int y;
	char *mesg;
	
	lockdisplay(display);
	draw(screen, screen->r, display->black, nil, ZP);
	mid.x = Dx(screen->r)/2 + screen->r.min.x;
	mid.y = Dy(screen->r)/2 + screen->r.min.y;
	draw(screen, Rect(mid.x, screen->r.min.y, screen->r.max.x, mid.y), display->white, nil, ZP);
	draw(screen, Rect(screen->r.min.x, mid.y, mid.x, screen->r.max.y), display->white, nil, ZP);
	draw(screen, screen->r, pencol, nil, ZP);
	drawpt.x = (Dx(screen->r) - Dx(canvas->r))/2 + screen->r.min.x;
	drawpt.y = (Dy(screen->r) - Dy(canvas->r))/2 + screen->r.min.y;
	border(screen, rectaddpt(canvas->r, drawpt), -2, grey, ZP);
	draw(screen, screen->r, canvas, nil, subpt(screen->r.min, drawpt));
	draw(screen, screen->r, outbuf, nil, subpt(screen->r.min, drawpt));
	draw(screen, screen->r, out, nil, subpt(screen->r.min, drawpt));
	if(writing){
		mesg = "writing...";
	}else{
		mesg = "ready.";
	}
	ss = stringsize(display->defaultfont, mesg);
	draw(screen, Rpt(screen->r.min, addpt(screen->r.min, ss)), display->white, nil, ZP);
	string(screen, screen->r.min, display->black, ZP, display->defaultfont, mesg);
	
	if(reading){
		mesg = "reading...";
	}else{
		mesg = "done.";
	}
	y = ss.y;
	ss = stringsize(display->defaultfont, mesg);
	ss.y += y;
	lu = addpt(screen->r.min, Pt(0,y));
	draw(screen, Rpt(lu, addpt(screen->r.min, ss)), display->white, nil, ZP);
	string(screen, lu, display->black, ZP, display->defaultfont, mesg);
	
	unlockdisplay(display);
}

struct{
	int ufd;
	int cfd;
	Channel *chan; /* char */
} ups;

void
updateproc(void*)
{
	/* uses struct ups instead of aux arg */
	char i;
	long n;
	
	threadsetname("updater");
	for(;;){
		n = read(ups.ufd, &i, sizeof(i));
		if(n <= 0){
			yield(); /* if error is due to exiting, we'll exit here */
			if(n < 0){
				fprint(2, "Whiteboard probably closed.\n");
				threadexitsall("read error");
			}
			fprint(2, "Why was there a size 0 read?\n");
		}
		reading = 1;
		nbsend(ups.chan, nil);
		in = getcanvasimage(ups.cfd);
		if(in == nil)
			sysfatal("Invalid image update read.");
		/* potential to add dynamic resizing */
		if(!eqrect(in->r, canvas->r))
			sysfatal("Canvas rectangle changed.");
		lockdisplay(display);
		draw(canvas, canvas->r, in, nil, ZP);
		freeimage(in);
		reading = 0;
		unlockdisplay(display);
		nbsend(ups.chan, &i);
	}
}

struct{
	int cfd;
	Channel *chan; /* Image *i */
} sends;
void
sendproc(void*)
{
	/* uses struct ups instead of aux arg */
	int n;
	Image *im;
	
	im = nil;
	threadsetname("sender");
	for(;;){
		n = recv(sends.chan, &im);
		if(n != 1){
			yield(); /* if error is due to exiting, we'll exit here */
			if(n < 0){
				fprint(2, "Whiteboard probably closed.\n");
				threadexitsall("recv error");
			}
			fprint(2, "Why was there a size 0 send?\n");
		}
		
		writeimage(sends.cfd, im, 1);
		lockdisplay(display);
		drawop(im, im->r, clear, nil, ZP, S);
		unlockdisplay(display);
		writing = 0;
		nbsend(ups.chan, nil);
	}
}

void
sendoutimage(void)
{
	Image *i;
	
	send(sends.chan, &out);
	writing = 1;
	i = out;
	out = outbuf;
	outbuf = i;
}

struct{
	Channel *chan; /* ulong */
	int fd;
} makeu;
void
makeuproc(void*){
	char buf[12];
	ulong col;
	int n;
	
	for(;;){
		n = read(makeu.fd, buf, sizeof(buf));
		if(n < 1){
			yield(); /* if error is due to exiting, we'll exit here */
			if(n < 0){
				fprint(2, "whiteboard probably closed.\n");
				threadexitsall("read error");
			}
			if(n == 0){
				fprint(2, "makeu closed?\n");
				//threadexitsall(nil);
			}
		}
		buf[sizeof(buf)-1] = '\0';
		col = strtoul(buf, nil, 0);
		send(makeu.chan, &col);
	}
}

void
threadmain(int argc, char **argv)
{
	static Keyboardctl *kctl;
	Rune r;
	static Mousectl *mctl;
	static Mouse m, mold; /* don't you love puns? */
	Point drawpt;
	int inside;
	int state;
	static char hdr[5*12+1];
	long n;
	char *dir;
	static char path[128];
	Rectangle rect;
	ulong col;
	int cflag;
	uchar ldcol[4];
	int pid;
	int pfd[2];
	char *srvwsys;
	int wsysfd;
	
	dir = "/mnt/whiteboard";
	cflag = 0;
	ARGBEGIN{
	case 'd':
		dir = argv[0];
		break;
	case 'c':
		cflag = 1;
		break;
	default:
		usage();
	}ARGEND;
	
	snprint(path, sizeof(path), "%s/%s", dir, "canvas.bit");
	if((ups.cfd = open(path, ORDWR)) < 0)
		sysfatal("%r");
	/*
	 * readimage and writeimage both implicitly seek on the fd.
	 * dup doesn't separate the seeks, so open again.
	 */
	if((sends.cfd = open(path, ORDWR)) < 0)
		sysfatal("%r");
	snprint(path, sizeof(path), "%s/%s", dir, "update");
	if((ups.ufd = open(path, ORDWR)) < 0)
		sysfatal("%r");
	ups.chan = chancreate(sizeof(char), 1);
	sends.chan = chancreate(sizeof(Image*), 0);
	if(initdraw(nil, nil, argv0) < 0)
		sysfatal("%r");
	display->locking = 1;
	unlockdisplay(display);
	if((mctl = initmouse(nil, screen)) == nil)
		sysfatal("%r");
	if((kctl = initkeyboard(nil)) == nil)
		sysfatal("%r");
	
	if(cflag){
		if(pipe(pfd) < 0)
			sysfatal("%s: pipe failed: %r", argv0);
		makeu.fd = pfd[0];
		pid = rfork(RFFDG|RFREND|RFPROC|RFNOWAIT);
		if(pid == 0){
			dup(pfd[1], 1);
			srvwsys = getenv("wsys");
			if(srvwsys == nil)
				sysfatal("can't find $wsys: %r");
			rfork(RFNAMEG);
			wsysfd = open(srvwsys, ORDWR);
			if(wsysfd < 0)
				sysfatal("can't open $wsys: %r");
			if(mount(wsysfd, -1, "/mnt/wsys", MREPL, "new -dx 400 -dy 300") < 0)
				sysfatal("can't mount new window: %r");
			if(bind("/mnt/wsys", "/dev", MBEFORE) < 0)
				sysfatal("can't bind: %r");
			execl("/bin/makeu", "makeu", "-p", "-c", "0x000000FF", nil);
			fprint(2, "%s: makeu exec failed: %r", argv0);
			threadexitsall("makeu exec failed");
		}
		makeu.chan = chancreate(sizeof(ulong), 1);
		if(proccreate(makeuproc, &sends, 4096) < 0)
			sysfatal("%r");
	}else{
		makeu.chan = nil;
	}
	
	dogetwindow();
	
	n = pread(ups.cfd, hdr, 5*12, 0);
	if(n < 5*12){
		sysfatal("No image header.");
	}
	/* basic protection */
	hdr[11] = hdr[23] = hdr[35] = hdr[47] = hdr[59] = '\0';
	if(strtochan(hdr) == 0)
		sysfatal("Bad image channel.");
	rect.min.x = atoi(hdr+1*12);
	rect.min.y = atoi(hdr+2*12);
	rect.max.x = atoi(hdr+3*12);
	rect.max.y = atoi(hdr+4*12);
	if(rect.min.x != 0 || rect.min.y != 0 || rect.max.x <= 0 || rect.max.y <= 0)
		sysfatal("Bad image size.");
	if(rect.max.x > 4096 || rect.max.y > 2160)
		sysfatal("Image too large to be useful on most screens.");
	/* can be subverted for a massive size, despite previous checking */
	
	canvas = getcanvasimage(ups.cfd);
	
	lockdisplay(display);
	out = allocimage(display, canvas->r, RGBA32, 0, DTransparent);
	outbuf = allocimage(display, canvas->r, RGBA32, 0, DTransparent);
	clear = allocimage(display, Rect(0,0,1,1), RGBA32, 1, DTransparent);
	grey = allocimage(display, Rect(0,0,1,1), RGBA32, 1, 0x555555FF);
	pencol = allocimage(display, Rect(0,0,1,1), RGBA32, 1, DBlack);
	if(out == nil || grey == nil)
		sysfatal("Allocimage failed. %r");
	unlockdisplay(display);
	
	if(proccreate(updateproc, &ups, 4096) < 0)
		sysfatal("%r");
	if(proccreate(sendproc, &sends, 4096) < 0)
		sysfatal("%r");
	
	state = 0;
	mold.buttons = 0;
	mold.xy = screen->r.min; /* arbitrary */
	
	dogetwindow();
	redraw();
	enum { MOUSE, RESIZE, KEYS, UPDATE, MAKEU, NONE };
	Alt alts[] = {
		[MOUSE] =  {mctl->c, &m, CHANRCV},
		[RESIZE] =  {mctl->resizec, nil, CHANRCV},
		[KEYS] = {kctl->c, &r, CHANRCV},
		[UPDATE] = {ups.chan, nil, CHANRCV},
		[MAKEU] =  {makeu.chan, &col, CHANEND},
		[NONE] =  {nil, nil, CHANEND},
	};
	
	if(cflag)
		alts[MAKEU].op = CHANRCV;
	
	for(;;){
		lockdisplay(display);
		flushimage(display, 1);
		unlockdisplay(display);
noflush:
		switch(alt(alts)){
		case MOUSE:
			switch(state){
			case 0:
				drawpt.x = (Dx(screen->r) - Dx(canvas->r))/2 + screen->r.min.x;
				drawpt.y = (Dy(screen->r) - Dy(canvas->r))/2 + screen->r.min.y;
				inside = ptinrect(subpt(m.xy, drawpt), canvas->r);
				if(m.buttons == 1 && inside)
					state = 1;
				else if(m.buttons == 4 && inside)
					state = 2;
				else
					goto noflush;
				break;
			case 1:
				lockdisplay(display);
				line(out, subpt(mold.xy, drawpt), subpt(m.xy, drawpt),
					Enddisc, Enddisc, pensize, pencol, ZP);
				unlockdisplay(display);
				if(m.buttons != 1){
					state = 0;
					lockdisplay(display);
					draw(canvas, out->r, out, nil, ZP);
					unlockdisplay(display);
					sendoutimage();
				}
				break;
			case 2:
				lockdisplay(display);
				line(out, subpt(mold.xy, drawpt), subpt(m.xy, drawpt),
					Enddisc, Enddisc, pensize, display->white, ZP);
				unlockdisplay(display);
				if(m.buttons != 4){
					state = 0;
					lockdisplay(display);
					draw(canvas, out->r, out, nil, ZP);
					unlockdisplay(display);
					sendoutimage();
				}
				break;
			}
			redraw();
			mold = m;
			break;
		case RESIZE:
			dogetwindow();
			redraw();
			break;
		case KEYS:
			if(r == Kdel){
				threadexitsall(nil);
			}else if(r == ']'){
				pensize += 1;
				if(pensize > 42)
					pensize = 42;
			}else if(r == '['){
				pensize -= 1;
				if(pensize < 0)
					pensize = 0;
			}
			goto noflush;
		case UPDATE:
			redraw();
			break;
		case MAKEU:
			ldcol[0] = col>>0 & 0xFF;
			ldcol[1] = col>>8 & 0xFF;
			ldcol[2] = col>>16 & 0xFF;
			ldcol[3] = col>>24 & 0xFF;
			loadimage(pencol, pencol->r, ldcol, sizeof(ldcol));
			redraw();
			break;
		case NONE:
			print("I'm a woodchuck, not a woodchucker! (thanks for playing)\n");
			goto noflush;
		}
	}
}

A  => imageload.c +140 -0
@@ 1,140 @@
/* Amavect! */
/*
 * This is a reimplementation of readimage, creadimage, writeimage, readmemimage,
 * creadmemimage, writememimage in such a way that breaks down into
 * functions that can be used to construct Images over 9p.
 * There is some repetition between the current readimage and creadimage, so I
 * thought I could consolidate that.
 *
 * neglects old image format
 * 
 * i like to put fake in front of my function defs.
 */
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <memdraw.h>

/* 
 * Allocate an empty memimage based on the 60 byte header.
 * Assumes *hdr contains points to at least 60 bytes.
 * returns nil on error
 */
Memimage *
hdrallocmemimage(char *hdr)
{
	ulong chan;
	Rectangle r;
	Memimage *i;
	
	if(hdr[11] != ' '){
		werrstr("hdrallocmemimage: bad format");
		return nil;
	}
	
	hdr[11] = '\0';
	if((chan = strtochan(hdr)) == 0){
		werrstr("hdrallocmemimage: bad channel string %s", hdr);
		return nil;
	}
	
	hdr[1*12+11] = hdr[2*12+11] = hdr[3*12+11] = hdr[4*12+11] = '\0';
	r.min.x=atoi(hdr+1*12);
	r.min.y=atoi(hdr+2*12);
	r.max.x=atoi(hdr+3*12);
	r.max.y=atoi(hdr+4*12);
	if(r.min.x > r.max.x || r.min.y > r.max.y){
		werrstr("hdrallocmemimage: bad rectangle");
		return nil;
	}

	i = allocmemimage(r, chan);
	if(i == nil)
		return nil;
	return i;
}

/*
 * send a maximum 6025 byte data buffer block
 * overwrites miny to be the the last row updated
 * returns number of bytes read from buffer, -1 on failure
 * 
 * currently returns -1 if n is not the same as the block size.
 */
int
blockcloadmemimage(Memimage *i, uchar *buf, int n, int *miny)
{
	int maxy;
	int nb, ncblock;
	
	if(n < 2*12){
		werrstr("blockcloadmemimage: short read");
		return -1;
	}
	buf[0*12+11] = buf[1*12+11] = '\0';
	nb = atoi((char*)buf+1*12);
	if(n < nb + 2*12)
		return 0;
	maxy = atoi((char*)buf+0*12);
	if(maxy <= *miny || maxy > i->r.max.y){
		werrstr("blockcloadmemimage: bad maxy %d", maxy);
		return -1;
	}
	ncblock = _compblocksize(i->r, i->depth);
	if(nb <= 0 || nb > ncblock){
		werrstr("blockcloadmemimage: bad count %d", nb);
		return -1;
	}
	if(nb + 2*12 != n){
		werrstr("blockcloadmemimage: block size != buffer size, %d != %d", nb, n);
		return -1;
	}
	if(cloadmemimage(i, Rect(i->r.min.x, *miny, i->r.max.x, maxy), buf+2*12, nb) < 0){
		werrstr("blockcloadmemimage: bad cloadmemimage");
		return -1;
	}
	*miny = maxy;
	return nb;
}

/* 
 * returns number of bytes written. 
 * returns -1 on error, or 0 on no bytes written
 */
int
lineloadmemimage(Memimage *i, uchar *buf, int n, int *miny)
{
	int l, dy;
	
	l = bytesperline(i->r, i->depth);
	if(l <= 0){
		werrstr("blockloadmemimage: bad bytes per line size %d", l);
		return -1;
	}
	dy = n / l;
	if(dy < 0){
		werrstr("blockloadmemimage: n was negative %d", n);
		return -1;
	}else if(dy == 0){
		return 0;
	}
	if(dy + *miny > i->r.max.y){
		werrstr("blockloadmemimage: buf size too large %d", n);
		return -1;
	}
	if(loadmemimage(i, Rect(i->r.min.x, *miny, i->r.max.x, *miny+dy), buf, dy*l) < 0){
		werrstr("blockloadmemimage: bad loadmemimage");
		return -1;
	}
	*miny += dy;
	return dy*l;
}

int
blockloadmemimage(Memimage *i, uchar *buf, int n, int *miny, int comp)
{
	if(comp)
		return blockcloadmemimage(i, buf, n, miny);
	else
		return lineloadmemimage(i, buf, n, miny);
}

A  => mkfile +32 -0
@@ 1,32 @@
</$objtype/mkfile

TARG=whiteboardfs dryerase

EXE=${TARG:%=$O.%}
INST=${TARG:%=/$objtype/bin/%}

default:V: all

/$objtype/bin/%: $O.%
	cp $prereq $target

$O.%: %.$O
	$LD $LDFLAGS -o $target $prereq

$O.whiteboardfs: whiteboardfs.$O imageload.$O
	$LD $LDFLAGS -o $target $prereq

%.$O: %.c
	$CC $CFLAGS $stem.c

/sys/man/4/whiteboardfs: whiteboardfs.4.man
	cp whiteboardfs.4.man /sys/man/4/whiteboardfs

all:V: $EXE

install:V: $INST

man:V: /sys/man/4/whiteboardfs

clean:V:
	rm -f $O.$TARG [$OS].$TARG *.[$OS]

A  => whiteboardfs.4.man +117 -0
@@ 1,117 @@
.TH WHITEBOARDFS 4
.SH NAME
whiteboardfs, dryerase \- collaborative drawing file server
.SH SYNOPSIS
.PP
.B whiteboardfs
[
.B -D
] [
.B -m
.I mntpnt
] [
.B -s
.I srvname
] [
.B -x
.I width
] [
.B -y
.I height
]
.PP
.B dryerase
[
.B -c
] [
.B -d
.I dir
]
.SH DESCRIPTION
.I Whiteboardfs
is a file server for collaborative drawing. It serves the following files:
.TF canvas.bit
.TP
.B canvas.bit
Reads will return an uncompressed image, like 
.BR /dev/screen .
This allows data races to not interrupt a 9p data stream, at the cost of greatly increased read latency. Writes must send a well-formed image within the size of the canvas. Writes may be compressed as specified in
.IR image (6).
Writes will be composited using SoverD. Unsuccessful writes will return Rerror and be discarded.
.TP
.B update
Reads will block. Whenever a write is successful, a single character is read.
.PD
.PP
.I Whiteboardfs
has the following options:
.TF "-s srvname"
.TP
.B -D
Turn on 9p debug output.
.TP
.BI -m " mntpnt"
Set the mount point for the remote filesystem. The default is 
.BR /mnt/whiteboard .
.TP
.BI -s " srvname"
Post the service as
.BI /srv/ srvname.
.TP
.BI -x " width"
Set the canvas image width. The default is 256.
.TP
.BI -y " height"
Set the canvas image height. The default is 256.
.PD
.PP
.I Dryerase
is a client for drawing on a whiteboardfs. Mouse 1 will draw black, mouse 3 will draw white. When a mouse button is released,
.I dryerase
will write the changes to the
.B canvas.bit
file.
The 
.B [
and
.B ]
keys will decrease and increase the pen size, respectively. The
.B Del
key will end the
.I dryerase
session.
.PP
Some diagnostic info is presented. When "reading..." is shown, the
.B canvas.bit
file is currently being read, and when completed, the shown image will update.
When "writing..." is shown,
.I dryerase
is writing changes to the
.B canvas.bit
file.
.PP
.I Dryerase
has a few options:
.TF "-d dir"
.TP
.B -c
Color. An instance of
.IR makeu (1)
will fork to a new window. Sending colors from
.I makeu
will replace the mouse 1 color.
.TP
.BI -d " dir"
Specify the path where whiteboardfs is mounted.
.SH SOURCE
.B https://bitbucket.org/amavect/whiteboardfs
.br
.SH SEE ALSO
.IR colors (1), 
.IR paint (1), 
.IR makeu (1)
.SH BUGS
.PP
While drawing is double-buffered in
.IR dryerase ,
continuing to draw while "writing..." is shown may result in lost mouse inputs.

A  => whiteboardfs.c +558 -0
@@ 1,558 @@
/* Amavect! */
#include <u.h>
#include <libc.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <draw.h>
#include <memdraw.h>

extern Memimage *hdrallocmemimage(char *);
extern int blockloadmemimage(Memimage *i, uchar *buf, int n, int *miny, int comp);

#define DEBUG 0

enum {
	Qroot,
	Qcanvas,
	Qupdate,
};

typedef struct F {
	char *name;
	Qid qid;
	ulong perm;
} F;

typedef struct Readq Readq;
struct Readq {
	Req *this;
	Readq *last; /* doubly linked list */
	Readq *link;
};

typedef struct Writeq Writeq;
struct Writeq {
	Rectangle r;
	uchar comp; /* is compressed */
	int miny; /* last y row written */
	Memimage *mi;
	ulong chan;
	uvlong s; /* residual write buf size */
	uvlong i; /* current buffer index */
	uchar *buf; /* residual write buffer, size bytesperline */
};

typedef struct Updateq Updateq;
struct Updateq {
	Updateq *last; /* doubly linked list */
	Updateq *link;
	u32int fid;
	uchar hasread;
};

char *user;
Memimage *canvas, *diff;
char imhdr[5*12+1];
uchar *imdata;
Updateq *fqhead, *fqtail;
Readq *rqhead, *rqtail;

F root = {"/", {Qroot, 0, QTDIR}, 0555|DMDIR};
F canvasf = {"canvas.bit", {Qcanvas, 0, QTFILE}, 0666};
F updatef = {"update", {Qupdate, 0, QTFILE}, 0666};

void
usage(void)
{
	fprint(2, "%s [-D] [-m mtpt] [-s srv] [-x width] [-y height] \n", argv0);
	exits("usage");
}

ulong
memimagebytelen(Memimage *i)
{
	return Dy(i->r)*bytesperline(i->r, i->depth);
}

void
imdataupdate(void)
{
	unloadmemimage(canvas, canvas->r, imdata, memimagebytelen(canvas));
}

/* update the imhdr, reallocate imdata, and update imdata */
void
imhdrupdate(void)
{
	char ch[11];
	snprint(imhdr, sizeof(imhdr), "%11s %11d %11d %11d %11d ",
		chantostr(ch, canvas->chan), canvas->r.min.x, 
		canvas->r.min.y, canvas->r.max.x, canvas->r.max.y);
	if(imdata != nil)
		imdata = realloc(imdata, memimagebytelen(canvas));
	else
		imdata = malloc(memimagebytelen(canvas));
	imdataupdate();
}

F *
filebypath(uvlong path)
{
	if(path == Qroot)
		return &root;
	else if(path == Qcanvas)
		return &canvasf;
	else if(path == Qupdate)
		return &updatef;
	else
		return nil;
}

void 
fsattach(Req *r)
{
	r->fid->qid = filebypath(Qroot)->qid;
	r->ofcall.qid = r->fid->qid;
	respond(r, nil);
}

char *
fswalk1(Fid *fid, char *name, Qid *qid)
{
	if(fid->qid.path == Qroot){
		if(strcmp(name, canvasf.name) == 0){
			*qid = canvasf.qid;
			fid->qid = *qid;
			return nil;
		}else if(strcmp(name, updatef.name) == 0){
			*qid = updatef.qid;
			fid->qid = *qid;
			return nil;
		}else if(strcmp(name, "..") == 0){
			*qid = root.qid;
			fid->qid = *qid;
			return nil;
		}	
	}
	return "not found";
}

void
fillstat(Dir *d, uvlong path)
{
	F *f;
	
	f = filebypath(path);
	d->qid = f->qid;	/* unique id from server */
	d->mode = f->perm;	/* permissions */
	d->atime = time(0);	/* last read time */
	d->mtime = time(0);	/* last write time */
	d->length = 0;	/* file length */
	d->name = estrdup9p(f->name);	/* last element of path */
	d->uid = estrdup9p(user);	/* owner name */
	d->gid = estrdup9p(user);	/* group name */
	d->muid = estrdup9p(user);	/* last modifier name */
}

void
fsstat(Req *r)
{
	fillstat(&r->d, r->fid->qid.path);
	respond(r, nil);
}

int
rootgen(int n, Dir *d, void *)
{
	switch(n){
	case 0:
		fillstat(d, canvasf.qid.path);
		break;
	case 1:
		fillstat(d, updatef.qid.path);
		break;
	default:
		return -1;
	}
	return 0;
}

void
fsopen(Req *r)
{
	Updateq *fq;
	
	if(r->fid->qid.path == Qupdate){
		fq = emalloc9p(sizeof(Updateq));
		fq->fid = r->ifcall.fid;
		fq->last = fqtail;
		if(fqtail != nil)
			fqtail->link = fq;
		else
			fqhead = fq; /* assert fqhead == nil */
		fqtail = fq;
		fq->hasread = 1;
		r->fid->aux = fq;
	}
	
	respond(r, nil);
}


void
readspond(Req *r)
{
	Updateq *fq;
	
	fq = r->fid->aux;
	
	if(r->ifcall.count < 1){
		respond(r, "buffer too small");
	}else{
		r->ofcall.count = 1;
		r->ofcall.data[0] = 'y'; /* draw(3) y, but not really */
		fq->hasread = 1;
		respond(r,nil);
	}
}

void
fsread(Req *r)
{
	uvlong path;
	Updateq *fq;
	Readq *rq;
	ulong c, o, n, maxoff;
	
	path = r->fid->qid.path;
	if(path == Qroot){
		dirread9p(r, rootgen, nil);
		respond(r, nil);
	}else if(path == Qcanvas){
		c = r->ifcall.count;
		o = r->ifcall.offset;
		n = 0;
		if(o < 5*12){
			n += 5*12 - o;
			if(c < n)
				n = c;
			memcpy(r->ofcall.data, imhdr + o, n);
			c -= n;
			o += n;
		}
		o -= 5*12;
		maxoff = memimagebytelen(canvas);
		if(o < maxoff && c > 0){
			if(c + o > maxoff)
				c = maxoff - o;
			memcpy(r->ofcall.data + n, imdata + o, c);
			n += c;
		}
		r->ofcall.count = n;
		respond(r, nil);
	}else if(path == Qupdate){
		/* if buffer ptr is behind, catchup, otherwise add to queue */
		fq = r->fid->aux;
		if(fq->hasread){
			rq = emalloc9p(sizeof(Readq)); /* potential cache thrasher */
			rq->this = r;
			rq->last = rqtail;
			if(rqtail != nil)
				rqtail->link = rq;
			else
				rqhead = rq; /* assert rqhead == nil */
			rqtail = rq;
			r->aux = rq;
		}else{
			readspond(r);
		}
	}else{
		respond(r, "invalid Qid path");
	}
}

void
fswrite(Req *r)
{
	Readq *rq, *next;
	int n, e, s;
	Writeq *wq;
	Updateq *uq;
	
	if(r->fid->qid.path != Qcanvas){
		respond(r, "Cannot write to there!");
		return;
	}
	n = 0;
	/* basic sanitization until I add support for general image types and compression */
	wq = r->fid->aux;
	if(wq == nil){
		if(r->ifcall.count < 60){
			respond(r, "write buffer too small, cannot recover header");
			return;
		}
		wq = malloc(sizeof(Writeq));
		if(wq == nil){
			respond(r, "out of memory #1");
			return;
		}
		if(memcmp(r->ifcall.data, "compressed\n", 11) == 0){
			wq->comp = 1;
			n += 11;
		}else{
			wq->comp = 0;
		}
		wq->mi = hdrallocmemimage(r->ifcall.data + n);
		if(wq->mi == nil){
			free(wq);
			respond(r, "bad header");
			return;
		}
		if(!rectinrect(wq->mi->r, canvas->r)){
			freememimage(wq->mi);
			free(wq);
			respond(r, "image size not within canvas rectangle");
			return;
		}
		n += 60;
		wq->i = 0;
		if(wq->comp)
			wq->s = 6025;
		else
			wq->s = bytesperline(wq->mi->r, wq->mi->depth); /* residual buffer size */
		wq->buf = malloc(wq->s); /* residual buffer for uncompressed writes */
		if(wq->buf == nil){
			freememimage(wq->mi);
			free(wq);
			respond(r, "out of memory #2");
			return;
		}
		wq->miny = wq->mi->r.min.y;
		r->fid->aux = wq;
	}
	
	e = 1;
	while(n < r->ifcall.count && e > 0){
		if(DEBUG) fprint(2, "start: n=%d e=%d wqi=%ulld\n", n, e, wq->i);
		if(wq->i > 0){
			s = r->ifcall.count - n < wq->s - wq->i ? r->ifcall.count - n : wq->s - wq->i;
			memcpy(wq->buf + wq->i, r->ifcall.data, s);
			wq->i += s;
			e = blockloadmemimage(wq->mi, wq->buf, wq->i, &wq->miny, wq->comp);
			wq->i = 0;
		}else{
			e = blockloadmemimage(wq->mi, (uchar*)r->ifcall.data+n, r->ifcall.count, &wq->miny, wq->comp);
		}
		if(e < 0){
			freememimage(wq->mi);
			free(wq->buf);
			free(wq);
			r->fid->aux = nil;
			fprint(2, "%r\n");
			respond(r, "error loading image. deleting write request");
			return;
		}
		n += e;
		if(DEBUG) fprint(2, "end: n=%d e=%d wqi=%ulld\n", n, e, wq->i);
	}
	if(n < r->ifcall.count){
		wq->i = r->ifcall.count - n;
		if(wq->i > wq->s)
			sysfatal("Why would that be bigger? %ulld", wq->i);
		memcpy(wq->buf, r->ifcall.data+n, wq->i);
	}
	if(wq->miny == wq->mi->r.max.y && wq->i != 0){
		freememimage(wq->mi);
		free(wq->buf);
		free(wq);
		r->fid->aux = nil;
		respond(r, "too much data sent. deleting write request");
		return;
	}
	r->ofcall.count = r->ifcall.count;
	if(wq->miny < wq->mi->r.max.y){
		respond(r, nil);
		return;
	}else if(wq->miny > wq->mi->r.max.y){
		/* assert cannot happen */
		fprint(2, "write size error: %d > %d", wq->miny, wq->mi->r.max.y);
		freememimage(wq->mi);
		free(wq->buf);
		free(wq);
		r->fid->aux = nil;
		respond(r, "image write too large, somehow. deleting write request");
		return;
	}
	if(DEBUG) fprint(2, "successful image write\n");
	/* Successful image write. composite, update, and notify. */
	/*
	 * Currently: wq->chan == diff->chan && wq->r == diff->r
	 * To support different image write sizes, a new Memimage will
	 * need to be allocated.
	 */
	memimagedraw(canvas, canvas->r, wq->mi, ZP, nil, ZP, SoverD);
	freememimage(wq->mi);
	free(wq->buf);
	free(wq);
	r->fid->aux = nil;
	respond(r,nil);
	imdataupdate();
	/* invalidate all updateqs */
	for(uq = fqhead; uq != nil; uq = uq->link){
		uq->hasread = 0;
	}
	/* respond to all waiting reads */
	for(rq = rqhead; rq != nil; rq = next){
		/* respond() calls destroyreq, which frees this rq */
		next = rq->link;
		readspond(rq->this);
	}
	rqhead = nil;
	rqtail = nil;
}

void
fsflush(Req *r)
{
	Readq *rq;
	for(rq = rqhead; rq != nil; rq = rq->link){
		if(r->ifcall.oldtag == rq->this->tag){
			respond(rq->this, "interrupted");
			if(rq->last != nil)
				rq->last->link = rq->link;
			else
				rqhead = rq->link;
			if(rq->link != nil)
				rq->link->last = rq->last;
			else
				rqtail = rq->last;
			respond(r, nil);
			return;
		}
	}
	respond(r, "invalid tag");
}

void
fsdestroyfid(Fid *fid)
{
	Updateq *fq;
	Writeq *wq;
	
	if(fid->aux == nil)
		return;
	if(fid->qid.path == Qupdate){
		fq = fid->aux;
		/* remove self from DLL */
		if(fq->last == nil){
			fqhead = fq->link;
		}else{
			fq->last->link = fq->link;
		}
		if(fq->link == nil){
			fqtail = fq->last;
		}else{
			fq->link->last = fq->last;
		}
		
		free(fq);
		fid->aux = nil;
	}else if(fid->qid.path == Qcanvas){
		wq = fid->aux;
		freememimage(wq->mi);
		free(wq->buf);
		free(wq);
		fid->aux = nil;
	}else{ /* assert this branch cannot happen */
		sysfatal("unknown dangling fid->aux, qid=%ulld", fid->qid.path);
	}
}

void
fsdestroyreq(Req *r)
{
	Readq *rq;
	
	if(r->aux == nil)
		return;
	rq = r->aux;
	/* remove self from DLL */
	if(rq->last == nil){
		rqhead = rq->link;
	}else{
		rq->last->link = rq->link;
	}
	if(rq->link == nil){
		rqtail = rq->last;
	}else{
		rq->link->last = rq->last;
	}
	
	free(rq);
	r->aux = nil;
}

Srv fs = {
	.attach = fsattach,
	.walk1 = fswalk1,
	.stat = fsstat,
	.open = fsopen,
	.read = fsread,
	.write = fswrite,
	.flush = fsflush,
	.destroyfid = fsdestroyfid,
	.destroyreq = fsdestroyreq,
};

void
main(int argc, char *argv[])
{
	char *mtpt, *srv;
	int x, y;
	
	mtpt = "/mnt/whiteboard";
	srv = nil;
	x = 256;
	y = 256;
	ARGBEGIN{
	case 'm':
		mtpt = EARGF(usage());
		break;
	case 's':
		srv = EARGF(usage());
		break;
	case 'x':
		x = strtol(EARGF(usage()), nil, 0);
		break;
	case 'y':
		y = strtol(EARGF(usage()), nil, 0);
		break;
	case 'D':
		chatty9p++;
		break;
	default:
		usage();
	}ARGEND;
	
	if(x <= 1)
		exits("x too small");
	if(x >= 1024) /* don't break the network */
		exits("x too big");
	if(y <= 1)
		exits("y too small");
	if(y >= 1024)
		exits("y too big");
	
	if(memimageinit() < 0)
		sysfatal("%r");
	canvas = allocmemimage(Rect(0,0,x,y), RGB24);
	memfillcolor(canvas, 0xA0A0A0FF);
	imhdrupdate();
	diff = allocmemimage(Rect(0,0,x,y), RGBA32);
	
	user = getuser();
	postmountsrv(&fs, srv, mtpt, MREPL);
	
	exits(nil);
}