shithub: purgatorio

ref: 3efb5bbb4061056e523858b134c555949591efe2
dir: /appl/wm/drawmux/drawmux.b/

View raw version
implement Drawmux;

include "sys.m";
include "draw.m";
include "drawmux.m";

include "drawoffs.m";

sys : Sys;
draw : Draw;

Display, Point, Rect, Chans : import draw;

Ehungup : con "Hangup";

drawR: Draw->Rect;
drawchans: Draw->Chans;
drawop := Draw->SoverD;
drawfd: ref Sys->FD;
images: ref Imageset;
screens: ref Screenset;
viewers: list of ref Viewer;
drawlock: chan of chan of int;
readdata: array of byte;
nhangups := 0;
prevnhangups := 0;

init() : (string, ref Draw->Display)
{
	sys = load Sys Sys->PATH;
	draw = load Draw Draw->PATH;

	if (draw == nil)
		return (sys->sprint("cannot load %s: %r", Draw->PATH), nil);
	drawlock = chan of chan of int;
	images = Imageset.new();
	screens = Screenset. new();
	res := chan of (string, ref Draw->Display);
	spawn getdisp(res);
	r := <- res;
	return r;
}

newviewer(fd : ref Sys->FD)
{
	reply := array of byte sys->sprint("%.11d %.11d ", drawR.max.x - drawR.min.x, drawR.max.y - drawR.min.y);
	if (sys->write(fd, reply, len reply) != len reply) {
#		sys->print("viewer hangup\n");
		return;
	}

	buf := array [Sys->ATOMICIO] of byte;
	n := sys->read(fd, buf, len buf);
	if (n < 24)
		return;
	pubscr := int string buf[0:12];
	chans := Chans.mk(string buf[12:24]);

	sys->pctl(Sys->FORKNS, nil);
	sys->mount(fd, nil, "/", Sys->MREPL, nil);
	cfd := sys->open("/new", Sys->OREAD);
	sys->read(cfd, buf, len buf);
	cnum := int string buf[0:12];
	cdata := sys->sprint("/%d/data", cnum);
	datafd := sys->open(cdata, Sys->ORDWR);

	if (datafd == nil) {
#		sys->print("cannot open viewer data file: %r\n");
		return;
	}
	Viewer.new(datafd, pubscr, chans);
}

getdisp(result : chan of (string, ref Draw->Display))
{	
	sys->pctl(Sys->FORKNS, nil);
	sys->bind("#i", "/dev", Sys->MREPL);
	sys->bind("#s", "/dev/draw", Sys->MBEFORE);
	newio := sys->file2chan("/dev/draw", "new");
	if (newio == nil) {
		result <- = ("cannot create /dev/new file2chan", nil);
		return;
	}
	spawn srvnew(newio);
	disp := Display.allocate(nil);
	if (disp == nil) {
		result <-= (sys->sprint("%r"), nil);
		return;
	}
	
	draw->disp.image.draw(disp.image.r, disp.rgb(0,0,0), nil, Point(0,0));
	result <- = (nil, disp);
}

srvnew(newio : ref Sys->FileIO)
{
	for (;;) alt {
	(offset, count, fid, rc) := <- newio.read =>
		if (rc != nil) {
			c := chan of (string, ref Sys->FD);
			fd := sys->open("#i/draw/new", Sys->OREAD);
			# +1 because of a sprint() nasty in devdraw.c
			buf := array [(12 * 12)+1] of byte;
			nn := sys->read(fd, buf, len buf);
			cnum := int string buf[0:12];
			drawchans = Chans.mk(string buf[24:36]);
			# repl is at [36:48]
			drawR.min.x = int string buf[48:60];
			drawR.min.y = int string buf[60:72];
			drawR.max.x = int string buf[72:84];
			drawR.max.y = int string buf[84:96];

			bwidth := bytesperline(drawR, drawchans);
			img := ref Image (0, 0, 0, 0, drawchans, 0, drawR, drawR, Draw->Black, nil, drawR.min, bwidth, 0, "");
			images.add(0, img);

			cdir := sys->sprint("/dev/draw/%d", cnum);
			dpath := sys->sprint("#i/draw/%d/data", cnum);
			drawfd = sys->open(dpath, Sys->ORDWR);
			fd = nil;
			if (drawfd == nil) {
				rc <-= (nil, sys->sprint("%r"));
				return;
			}
			sys->bind("#s", cdir, Sys->MBEFORE);
			drawio := sys->file2chan(cdir, "data");
			spawn drawclient(drawio);
			rc <- = (buf, nil);
			return;
		}
	(offset, data, fid, wc) := <- newio.write =>
		if (wc != nil)
			writereply(wc, (0, "permission denied"));
	}
}

# for simplicity make the file 'exclusive use'
drawclient(drawio : ref Sys->FileIO)
{
	activefid := -1;
	closecount := 2;

	for (;closecount;) {
		alt {
		unlock := <- drawlock =>
			<- unlock;
	
		(offset, count, fid, rc) := <- drawio.read =>
				if (activefid == -1)
					activefid = fid;
	
				if (rc == nil) {
					closecount--;
					continue;
				}
				if (fid != activefid) {
					rc <-= (nil, "file busy");
					continue;
				}
				if (readdata == nil) {
					rc <-= (nil, nil);
					continue;
				}
				if (count > len readdata)
					count = len readdata;
				rc <- = (readdata[0:count], nil);
				readdata = nil;
	
		(offset, data, fid, wc) := <- drawio.write =>
			if (wc == nil) {
				closecount--;
				continue;
			}
			writereply(wc, process(data));
		}
		if (nhangups != prevnhangups) {
			ok : list of ref Viewer;
			for (ok = nil; viewers != nil; viewers = tl viewers) {
				v := hd viewers;
				if (!v.hungup)
					ok = v :: ok;
				else {
#					sys->print("shutting down Viewer\n");
					v.output <- = (nil, nil);
				}
			}
			viewers = ok;
			prevnhangups = nhangups;
		}
	}
#	sys->print("DRAWIO DONE!\n");
}

writereply(wc : chan of (int, string), val : (int, string))
{
	alt {
	wc <-= val =>
		;
	* =>
		;
	}
}

Image: adt {
	id: int;
	refc: int;
	screenid: int;
	refresh: int;
	chans: Draw->Chans;
	repl: int;
	R: Draw->Rect;
	clipR: Draw->Rect;
	rrggbbaa: int;
	font: ref Font;
	lorigin: Draw->Point;
	bwidth: int;
	dirty: int;
	name: string;
};

Screen: adt {
	id: int;
	imageid: int;
	fillid: int;
	windows: array of int;

	setz: fn (s: self ref Screen, z: array of int, top: int);
	addwin: fn (s: self ref Screen, wid: int);
	delwin: fn (s: self ref Screen, wid: int);
};

Font: adt {
	ascent: int;
	chars: array of ref Fontchar;
};

Fontchar: adt {
	srcid: int;
	R: Draw->Rect;
	P: Draw->Point;
	left: int;
	width: int;
};

Idpair: adt {
	key: int;
	val: int;
	next: cyclic ref Idpair;
};

Idmap: adt {
	buckets: array of ref Idpair;

	new: fn (): ref Idmap;
	add: fn (m: self ref Idmap, key, val: int);
	del: fn (m: self ref Idmap, key: int);
	lookup: fn (m: self ref Idmap, key: int): int;
};

Imageset: adt {
	images: array of ref Image;
	ixmap: ref Idmap;
	freelist: list of int;
	new: fn (): ref Imageset;
	add: fn (s: self ref Imageset, id: int, img: ref Image);
	del: fn (s: self ref Imageset, id: int);
	lookup: fn (s: self ref Imageset, id: int): ref Image;
	findname: fn(s: self ref Imageset, name: string): ref Image;
};

Screenset: adt {
	screens: array of ref Screen;
	ixmap: ref Idmap;
	freelist: list of int;
	new: fn (): ref Screenset;
	add: fn (s: self ref Screenset, scr: ref Screen);
	del: fn (s: self ref Screenset, id: int);
	lookup: fn (s: self ref Screenset, id: int): ref Screen;
};


Drawreq: adt {
	data: array of byte;
	pick {
#	a =>	# allocate image
#		id: int;
#		screenid: int;
#		refresh: int;
#		ldepth: int;
#		repl: int;
#		R: Draw->Rect;
#		clipR: Draw->Rect;
#		value: int;
	b =>	# new allocate image
		id: int;
		screenid: int;
		refresh: int;
		chans: Draw->Chans;
		repl: int;
		R: 	Draw->Rect;
		clipR: Draw->Rect;
		rrggbbaa: int;
	A => # allocate screen
		id: int;
		imageid: int;
		fillid: int;
	c => # set clipr and repl
		dstid: int;
		repl: int;
		clipR: Draw->Rect;
#	x => # move cursor
#	C => # set cursor image and hotspot
#		_: int;
	d => # general draw op
		dstid: int;
		srcid: int;
		maskid: int;
	D => # debug mode
		_: int;
	e => # draw ellipse
		dstid: int;
		srcid: int;
	f => # free image
		id: int;
		img: ref Image;	# helper for Viewers
	F =>	 # free screen
		id: int;
	i => # convert image to font
		fontid: int;
		nchars: int;
		ascent: int;
	l => # load a char into font
		fontid: int;
		srcid: int;
		index: int;
		R: Draw->Rect;
		P: Draw->Point;
		left: int;
		width: int;
	L => # draw line
		dstid: int;
		srcid: int;
	n =>	# attach to named image
		dstid: int;
		name: string;
	N => # name image
		dstid: int;
		in: int;
		name: string;
	o =>	# set window origins
		id: int;
		rmin: Draw->Point;
		screenrmin: Draw->Point;
	O => # set next compositing op
		op: int;
	p =>	# draw polygon
		dstid: int;
		srcid: int;
	r =>	# read pixels
		id: int;
		R: Draw->Rect;
	s =>	# draw text
		dstid: int;
		srcid: int;
		fontid: int;
	x => # draw text with bg
		dstid: int;
		srcid: int;
		fontid: int;
		bgid: int;
	S =>	# import public screen
	t =>	# adjust window z order
		top: int;
		ids: array of int;
	v =>	 # flush updates to display
	y =>	# write pixels
		id: int;
		R: Draw->Rect;
	}
};

getreq(data : array of byte, ix : int) : (ref Drawreq, string)
{
	mlen := 0;
	err := "short draw message";
	req : ref Drawreq;

	case int data[ix] {
	'b' => # alloc image
		mlen = 1+4+4+1+4+1+(4*4)+(4*4)+4;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.b;
			r.data = data;
			r.id = get4(data, OPb_id);
			r.screenid = get4(data, OPb_screenid);
			r.refresh = get1(data, OPb_refresh);
			r.chans = Draw->Chans(get4(data, OPb_chans));
			r.repl = get1(data, OPb_repl);
			r.R = getR(data, OPb_R);
			r.clipR = getR(data, OPb_clipR);
			r.rrggbbaa = get4(data, OPb_rrggbbaa);
			req = r;
		}
	'A' => # alloc screen
		mlen = 1+4+4+4+1;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.A;
			r.data = data;
			r.id = get4(data, OPA_id);
			r.imageid = get4(data, OPA_imageid);
			r.fillid = get4(data, OPA_fillid);
			req = r;
		}
	'c' => # set clipR
		mlen = 1+4+1+(4*4);
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.c;
			r.data = data;
			r.dstid = get4(data, OPc_dstid);
			r.repl = get1(data, OPc_repl);
			r.clipR = getR(data, OPc_clipR);
			req = r;
		}
	'd' => # draw
		mlen = 1+4+4+4+(4*4)+(2*4)+(2*4);
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.d;
			r.data = data;
			r.dstid = get4(data, OPd_dstid);
			r.srcid = get4(data, OPd_srcid);
			r.maskid = get4(data, OPd_maskid);
			req = r;
		}
	'D' =>
		# debug mode
		mlen = 1+1;
		if (mlen+ix <= len data) {
			req = ref Drawreq.v;
			req.data = data[ix:ix+mlen];
		}
	'e' or
	'E' => # ellipse
		mlen = 1+4+4+(2*4)+4+4+4+(2*4)+4+4;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.e;
			r.data = data;
			r.dstid = get4(data, OPe_dstid);
			r.srcid = get4(data, OPe_srcid);
			req = r;
		}
	'f' => # free image
		mlen = 1+4;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.f;
			r.data = data;
			r.id = get4(data, OPf_id);
			req = r;
		}
	'F' => # free screen
		mlen = 1+4;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.f;
			r.data = data;
			r.id = get4(data, OPF_id);
			req = r;
		}
	'i' =>	 # alloc font
		mlen = 1+4+4+1;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.i;
			r.data = data;
			r.fontid = get4(data, OPi_fontid);
			r.nchars = get4(data, OPi_nchars);
			r.ascent = get1(data, OPi_ascent);
			req = r;
		}
	'l' =>	 # load font char
		mlen = 1+4+4+2+(4*4)+(2*4)+1+1;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.l;
			r.data = data;
			r.fontid = get4(data, OPl_fontid);
			r.srcid = get4(data, OPl_srcid);
			r.index = get2(data, OPl_index);
			r.R = getR(data, OPl_R);
			r.P = getP(data, OPl_P);
			r.left = get1(data, OPl_left);
			r.width = get1(data, OPl_width);
			req = r;
		}
	'L' => # line
		mlen = 1+4+(2*4)+(2*4)+4+4+4+4+(2*4);
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.L;
			r.data = data;
			r.dstid = get4(data, OPL_dstid);
			r.srcid = get4(data, OPL_srcid);
			req = r;
		}
	'n' => # attach to named image
		mlen = 1+4+1;
		if (mlen+ix < len data) {
			mlen += get1(data, ix+OPn_j);
			if (mlen+ix <= len data) {
				data = data[ix:ix+mlen];
				r := ref Drawreq.n;
				r.data = data;
				r.dstid = get4(data, OPn_dstid);
				r.name = string data[OPn_name:];
				req = r;
			}
		}
	'N' => # name image
		mlen = 1+4+1+1;
		if (mlen+ix < len data) {
			mlen += get1(data, ix+OPN_j);
			if (mlen+ix <= len data) {
				data = data[ix:ix+mlen];
				r := ref Drawreq.N;
				r.data = data;
				r.dstid = get4(data, OPN_dstid);
				r.in = get1(data, OPN_in);
				r.name = string data[OPN_name:];
				req = r;
			}
		}
	'o' => # set origins
		mlen = 1+4+(2*4)+(2*4);
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.o;
			r.data = data;
			r.id = get4(data, OPo_id);
			r.rmin = getP(data, OPo_rmin);
			r.screenrmin = getP(data, OPo_screenrmin);
			req = r;
		}
	'O' => # set next compop
		mlen = 1+1;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.O;
			r.data = data;
			r.op = get1(data, OPO_op);
			req = r;
		}
	'p' or
	'P' => # polygon
		mlen = 1+4+2+4+4+4+4+(2*4);
		if (mlen + ix <= len data) {
			n := get2(data, ix+OPp_n);
			nb := coordslen(data, ix+OPp_P0, 2*(n+1));
			if (nb == -1)
				err = "bad coords";
			else {
				mlen += nb;
				if (mlen+ix <= len data) {
					data = data[ix:ix+mlen];
					r := ref Drawreq.p;
					r.data = data;
					r.dstid = get4(data, OPp_dstid);
					r.srcid = get4(data, OPp_srcid);
					req = r;
				}
			}
		}
	'r' =>	 # read pixels
		mlen = 1+4+(4*4);
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			r := ref Drawreq.r;
			r.data = data;
			r.id = get4(data, OPr_id);
			r.R = getR(data, OPr_R);
			req = r;
		}
	's' => # text
		mlen = 1+4+4+4+(2*4)+(4*4)+(2*4)+2;
		if (ix+mlen <= len data) {
			ni := get2(data, ix+OPs_ni);
			mlen += (2*ni);
			if (mlen+ix <= len data) {
				data = data[ix:ix+mlen];
				r := ref Drawreq.s;
				r.data = data;
				r.dstid = get4(data, OPs_dstid);
				r.srcid = get4(data, OPs_srcid);
				r.fontid = get4(data, OPs_fontid);
				req = r;
			}
		}
	'x' => # text with bg img
		mlen = 1+4+4+4+(2*4)+(4*4)+(2*4)+2+4+(2*4);
		if (ix+mlen <= len data) {
			ni := get2(data, ix+OPx_ni);
			mlen += (2*ni);
			if (mlen+ix <= len data) {
				data = data[ix:ix+mlen];
				r := ref Drawreq.x;
				r.data = data;
				r.dstid = get4(data, OPx_dstid);
				r.srcid = get4(data, OPx_srcid);
				r.fontid = get4(data, OPx_fontid);
				r.bgid = get4(data, OPx_bgid);
				req = r;
			}
		}
	'S' => # import public screen
		mlen = 1+4+4;
		if (mlen+ix <= len data) {
			data = data[ix:ix+mlen];
			req = ref Drawreq.S;
			req.data = data;
		}
	't' => # adjust window z order
		mlen = 1+1+2;
		if (ix+mlen<= len data) {
			nw := get2(data, ix+OPt_nw);
			mlen += (4*nw);
			if (mlen+ix <= len data) {
				data = data[ix:ix+mlen];
				r := ref Drawreq.t;
				r.data = data;
				r.top = get1(data, OPt_top);
				r.ids = array [nw] of int;
				for (n := 0; n < nw; n++)
					r.ids[n] = get4(data, OPt_id + 4*n);
				req = r;
			}
		}
	'v' => # flush
		req = ref Drawreq.v;
		req.data = data[ix:ix+1];
	'y' or
	'Y' =>	# write pixels
		mlen = 1+4+(4*4);
		if (ix+mlen <= len data) {
			imgid := get4(data, ix+OPy_id);
			img := images.lookup(imgid);
			compd := data[ix] == byte 'Y';
			r := getR(data, ix+OPy_R);
			n := imglen(img, data, ix+mlen, r, compd);
			if (n == -1)
				err ="bad image data";
			mlen += n;
			if (mlen+ix <= len data)
				req = ref Drawreq.y (data[ix:ix+mlen], imgid, r);
		}
	* =>
		err = "bad draw command";
	}

	if (req == nil)
		return (nil, err);
	return (req, nil);
}

process(data : array of byte) : (int, string)
{
	offset := 0;
	while (offset < len data) {
		(req, err) := getreq(data, offset);
		if (err != nil)
			return (0, err);
		offset += len req.data;
		n := sys->write(drawfd, req.data, len req.data);
		if (n <= 0)
			return (n, sys->sprint("[%c] %r", int req.data[0]));

		readn := 0;
		sendtoviews := 1;

		# actions that must be done before sending to Viewers
		pick r := req {
		b =>	# allocate image
			bwidth := bytesperline(r.R, r.chans);
			img := ref Image (r.id, 0, r.screenid, r.refresh, r.chans, r.repl, r.R, r.clipR, r.rrggbbaa, nil, r.R.min, bwidth, 0, "");
			images.add(r.id, img);
			if (r.screenid != 0) {
				scr := screens.lookup(r.screenid);
				scr.addwin(r.id);
			}

		A =>	# allocate screen
			scr := ref Screen (r.id, r.imageid, r.fillid, nil);
			screens.add(scr);
			# we never allocate public screens on our Viewers
			put1(r.data, OPA_public, 0);
			dirty(r.imageid, 0);

		c =>	# set clipr and repl
			img := images.lookup(r.dstid);
			img.repl = r.repl;
			img.clipR = r.clipR;

		d =>	# general draw op
			dirty(r.dstid, 1);
			drawop = Draw->SoverD;

		e =>	# draw ellipse
			dirty(r.dstid, 1);
			drawop = Draw->SoverD;

		f => # free image
			# help out Viewers, real work is done later
			r.img = images.lookup(r.id);

		L =>	# draw line
			dirty(r.dstid, 1);
			drawop = Draw->SoverD;

		n =>	# attach to named image
			img := images.findname(r.name);
			images.add(r.dstid, img);
			
		N => # name image
			img := images.lookup(r.dstid);
			if (r.in)
				img.name = r.name;
			else
				img.name = nil;

		o => # set image origins
			img := images.lookup(r.id);
			deltax := img.lorigin.x - r.rmin.x;
			deltay := img.lorigin.y - r.rmin.y;
			w := img.R.max.x - img.R.min.x;
			h := img.R.max.y - img.R.min.y;
			
			img.R = Draw->Rect(r.screenrmin, (r.screenrmin.x + w, r.screenrmin.y + h));
			img.clipR = Draw->Rect((img.clipR.min.x - deltax, img.clipR.min.y - deltay), (img.clipR.max.x - deltax, img.clipR.max.y - deltay));
			img.lorigin = r.rmin;

		O =>	# set compositing op
			drawop = r.op;

		p =>	# draw polygon
			dirty(r.dstid, 1);
			drawop = Draw->SoverD;

		r => # read pixels
			img := images.lookup(r.id);
			bpl := bytesperline(r.R, img.chans);
			readn = bpl * (r.R.max.y - r.R.min.y);

		s =>	# draw text
			dirty(r.dstid, 1);
			drawop = Draw->SoverD;

		x => # draw text with bg
			dirty(r.dstid, 1);
			drawop = Draw->SoverD;

		t =>	# adjust window z order
			if (r.ids != nil) {
				img := images.lookup(r.ids[0]);
				scr := screens.lookup(img.screenid);
				scr.setz(r.ids, r.top);
			}

		y =>	# write pixels
			dirty(r.id, 1);
		}

		if (readn) {
			rdata := array [readn] of byte;
			if (sys->read(drawfd, rdata, readn) == readn)
				readdata = rdata;
		}

		for (vs := viewers; vs != nil; vs = tl vs) {
			v := hd vs;
			v.process(req);
		}

		# actions that must only be done after sending to Viewers
		pick r := req {
		f => # free image
			img := images.lookup(r.id);
			if (img.screenid != 0) {
				scr := screens.lookup(img.screenid);
				scr.delwin(img.id);
			}
			images.del(r.id);

		F =>	# free screen
			scr := screens.lookup(r.id);
			for (i := 0; i < len scr.windows; i++) {
				img := images.lookup(scr.windows[i]);
				img.screenid = 0;
			}
			screens.del(r.id);

		i => # convert image to font
			img := images.lookup(r.fontid);
			font := ref Font;
			font.ascent = r.ascent;
			font.chars = array[r.nchars] of ref Fontchar;
			img.font = font;

		l =>	# load a char into font
			img := images.lookup(r.fontid);
			font := img.font;
			fc := ref Fontchar(r.srcid, r.R, r.P, r.left, r.width);
			font.chars[r.index] = fc;
		}
	}
	return (offset, nil);
}

coordslen(data : array of byte, ix, n : int) : int
{
	start := ix;
	dlen := len data;
	if (ix == dlen)
		return -1;
	while (ix < dlen && n) {
		n--;
		if ((int data[ix++]) & 16r80)
			ix += 2;
	}
	if (n)
		return -1;
	return ix - start;
}


imglen(i : ref Image, data : array of byte, ix : int, r : Draw->Rect, comp : int) : int
{
	bpl := bytesperline(r, i.chans);
	if (!comp)
		return (r.max.y - r.min.y) * bpl;
	y := r.min.y;
	lineix := byteaddr(i, r.min);
	elineix := lineix+bpl;
	start := ix;
	eix := len data;
	for (;;) {
		if (lineix == elineix) {
			if (++y == r.max.y)
				break;
			lineix = byteaddr(i, Point(r.min.x, y));
			elineix = lineix+bpl;
		}
		if (ix == eix)	# buffer too small
			return -1;
		c := int data[ix++];
		if (c >= 128) {
			for (cnt := c-128+1; cnt != 0; --cnt) {
				if (ix == eix)	# buffer too small
					return -1;
				if (lineix == elineix)	# phase error
					return -1;
				lineix++;
				ix++;
			}
		} else {
			if (ix == eix)	# short buffer
				return -1;
			ix++;
			for (cnt := (c >> 2)+3; cnt != 0; --cnt) {
				if (lineix == elineix) # phase error
					return -1;
				lineix++;
			}
		}
	}
	return ix-start;
}

byteaddr(i: ref Image, p: Point): int
{
	x := p.x - i.lorigin.x;
	y := p.y - i.lorigin.y;
	bits := i.chans.depth();
	if (bits == 0)
		# invalid chans
		return 0;
	return (y*i.bwidth)+(x<<3)/bits;
}

bytesperline(r: Draw->Rect, chans: Draw->Chans): int
{
	d := chans.depth();
	l, t: int;

	if(r.min.x >= 0){
		l = (r.max.x*d+8-1)/8;
		l -= (r.min.x*d)/8;
	}else{			# make positive before divide
		t = (-r.min.x*d+8-1)/8;
		l = t+(r.max.x*d+8-1)/8;
	}
	return l;
}

get1(data : array of byte, ix : int) : int
{
	return int data[ix];
}

put1(data : array of byte, ix, val : int)
{
	data[ix] = byte val;
}

get2(data : array of byte, ix : int) : int
{
	return int data[ix] | ((int data[ix+1]) << 8);
}

put2(data : array of byte, ix, val : int)
{
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
}

get4(data : array of byte, ix : int) : int
{
	return int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
}

put4(data : array of byte, ix, val : int)
{
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
}

getP(data : array of byte, ix : int) : Draw->Point
{
	x := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
	ix += 4;
	y := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
	return Draw->Point(x, y);
}

putP(data : array of byte, ix : int, P : Draw->Point)
{
	val := P.x;
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
	val = P.y;
	ix += 4;
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
}

getR(data : array of byte, ix : int) : Draw->Rect
{
	minx := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
	ix += 4;
	miny := int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
	ix += 4;
	maxx :=  int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
	ix += 4;
	maxy :=  int data[ix] | ((int data[ix+1]) << 8) | ((int data[ix+2]) << 16) | ((int data[ix+3]) << 24);
	
	return Draw->Rect(Draw->Point(minx, miny), Draw->Point(maxx, maxy));
}

putR(data : array of byte, ix : int , R : Draw->Rect)
{
	val := R.min.x;
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
	val = R.min.y;
	ix += 4;
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
	val = R.max.x;
	ix += 4;
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
	val = R.max.y;
	ix += 4;
	data[ix] = byte val;
	data[ix+1] = byte (val >> 8);
	data[ix+2] = byte (val >> 16);
	data[ix+3] = byte (val >> 24);
}

dirty(id, v : int)
{
	img := images.lookup(id);
	img.dirty = v;
}

Screen.setz(s : self ref Screen, z : array of int, top : int)
{
	old := s.windows;
	nw := array [len old] of int;
	# use a dummy idmap to ensure uniqueness;
	ids := Idmap.new();
	ix := 0;
	if (top) {
		for (i := 0; i < len z; i++) {
			if (ids.lookup(z[i]) == -1) {
				ids.add(z[i], 0);
				nw[ix++] = z[i];
			}
		}
	}
	for (i := 0; i < len old; i++) {
		if (ids.lookup(old[i]) == -1) {
			ids.add(old[i], 0);
			nw[ix++] = old[i];
		}
	}
	if (!top) {
		for (i = 0; i < len z; i++) {
			if (ids.lookup(z[i]) == -1) {
				ids.add(z[i], 0);
				nw[ix++] = z[i];
			}
		}
	}
	s.windows = nw;
}

Screen.addwin(s : self ref Screen, wid : int)
{
	nw :=  array [len s.windows + 1] of int;
	nw[0] = wid;
	nw[1:] = s.windows;
	s.windows = nw;
}

Screen.delwin(s : self ref Screen, wid : int)
{
	if (len s.windows == 1) {
		# assert s.windows[0] == wid
		s.windows = nil;
		return;
	}
	nw := array [len s.windows - 1] of int;
	ix := 0;
	for (i := 0; i < len s.windows; i++) {
		if (s.windows[i] == wid)
			continue;
		nw[ix++] = s.windows[i];
	}
	s.windows = nw;
}

Idmap.new() : ref Idmap
{
	m := ref Idmap;
	m.buckets = array[256] of ref Idpair;
	return m;
}

Idmap.add(m : self ref Idmap, key, val : int)
{
	h := key & 16rff;
	m.buckets[h] = ref Idpair (key, val, m.buckets[h]);
}

Idmap.del(m : self ref Idmap, key : int)
{
	h := key &16rff;
	prev := m.buckets[h];
	if (prev == nil)
		return;
	if (prev.key == key) {
		m.buckets[h] = m.buckets[h].next;
		return;
	}
	for (idp := prev.next; idp != nil; idp = idp.next) {
		if (idp.key == key)
			break;
		prev = idp;
	}
	if (idp != nil)
		prev.next = idp.next;
}

Idmap.lookup(m :self ref Idmap, key : int) : int
{
	h := key &16rff;
	for (idp := m.buckets[h]; idp != nil; idp = idp.next) {
		if (idp.key == key)
			return idp.val;
	}
	return -1;
}

Imageset.new() : ref Imageset
{
	s := ref Imageset;
	s.images = array [32] of ref Image;
	s.ixmap = Idmap.new();
	for (i := 0; i < len s.images; i++)
		s.freelist = i :: s.freelist;
	return s;
}

Imageset.add(s: self ref Imageset, id: int, img: ref Image)
{
	if (s.freelist == nil) {
		n := 2 * len s.images;
		ni := array [n] of ref Image;
		ni[:] = s.images;
		for (i := len s.images; i < n; i++)
			s.freelist = i :: s.freelist;
		s.images = ni;
	}
	ix := hd s.freelist;
	s.freelist = tl s.freelist;
	s.images[ix] = img;
	s.ixmap.add(id, ix);
	img.refc++;
}

Imageset.del(s: self ref Imageset, id: int)
{
	ix := s.ixmap.lookup(id);
	if (ix == -1)
		return;
	img := s.images[ix];
	if (img != nil)
		img.refc--;
	s.images[ix] = nil;
	s.freelist = ix :: s.freelist;
	s.ixmap.del(id);
}

Imageset.lookup(s : self ref Imageset, id : int ) : ref Image
{
	ix := s.ixmap.lookup(id);
	if (ix == -1)
		return nil;
	return s.images[ix];
}

Imageset.findname(s: self ref Imageset, name: string): ref Image
{
	for (ix := 0; ix < len s.images; ix++) {
		img := s.images[ix];
		if (img != nil && img.name == name)
			return img;
	}
	return nil;
}

Screenset.new() : ref Screenset
{
	s := ref Screenset;
	s.screens = array [32] of ref Screen;
	s.ixmap = Idmap.new();
	for (i := 0; i < len s.screens; i++)
		s.freelist = i :: s.freelist;
	return s;
}

Screenset.add(s : self ref Screenset, scr : ref Screen)
{
	if (s.freelist == nil) {
		n := 2 * len s.screens;
		ns := array [n] of ref Screen;
		ns[:] = s.screens;
		for (i := len s.screens; i < n; i++)
			s.freelist = i :: s.freelist;
		s.screens = ns;
	}
	ix := hd s.freelist;
	s.freelist = tl s.freelist;
	s.screens[ix] = scr;
	s.ixmap.add(scr.id, ix);
}

Screenset.del(s : self ref Screenset, id : int)
{
	ix := s.ixmap.lookup(id);
	if (ix == -1)
		return;
	s.screens[ix] = nil;
	s.freelist = ix :: s.freelist;
	s.ixmap.del(id);
}

Screenset.lookup(s : self ref Screenset, id : int ) : ref Screen
{
	ix := s.ixmap.lookup(id);
	if (ix == -1)
		return nil;
	return s.screens[ix];
}


Viewer : adt {
	imgmap:	ref Idmap;
	scrmap:	ref Idmap;
	chanmap:	ref Idmap;	# maps to 1 for images that require chan conversion

	imageid:	int;
	screenid:	int;
	whiteid:	int;
	hungup:	int;
	dchans:	Draw->Chans;	# chans.desc of remote display img

	# temporary image for chan conversion
	tmpid:	int;
	tmpR:	Draw->Rect;

	output:	chan of (array of byte, chan of string);

	new:		fn(fd: ref Sys->FD, pubscr: int, chans: Draw->Chans): string;
	process:	fn(v: self ref Viewer, req: ref Drawreq);
	getimg:	fn(v: self ref Viewer, id: int): int;
	getscr:	fn(v: self ref Viewer, id, win: int): (int, int);
	copyimg:	fn(v: self ref Viewer, img: ref Image, id: int);
	chanconv:	fn(v: self ref Viewer, img: ref Image, id: int, r: Rect, ymsg: array of byte);
};

vwriter(fd : ref Sys->FD, datac : chan of array of byte, nc : chan of string)
{
	for (;;) {
		data := <- datac;
		if (data == nil)
			return;
		n := sys->write(fd, data, len data);
		if (n != len data) {
#			sys->print("[%c]: %r\n", int data[0]);
#			sys->print("[%c] datalen %d got %d error: %r\n", int data[0], len data, n);
			nc <-= sys->sprint("%r");
		} else {
#			sys->print("[%c]", int data[0]);
			nc <-= nil;
		}
	}
}

vbmsg : adt {
	data : array of byte;
	rc : chan of string;
	next : cyclic ref vbmsg;
};

vbuffer(v : ref Viewer, fd : ref Sys->FD)
{
	ioc := v.output;
	datac := chan of array of byte;
	errc := chan of string;
	spawn vwriter(fd, datac, errc);
	fd = nil;

	msghd : ref vbmsg;
	msgtl : ref vbmsg;

Loop:
	for (;;) alt {
	(data, rc) := <- ioc =>
		if (data == nil)
			break Loop;
		if (msgtl != nil) {
			if (msgtl != msghd && msgtl.rc == nil && (len msgtl.data + len data) <= Sys->ATOMICIO) {
				ndata := array [len msgtl.data + len data] of byte;
				ndata[:] = msgtl.data;
				ndata[len msgtl.data:] = data;
				msgtl.data = ndata;
				msgtl.rc = rc;
			} else {
				msgtl.next = ref vbmsg (data, rc, nil);
				msgtl = msgtl.next;
			}
		} else {
			msghd = ref vbmsg (data, rc, nil);
			msgtl = msghd;
			datac <-= data;
		}
	err := <- errc =>
		if (msghd.rc != nil)
			msghd.rc <- = err;
		msghd = msghd.next;
		if (msghd != nil)
			datac <-= msghd.data;
		else
			msgtl = nil;
		if (err == Ehungup) {
			nhangups++;
			v.hungup = 1;
		}
	}
	# shutdown vwriter (may be blocked sending on errc)
	for (;;) alt {
	<- errc =>
		;
	datac <- = nil =>
		return;
	}
}

Viewer.new(fd: ref Sys->FD, pubscr: int, chans: Draw->Chans): string
{
	v := ref Viewer;
	v.output = chan of (array of byte, chan of string);
	spawn vbuffer(v, fd);

	v.imgmap = Idmap.new();
	v.scrmap = Idmap.new();
	v.chanmap = Idmap.new();
	v.imageid = 0;
	v.screenid = pubscr;
	v.hungup = 0;
	v.dchans = chans;
	v.tmpid = 0;
	v.tmpR = Rect((0,0), (0,0));

#D := array[1+1] of byte;
#D[0] = byte 'D';
#D[1] = byte 1;
#v.output <-= (D, nil);

	reply := chan of string;
	# import remote public screen into our remote draw client
	S := array [1+4+4] of byte;
	S[0] = byte 'S';
	put4(S, OPS_id, pubscr);
	put4(S, OPS_chans, chans.desc);
	v.output <-= (S, reply);
	err := <- reply;
	if (err != nil) {
		v.output <-= (nil, nil);
		return err;
	}

	# create remote window
	dispid := ++v.imageid;
	b := array [1+4+4+1+4+1+(4*4)+(4*4)+4] of byte;
	b[0] = byte 'b';
	put4(b, OPb_id, dispid);
	put4(b, OPb_screenid, pubscr);
	put1(b, OPb_refresh, 0);
	put4(b, OPb_chans, chans.desc);
	put1(b, OPb_repl, 0);
	putR(b, OPb_R, drawR);
	putR(b, OPb_clipR, drawR);
	put4(b, OPb_rrggbbaa, Draw->White);
	v.output <-= (b, reply);
	err = <- reply;
	if (err != nil) {
		v.output <-= (nil, nil);
		return err;
	}

	# map local display image id to remote window image id
	v.imgmap.add(0, dispid);
	if (!drawchans.eq(chans))
		# writepixels on this image must be chan converted
		v.chanmap.add(0, 1);

	# create 'white' repl image for use as mask
	v.whiteid = ++v.imageid;
	put4(b, OPb_id, v.whiteid);
	put4(b, OPb_screenid, 0);
	put1(b, OPb_refresh, 0);
	put4(b, OPb_chans, (Draw->RGBA32).desc);
	put1(b, OPb_repl, 1);
	putR(b, OPb_R, Rect((0,0), (1,1)));
	putR(b, OPb_clipR, Rect((-16r3FFFFFFF, -16r3FFFFFFF), (16r3FFFFFFF, 16r3FFFFFFF)));
	put4(b, OPb_rrggbbaa, Draw->White);
	v.output <-= (b, reply);
	err = <- reply;
	if (err != nil) {
		v.output <-= (nil, nil);
		return err;
	}

	img := images.lookup(0);
	key := chan of int;
	drawlock <- = key;
	v.copyimg(img, dispid);

	O := array [1+1] of byte;
	O[0] = byte 'O';
	O[1] = byte drawop;
	v.output <-= (O, nil);

	flush := array [1] of byte;
	flush[0] = byte 'v';
	v.output <- = (flush, nil);
	viewers = v :: viewers;
	key <-= 1;
	return nil;
}

Viewer.process(v : self ref Viewer, req : ref Drawreq)
{
	data := req.data;
	pick r := req {
	b => # allocate image
		imgid := ++v.imageid;
		if (r.screenid != 0) {
			(scrid, mapchans) := v.getscr(r.screenid, 0);
			put4(data, OPb_screenid, scrid);
			if (mapchans) {
				put4(data, OPb_chans, v.dchans.desc);
				v.chanmap.add(r.id, 1);
			}
		}
		v.imgmap.add(r.id, imgid);
		put4(data, OPb_id, imgid);

	A => # allocate screen
		imgid := v.getimg(r.imageid);
		put4(data, OPA_fillid, v.getimg(r.fillid));
		put4(data, OPA_imageid, imgid);
		reply := chan of string;
		for (i := 0; i < 25; i++) {
			put4(data, OPA_id, ++v.screenid);
			v.output <-= (data, reply);
			if (<-reply == nil) {
				v.scrmap.add(r.id, v.screenid);
				return;
			}
		}
		return;

	c => # set clipr and repl
		put4(data, OPc_dstid, v.getimg(r.dstid));

	d =>	 # general draw op
		dstid := v.imgmap.lookup(r.dstid);
		if (dstid == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.dstid);
			return;
		}
		put4(data, OPd_maskid, v.getimg(r.maskid));
		put4(data, OPd_srcid, v.getimg(r.srcid));
		put4(data, OPd_dstid, dstid);

	e =>	 # draw ellipse
		dstid := v.imgmap.lookup(r.dstid);
		if (dstid == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.dstid);
			return;
		}
		put4(data, OPe_srcid, v.getimg(r.srcid));
		put4(data, OPe_dstid, dstid);

	f => # free image
		id := v.imgmap.lookup(r.img.id);
		if (id == -1)
			# Viewer has never seen this image - ignore
			return;
		v.imgmap.del(r.id);
		# Viewers alias named images - only delete if last reference
		if (r.img.refc > 1)
			return;
		v.chanmap.del(r.img.id);
		put4(data, OPf_id, id);

	F => # free screen
		id := v.scrmap.lookup(r.id);
		scr := screens.lookup(r.id);
		# image and fill are free'd separately
		#v.imgmap.del(scr.imageid);
		#v.imgmap.del(scr.fillid);
		if (id == -1)
			return;
		put4(data, OPF_id, id);

	i => # convert image to font
		put4(data, OPi_fontid, v.getimg(r.fontid));

	l => # load a char into font
		put4(data, OPl_srcid, v.getimg(r.srcid));
		put4(data, OPl_fontid, v.getimg(r.fontid));

	L => # draw line
		dstid := v.imgmap.lookup(r.dstid);
		if (dstid == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.dstid);
			return;
		}
		put4(data, OPL_srcid, v.getimg(r.srcid));
		put4(data, OPL_dstid, dstid);

#	n =>	# attach to named image
#	N =>	# name
#		Handled by id remapping to avoid clashes in namespace of remote viewers.
#		If it is a name we know then the id is remapped within the images Imageset
#		Otherwise, there is nothing we can do other than ignore all ops related to the id.

	o =>	 # set image origins
		id := v.imgmap.lookup(r.id);
		if (id == -1)
			# Viewer has never seen this image - ignore
			return;
		put4(data, OPo_id, id);

	O =>	# set next compositing op
		;

	p =>	 # draw polygon
		dstid := v.imgmap.lookup(r.dstid);
		if (dstid == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.dstid);
			return;
		}
		put4(data, OPp_srcid, v.getimg(r.srcid));
		put4(data, OPp_dstid, dstid);

	s => # draw text
		dstid := v.imgmap.lookup(r.dstid);
		if (dstid == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.dstid);
			return;
		}
		put4(data, OPs_fontid, v.getimg(r.fontid));
		put4(data, OPs_srcid, v.getimg(r.srcid));
		put4(data, OPs_dstid, dstid);

	x =>	# draw text with bg
		dstid := v.imgmap.lookup(r.dstid);
		if (dstid == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.dstid);
			return;
		}
		put4(data, OPx_fontid, v.getimg(r.fontid));
		put4(data, OPx_srcid, v.getimg(r.srcid));
		put4(data, OPx_bgid, v.getimg(r.bgid));
		put4(data, OPx_dstid, dstid);

	t => # adjust window z order
		for (i := 0; i < len r.ids; i++)
			put4(data, OPt_id + 4*i, v.getimg(r.ids[i]));

	v => # flush updates to display
			;

	y => # write pixels
		id := v.imgmap.lookup(r.id);
		if (id == -1) {
			# don't do draw op as getimg() will do a writepixels
			v.getimg(r.id);
			return;
		}
		if (!drawchans.eq(v.dchans) && v.chanmap.lookup(r.id) != -1) {
			# chans clash
			img := images.lookup(r.id);
			# copy data as other Viewers may alter contents
			copy := (array [len data] of byte)[:] = data;
			v.chanconv(img, id, r.R, copy);
			return;
		}
		put4(data, OPy_id, id);

	* =>
		return;
	}
	# send out a copy of the data as other Viewers may alter contents
	copy := array [len data] of byte;
	copy[:] = data;
	v.output <-= (copy, nil);
}

Viewer.getimg(v: self ref Viewer, localid: int) : int
{
	remid := v.imgmap.lookup(localid);
	if (remid != -1)
		return remid;

	img := images.lookup(localid);
	if (img.id != localid) {
		# attached via name, see if we have the aliased image
		remid = v.imgmap.lookup(img.id);
		if (remid != -1) {
			# we have it, add mapping to save us this trouble next time
			v.imgmap.add(localid, remid);
			return remid;
		}
	}
	# is the image a window?
	scrid := 0;
	mapchans := 0;
	if (img.screenid != 0)
		(scrid, mapchans) = v.getscr(img.screenid, img.id);

	vid := ++v.imageid;
	# create the image
	# note: clipr for image creation has to be based on screen co-ords
	clipR := img.clipR.subpt(img.lorigin);
	clipR = clipR.addpt(img.R.min);
	b := array [1+4+4+1+4+1+(4*4)+(4*4)+4] of byte;
	b[0] = byte 'b';
	put4(b, OPb_id, vid);
	put4(b, OPb_screenid, scrid);
	put1(b, OPb_refresh, 0);
	if (mapchans)
		put4(b, OPb_chans, v.dchans.desc);
	else
		put4(b, OPb_chans, img.chans.desc);
	put1(b, OPb_repl, img.repl);
	putR(b, OPb_R, img.R);
	putR(b, OPb_clipR, clipR);
	put4(b, OPb_rrggbbaa, img.rrggbbaa);
	v.output <-= (b, nil);

	v.imgmap.add(img.id, vid);
	if (mapchans)
		v.chanmap.add(img.id, 1);

	# set the origin
	if (img.lorigin.x != img.R.min.x || img.lorigin.y != img.R.min.y) {
		o := array [1+4+(2*4)+(2*4)] of byte;
		o[0] = byte 'o';
		put4(o, OPo_id, vid);
		putP(o, OPo_rmin, img.lorigin);
		putP(o, OPo_screenrmin, img.R.min);
		v.output <-= (o, nil);
	}

	# is the image a font?
	if (img.font != nil) {
		f := img.font;
		i := array [1+4+4+1] of byte;
		i[0] = byte 'i';
		put4(i, OPi_fontid, vid);
		put4(i, OPi_nchars, len f.chars);
		put1(i, OPi_ascent, f.ascent);
		v.output <-= (i, nil);
	
		for (index := 0; index < len f.chars; index++) {
			ch := f.chars[index];
			if (ch == nil)
				continue;
			l := array [1+4+4+2+(4*4)+(2*4)+1+1] of byte;
			l[0] = byte 'l';
			put4(l, OPl_fontid, vid);
			put4(l, OPl_srcid, v.getimg(ch.srcid));
			put2(l, OPl_index, index);
			putR(l, OPl_R, ch.R);
			putP(l, OPl_P, ch.P);
			put1(l, OPl_left, ch.left);
			put1(l, OPl_width, ch.width);
			v.output <-= (l, nil);
		}
	}

	# if 'dirty' then writepixels
	if (img.dirty)
		v.copyimg(img, vid);

	return vid;
}

Viewer.copyimg(v : self ref Viewer, img : ref Image, id : int)
{
	dx := img.R.max.x - img.R.min.x;
	dy := img.R.max.y - img.R.min.y;
	srcR := Rect (img.lorigin, (img.lorigin.x + dx, img.lorigin.y + dy));
	bpl := bytesperline(srcR, img.chans);
	rlen : con 1+4+(4*4);
	ystep := (Sys->ATOMICIO - rlen)/ bpl;
	minx := srcR.min.x;
	maxx := srcR.max.x;
	maxy := srcR.max.y;

	chanconv := 0;
	if (!drawchans.eq(v.dchans) && v.chanmap.lookup(img.id) != -1)
		chanconv = 1;

	for (y := img.lorigin.y; y < maxy; y += ystep) {
		if (y + ystep > maxy)
			ystep = (maxy - y);
		R := Draw->Rect((minx, y), (maxx, y+ystep));
		r := array [rlen] of byte;
		r[0] = byte 'r';
		put4(r, OPr_id, img.id);
		putR(r, OPr_R, R);
		if (sys->write(drawfd, r, len r) != len r)
			break;

		nb := bpl * ystep;
		ymsg := array [1+4+(4*4)+nb] of byte;
		ymsg[0] = byte 'y';
#		put4(ymsg, OPy_id, id);
		putR(ymsg, OPy_R, R);
		n := sys->read(drawfd, ymsg[OPy_data:], nb);
		if (n != nb)
			break;
		if (chanconv)
			v.chanconv(img, id, R, ymsg);
		else {
			put4(ymsg, OPy_id, id);
			v.output <-= (ymsg, nil);
		}
	}
}

Viewer.chanconv(v: self ref Viewer, img: ref Image, id: int, r: Rect, ymsg: array of byte)
{
	# check origin matches and enough space in conversion image
	if (!(img.lorigin.eq(v.tmpR.min) && r.inrect(v.tmpR))) {
		# create new tmp image
		if (v.tmpid != 0) {
			f := array [1+4] of byte;
			f[0] = byte 'f';
			put4(f, OPf_id, v.tmpid);
			v.output <-= (f, nil);
		}
		v.tmpR = Rect((0,0), (img.R.dx(), img.R.dy())).addpt(img.lorigin);
		v.tmpid = ++v.imageid;
		b := array [1+4+4+1+4+1+(4*4)+(4*4)+4] of byte;
		b[0] = byte 'b';
		put4(b, OPb_id, v.tmpid);
		put4(b, OPb_screenid, 0);
		put1(b, OPb_refresh, 0);
		put4(b, OPb_chans, drawchans.desc);
		put1(b, OPb_repl, 0);
		putR(b, OPb_R, v.tmpR);
		putR(b, OPb_clipR, v.tmpR);
		put4(b, OPb_rrggbbaa, Draw->Nofill);
		v.output <-= (b, nil);
	}
	# writepixels to conversion image
	put4(ymsg, OPy_id, v.tmpid);
	v.output <-= (ymsg, nil);

	# ensure that drawop is Draw->S
	if (drawop != Draw->S) {
		O := array [1+1] of byte;
		O[0] = byte 'O';
		put1(O, OPO_op, Draw->S);
		v.output <-= (O, nil);
	}
	# blit across to real target
	d := array [1+4+4+4+(4*4)+(2*4)+(2*4)] of byte;
	d[0] = byte 'd';
	put4(d, OPd_dstid, id);
	put4(d, OPd_srcid, v.tmpid);
	put4(d, OPd_maskid, v.whiteid);
	putR(d, OPd_R, r);
	putP(d, OPd_P0, r.min);
	putP(d, OPd_P1, r.min);
	v.output <-= (d, nil);

	# restore drawop if necessary
	if (drawop != Draw->S) {
		O := array [1+1] of byte;
		O[0] = byte 'O';
		put1(O, OPO_op, drawop);
		v.output <-= (O, nil);
	}
}

# returns (rid, map)
# rid == remote screen id
# map indicates that chan mapping is required for windows on this screen

Viewer.getscr(v : self ref Viewer, localid, winid : int) : (int, int)
{
	remid := v.scrmap.lookup(localid);
	if (remid != -1) {
		if (drawchans.eq(v.dchans))
			return (remid, 0);
		scr := screens.lookup(localid);
		if (v.chanmap.lookup(scr.imageid) == -1)
			return (remid, 0);
		return (remid, 1);
	}

	scr := screens.lookup(localid);
	imgid := v.getimg(scr.imageid);
	fillid := v.getimg(scr.fillid);
	A := array [1+4+4+4+1] of byte;
	A[0] = byte 'A';
	put4(A, OPA_imageid, imgid);
	put4(A, OPA_fillid, fillid);
	put1(A, OPA_public, 0);

	reply := chan of string;
	for (i := 0; i < 25; i++) {
		put4(A, OPA_id, ++v.screenid);
		v.output <-= (A, reply);
		if (<-reply != nil)
			continue;
		v.scrmap.add(localid, v.screenid);
		break;
	}
	# if i == 25 then we have a problem
	# ...
	if (i == 25) {
#		sys->print("failed to create remote screen\n");
		return (0, 0);
	}

	# pre-construct the windows on this screen
	for (ix := len scr.windows -1; ix >=0; ix--)
		if (scr.windows[ix] != winid)
			v.getimg(scr.windows[ix]);

	if (drawchans.eq(v.dchans))
		return (v.screenid, 0);
	if (v.chanmap.lookup(scr.imageid) == -1)
		return (v.screenid, 0);
	return (v.screenid, 1);
}