summary refs log tree commit diff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rw-r--r--bin/.gitignore2
-rw-r--r--bin/Makefile26
-rw-r--r--bin/README10
-rw-r--r--bin/bin.722
-rw-r--r--bin/brot.c151
-rw-r--r--bin/gfx-cocoa.m162
-rw-r--r--bin/gfx-fb.c87
-rw-r--r--bin/gfx-x11.c135
-rw-r--r--bin/gfx.h24
-rw-r--r--bin/gfxx.c470
-rw-r--r--bin/man1/brot.163
-rw-r--r--bin/man1/gfxx.1138
12 files changed, 1284 insertions, 6 deletions
diff --git a/bin/.gitignore b/bin/.gitignore
index 5171e93d..b7d25952 100644
--- a/bin/.gitignore
+++ b/bin/.gitignore
@@ -1,9 +1,11 @@
 *.o
 atch
 bri
+brot
 dtch
 fbatt
 fbclock
+gfxx
 glitch
 hnel
 modem
diff --git a/bin/Makefile b/bin/Makefile
index 9ac12f16..655fc4e0 100644
--- a/bin/Makefile
+++ b/bin/Makefile
@@ -1,5 +1,11 @@
 PREFIX = ~/.local
 MANDIR = $(PREFIX)/share/man
+GFX = cocoa
+
+CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
+LDLIBS = -lm -lutil -lz
+LDLIBS_cocoa = $(LDLIBS) -framework Cocoa
+LDLIBS_x11 = $(LDLIBS) -lX11
 
 BINS += atch
 BINS += dtch
@@ -24,11 +30,13 @@ BINS_LINUX += fbatt
 BINS_LINUX += fbclock
 BINS_LINUX += psfed
 
-BINS_ALL = $(BINS) $(BINS_BSD) $(BINS_LINUX)
+BINS_GFX += brot
+BINS_GFX += gfxx
+
+BINS_ALL = $(BINS) $(BINS_BSD) $(BINS_LINUX) $(BINS_GFX)
 MAN1_ALL = $(BINS_ALL:%=man1/%.1)
 
-CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
-LDLIBS = -lm -lutil -lz
+GFX_OBJ = gfx-$(GFX).o
 
 any: .gitignore tags $(BINS)
 
@@ -36,19 +44,29 @@ bsd: any $(BINS_BSD)
 
 linux: any $(BINS_LINUX)
 
+gfx: any $(BINS_GFX)
+
 .o:
 	$(CC) $(LDFLAGS) $< $(LDLIBS) -o $@
 
 scheme.h: scheme
 	./scheme -c > scheme.h
 
+brot.o gfxx.o gfx-cocoa.o gfx-fb.o gfx-x11.o: gfx.h
+
 fbatt.o fbclock.o: scheme.h
 
-psf2png.o scheme.o: png.h
+gfxx.o psf2png.o scheme.o: png.h
 
 atch: dtch
 	ln -f dtch atch
 
+brot: brot.o $(GFX_OBJ)
+	$(CC) $(LDFLAGS) $@.o $(GFX_OBJ) $(LDLIBS_$(GFX)) -o $@
+
+gfxx: gfxx.o $(GFX_OBJ)
+	$(CC) $(LDFLAGS) $@.o $(GFX_OBJ) $(LDLIBS_$(GFX)) -o $@
+
 open pbcopy pbpaste: pbd
 	ln -f pbd $@
 
diff --git a/bin/README b/bin/README
index 0de6d849..a0353b53 100644
--- a/bin/README
+++ b/bin/README
@@ -8,9 +8,11 @@ DESCRIPTION
      target Linux.
 
      bri(1)      backlight brightness control
+     brot(1)     Mandelbrot renderer
      dtch(1)     detached sessions
      fbatt(1)    framebuffer battery indicator
      fbclock(1)  framebuffer clock
+     gfxx(1)     graphics data explorer
      glitch(1)   PNG glitcher
      hnel(1)     PTY input remapper
      modem(1)    fixed baud rate wrapper
@@ -23,4 +25,10 @@ DESCRIPTION
      wat(1)      watch files
      xx(1)       hexdump
 
-Causal Agency                  December 2, 2018                  Causal Agency
+     To build graphical tools, set one of:
+
+           GFX=cocoa
+           GFX=fb
+           GFX=x11
+
+Causal Agency                  January 17, 2019                  Causal Agency
diff --git a/bin/bin.7 b/bin/bin.7
index 4c5df7f0..c8b876a4 100644
--- a/bin/bin.7
+++ b/bin/bin.7
@@ -1,4 +1,4 @@
-.Dd December 2, 2018
+.Dd January 17, 2019
 .Dt BIN 7
 .Os "Causal Agency"
 .
@@ -19,6 +19,9 @@ Some tools target Linux.
 .It Xr bri 1
 backlight brightness control
 .
+.It Xr brot 1
+Mandelbrot renderer
+.
 .It Xr dtch 1
 detached sessions
 .
@@ -28,6 +31,9 @@ framebuffer battery indicator
 .It Xr fbclock 1
 framebuffer clock
 .
+.It Xr gfxx 1
+graphics data explorer
+.
 .It Xr glitch 1
 PNG glitcher
 .
@@ -61,3 +67,17 @@ watch files
 .It Xr xx 1
 hexdump
 .El
+.
+.Pp
+To build graphical tools,
+set one of:
+.
+.Pp
+.Bl -item -offset indent -compact
+.It
+.Va GFX Ns = Ns Cm cocoa
+.It
+.Va GFX Ns = Ns Cm fb
+.It
+.Va GFX Ns = Ns Cm x11
+.El
diff --git a/bin/brot.c b/bin/brot.c
new file mode 100644
index 00000000..e7ac442d
--- /dev/null
+++ b/bin/brot.c
@@ -0,0 +1,151 @@
+/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <complex.h>
+#include <err.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "gfx.h"
+
+static uint32_t rgb(uint8_t r, uint8_t g, uint8_t b) {
+	return (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b;
+}
+static uint32_t gray(uint8_t n) {
+	return rgb(n, n, n);
+}
+
+static double absSq(double complex z) {
+	return creal(z) * creal(z) + cimag(z) * cimag(z);
+}
+
+static uint32_t depth = 50;
+
+static uint32_t mandelbrot(double complex c) {
+	double complex z = 0;
+	for (uint32_t i = 0; i < depth; ++i) {
+		if (absSq(z) > 4.0) return i;
+		z = z * z + c;
+	}
+	return 0;
+}
+
+static double complex translate = -0.75;
+static double complex transform = 2.5;
+
+static uint32_t samples = 1;
+
+static void sample(uint32_t *buf, size_t width, size_t height) {
+	double yRatio = (height > width) ? (double)height / (double)width : 1.0;
+	double xRatio = (width > height) ? (double)width / (double)height : 1.0;
+
+	memset(buf, 0, 4 * width * height);
+	size_t superWidth = width * samples;
+	size_t superHeight = height * samples;
+	for (size_t y = 0; y < superHeight; ++y) {
+		for (size_t x = 0; x < superWidth; ++x) {
+			double zx = (((double)x + 0.5) / (double)superWidth - 0.5) * xRatio;
+			double zy = (((double)y + 0.5) / (double)superHeight - 0.5) * yRatio;
+			uint32_t n = mandelbrot((zx + zy * I) * transform + translate);
+			buf[(y / samples) * width + (x / samples)] += n;
+		}
+	}
+}
+
+static void color(uint32_t *buf, size_t width, size_t height) {
+	for (size_t i = 0; i < width * height; ++i) {
+		buf[i] = gray(255 * buf[i] / samples / samples / depth);
+	}
+}
+
+static double frameTime;
+void draw(uint32_t *buf, size_t width, size_t height) {
+	clock_t t0 = clock();
+	sample(buf, width, height);
+	color(buf, width, height);
+	frameTime = (double)(clock() - t0) / (double)CLOCKS_PER_SEC;
+}
+
+static double translateStep = 1.0 / 128.0;
+static double rotateStep = 1.0 / 128.0;
+static double scaleStep = 1.0 / 32.0;
+
+bool input(char in) {
+	const double PI = acos(-1.0);
+	switch (in) {
+		break; case 'q': return false;
+		break; case '.': depth++;
+		break; case ',': if (depth > 1) depth--;
+		break; case 'l': translate += translateStep * transform;
+		break; case 'h': translate -= translateStep * transform;
+		break; case 'j': translate += translateStep * I * transform;
+		break; case 'k': translate -= translateStep * I * transform;
+		break; case 'u': transform *= cexp(rotateStep * PI * I);
+		break; case 'i': transform /= cexp(rotateStep * PI * I);
+		break; case '+': transform *= 1.0 - scaleStep;
+		break; case '-': transform /= 1.0 - scaleStep;
+		break; case '0': translate = -0.75; transform = 2.5;
+		break; case ']': samples++;
+		break; case '[': if (samples > 1) samples--;
+	}
+	return true;
+}
+
+const char *status(void) {
+	static char buf[256];
+	snprintf(
+		buf, sizeof(buf),
+		"brot -s %u -i %u -t %g%+gi -f %g%+gi # %.6f",
+		samples, depth,
+		creal(translate), cimag(translate),
+		creal(transform), cimag(transform),
+		frameTime
+	);
+	return buf;
+}
+
+static double complex parseComplex(const char *str) {
+	double real = 0.0, imag = 0.0;
+	real = strtod(str, (char **)&str);
+	if (str[0] == 'i') {
+		imag = real;
+		real = 0.0;
+	} else if (str[0]) {
+		imag = strtod(str, NULL);
+	}
+	return real + imag * I;
+}
+
+int init(int argc, char *argv[]) {
+	int opt;
+	while (0 < (opt = getopt(argc, argv, "f:i:s:t:"))) {
+		switch (opt) {
+			break; case 'f': transform = parseComplex(optarg);
+			break; case 'i': depth = strtoul(optarg, NULL, 0);
+			break; case 's': samples = strtoul(optarg, NULL, 0);
+			break; case 't': translate = parseComplex(optarg);
+			break; default: return EX_USAGE;
+		}
+	}
+	return EX_OK;
+}
diff --git a/bin/gfx-cocoa.m b/bin/gfx-cocoa.m
new file mode 100644
index 00000000..7e7b7643
--- /dev/null
+++ b/bin/gfx-cocoa.m
@@ -0,0 +1,162 @@
+/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#import <err.h>
+#import <stdbool.h>
+#import <stdint.h>
+#import <stdlib.h>
+#import <sysexits.h>
+
+#import "gfx.h"
+
+@interface BufferView : NSView {
+	size_t bufSize;
+	uint32_t *buf;
+	CGColorSpaceRef colorSpace;
+	CGDataProviderRef dataProvider;
+}
+@end
+
+@implementation BufferView
+- (instancetype) initWithFrame: (NSRect) frameRect {
+	colorSpace = CGColorSpaceCreateDeviceRGB();
+	return [super initWithFrame: frameRect];
+}
+
+- (void) setWindowTitle {
+	[[self window] setTitle: [NSString stringWithUTF8String: status()]];
+}
+
+- (void) draw {
+	draw(buf, [self frame].size.width, [self frame].size.height);
+	[self setNeedsDisplay: YES];
+}
+
+- (void) setFrameSize: (NSSize) newSize {
+	[super setFrameSize: newSize];
+	size_t newBufSize = 4 * newSize.width * newSize.height;
+	if (newBufSize > bufSize) {
+		bufSize = newBufSize;
+		buf = malloc(bufSize);
+		if (!buf) err(EX_OSERR, "malloc(%zu)", bufSize);
+		CGDataProviderRelease(dataProvider);
+		dataProvider = CGDataProviderCreateWithData(NULL, buf, bufSize, NULL);
+	}
+	[self draw];
+}
+
+- (void) drawRect: (NSRect) dirtyRect {
+	(void)dirtyRect;
+	NSSize size = [self frame].size;
+	CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext];
+	CGImageRef image = CGImageCreate(
+		size.width, size.height,
+		8, 32, 4 * size.width,
+		colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipFirst,
+		dataProvider,
+		NULL, false, kCGRenderingIntentDefault
+	);
+	CGContextDrawImage(ctx, [self frame], image);
+	CGImageRelease(image);
+}
+
+- (BOOL) acceptsFirstResponder {
+	return YES;
+}
+
+- (void) keyDown: (NSEvent *) event {
+	char in;
+	BOOL converted = [
+		[event characters]
+		getBytes: &in
+		maxLength: 1
+		usedLength: NULL
+		encoding: NSASCIIStringEncoding
+		options: 0
+		range: NSMakeRange(0, 1)
+		remainingRange: NULL
+	];
+	if (converted) {
+		if (!input(in)) {
+			[NSApp terminate: self];
+		}
+		[self draw];
+		[self setWindowTitle];
+	}
+}
+@end
+
+@interface Delegate : NSObject <NSApplicationDelegate>
+@end
+
+@implementation Delegate
+- (BOOL) applicationShouldTerminateAfterLastWindowClosed:
+	(NSApplication *) sender {
+	(void)sender;
+	return YES;
+}
+@end
+
+int main(int argc, char *argv[]) {
+	int error = init(argc, argv);
+	if (error) return error;
+
+	[NSApplication sharedApplication];
+	[NSApp setActivationPolicy: NSApplicationActivationPolicyRegular];
+	[NSApp setDelegate: [Delegate new]];
+
+	NSString *name = [[NSProcessInfo processInfo] processName];
+	NSMenu *menu = [NSMenu new];
+	[
+		menu
+		addItemWithTitle: @"Close Window"
+		action: @selector(performClose:)
+		keyEquivalent: @"w"
+	];
+	[menu addItem: [NSMenuItem separatorItem]];
+	[
+		menu
+		addItemWithTitle: [@"Quit " stringByAppendingString: name]
+		action: @selector(terminate:)
+		keyEquivalent: @"q"
+	];
+	NSMenuItem *menuItem = [NSMenuItem new];
+	[menuItem setSubmenu: menu];
+	[NSApp setMainMenu: [NSMenu new]];
+	[[NSApp mainMenu] addItem: menuItem];
+
+	NSUInteger style = NSTitledWindowMask
+		| NSClosableWindowMask
+		| NSMiniaturizableWindowMask
+		| NSResizableWindowMask;
+	NSWindow *window = [
+		[NSWindow alloc]
+		initWithContentRect: NSMakeRect(0, 0, 800, 600)
+		styleMask: style
+		backing: NSBackingStoreBuffered
+		defer: YES
+	];
+	[window center];
+
+	BufferView *view = [[BufferView alloc] initWithFrame: [window frame]];
+	[window setContentView: view];
+	[view setWindowTitle];
+
+	[window makeKeyAndOrderFront: nil];
+	[NSApp activateIgnoringOtherApps: YES];
+	[NSApp run];
+}
diff --git a/bin/gfx-fb.c b/bin/gfx-fb.c
new file mode 100644
index 00000000..75daed22
--- /dev/null
+++ b/bin/gfx-fb.c
@@ -0,0 +1,87 @@
+/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <fcntl.h>
+#include <linux/fb.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sysexits.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "gfx.h"
+
+static struct termios saveTerm;
+static void restoreTerm(void) {
+	tcsetattr(STDERR_FILENO, TCSADRAIN, &saveTerm);
+}
+
+int main(int argc, char *argv[]) {
+	int error;
+
+	error = init(argc, argv);
+	if (error) return error;
+
+	const char *path = getenv("FRAMEBUFFER");
+	if (!path) path = "/dev/fb0";
+
+	int fb = open(path, O_RDWR);
+	if (fb < 0) err(EX_OSFILE, "%s", path);
+
+	struct fb_var_screeninfo info;
+	error = ioctl(fb, FBIOGET_VSCREENINFO, &info);
+	if (error) err(EX_IOERR, "%s", path);
+
+	size_t size = 4 * info.xres * info.yres;
+	uint32_t *buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0);
+	if (buf == MAP_FAILED) err(EX_IOERR, "%s", path);
+
+	error = tcgetattr(STDERR_FILENO, &saveTerm);
+	if (error) err(EX_IOERR, "tcgetattr");
+	atexit(restoreTerm);
+
+	struct termios term = saveTerm;
+	term.c_lflag &= ~(ICANON | ECHO);
+	error = tcsetattr(STDERR_FILENO, TCSADRAIN, &term);
+	if (error) err(EX_IOERR, "tcsetattr");
+
+	uint32_t saveBg = buf[0];
+
+	uint32_t back[info.xres * info.yres];
+	for (;;) {
+		draw(back, info.xres, info.yres);
+		memcpy(buf, back, size);
+
+		char in;
+		ssize_t len = read(STDERR_FILENO, &in, 1);
+		if (len < 0) err(EX_IOERR, "read");
+		if (!len) return EX_DATAERR;
+
+		if (!input(in)) {
+			for (uint32_t i = 0; i < info.xres * info.yres; ++i) {
+				buf[i] = saveBg;
+			}
+			fprintf(stderr, "%s\n", status());
+			return EX_OK;
+		}
+	}
+}
diff --git a/bin/gfx-x11.c b/bin/gfx-x11.c
new file mode 100644
index 00000000..f885ac31
--- /dev/null
+++ b/bin/gfx-x11.c
@@ -0,0 +1,135 @@
+/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <X11/Xlib.h>
+#include <err.h>
+#include <sysexits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#include "gfx.h"
+
+static size_t width = 800;
+static size_t height = 600;
+
+static Display *display;
+static Window window;
+static Atom WM_DELETE_WINDOW;
+static GC windowGc;
+static XImage *image;
+
+static size_t bufSize;
+static uint32_t *buf;
+
+static size_t pixmapWidth;
+static size_t pixmapHeight;
+static Pixmap pixmap;
+
+static void createWindow(void) {
+	display = XOpenDisplay(NULL);
+	if (!display) errx(EX_UNAVAILABLE, "XOpenDisplay: %s", XDisplayName(NULL));
+
+	Window root = DefaultRootWindow(display);
+	window = XCreateSimpleWindow(display, root, 0, 0, width, height, 0, 0, 0);
+
+	WM_DELETE_WINDOW = XInternAtom(display, "WM_DELETE_WINDOW", false);
+	XSetWMProtocols(display, window, &WM_DELETE_WINDOW, 1);
+
+	windowGc = XCreateGC(display, window, 0, NULL);
+
+	image = XCreateImage(display, NULL, 24, ZPixmap, 0, NULL, width, height, 32, 0);
+}
+
+static void resizePixmap(void) {
+	size_t newSize = 4 * width * height;
+	if (newSize > bufSize) {
+		bufSize = newSize;
+		free(buf);
+		buf = malloc(bufSize);
+		if (!buf) err(EX_OSERR, "malloc(%zu)", bufSize);
+	}
+
+	image->data = (char *)buf;
+	image->width = width;
+	image->height = height;
+	image->bytes_per_line = 4 * width;
+
+	if (width > pixmapWidth || height > pixmapHeight) {
+		pixmapWidth = width;
+		pixmapHeight = height;
+		if (pixmap) XFreePixmap(display, pixmap);
+		pixmap = XCreatePixmap(display, window, pixmapWidth, pixmapHeight, 24);
+	}
+}
+
+static void drawWindow(void) {
+	draw(buf, width, height);
+	XPutImage(display, pixmap, windowGc, image, 0, 0, 0, 0, width, height);
+	XCopyArea(display, pixmap, window, windowGc, 0, 0, width, height, 0, 0);
+}
+
+int main(int argc, char *argv[]) {
+	int error = init(argc, argv);
+	if (error) return error;
+
+	createWindow();
+	resizePixmap();
+	drawWindow();
+
+	XStoreName(display, window, status());
+	XMapWindow(display, window);
+
+	XEvent event;
+	XSelectInput(display, window, ExposureMask | StructureNotifyMask | KeyPressMask);
+	for (;;) {
+		XNextEvent(display, &event);
+
+		switch (event.type) {
+			case KeyPress: {
+				XKeyEvent key = event.xkey;
+				KeySym sym = XLookupKeysym(&key, key.state & ShiftMask);
+				if (sym > 0x80) break;
+				if (!input(sym)) return EX_OK;
+				drawWindow();
+				XStoreName(display, window, status());
+			} break;
+
+			case ConfigureNotify: {
+				XConfigureEvent configure = event.xconfigure;
+				width = configure.width;
+				height = configure.height;
+				resizePixmap();
+				drawWindow();
+			} break;
+
+			case Expose: {
+				XExposeEvent expose = event.xexpose;
+				XCopyArea(
+					display, pixmap, window, windowGc,
+					expose.x, expose.y,
+					expose.width, expose.height,
+					expose.x, expose.y
+				);
+			} break;
+
+			case ClientMessage: {
+				XClientMessageEvent client = event.xclient;
+				if ((Atom)client.data.l[0] == WM_DELETE_WINDOW) return EX_OK;
+			} break;
+		}
+	}
+}
diff --git a/bin/gfx.h b/bin/gfx.h
new file mode 100644
index 00000000..7fdf9f48
--- /dev/null
+++ b/bin/gfx.h
@@ -0,0 +1,24 @@
+/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+extern int init(int argc, char *argv[]);
+extern const char *status(void);
+extern void draw(uint32_t *buf, size_t width, size_t height);
+extern bool input(char in);
diff --git a/bin/gfxx.c b/bin/gfxx.c
new file mode 100644
index 00000000..7373fd4b
--- /dev/null
+++ b/bin/gfxx.c
@@ -0,0 +1,470 @@
+/* Copyright (C) 2018  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <fcntl.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "gfx.h"
+#include "png.h"
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#define MASK(b) ((1 << (b)) - 1)
+
+static uint32_t rgb(uint8_t r, uint8_t g, uint8_t b) {
+	return (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b;
+}
+static uint32_t gray(uint8_t n) {
+	return rgb(n, n, n);
+}
+
+static enum {
+	ColorIndexed,
+	ColorGrayscale,
+	ColorRGB,
+	ColorCount,
+} space = ColorRGB;
+static const char *ColorStr[ColorCount] = { "indexed", "grayscale", "rgb" };
+static uint32_t palette[256];
+
+static enum {
+	EndianLittle,
+	EndianBig,
+} byteOrder, bitOrder;
+
+enum { Pad, R, G, B };
+static uint8_t bits[4] = { 8, 8, 8, 8 };
+
+static uint8_t bitsColor(void) {
+	return bits[R] + bits[G] + bits[B];
+}
+static uint8_t bitsTotal(void) {
+	return bits[Pad] + bitsColor();
+}
+
+static size_t offset;
+static size_t width = 16;
+static bool flip;
+static bool mirror;
+static size_t scale = 1;
+
+static const char *prefix = "gfxx";
+
+static size_t size;
+static uint8_t *data;
+
+int init(int argc, char *argv[]) {
+	const char *pal = NULL;
+	const char *path = NULL;
+
+	int opt;
+	while (0 < (opt = getopt(argc, argv, "c:p:b:e:E:n:fmw:z:o:"))) {
+		switch (opt) {
+			break; case 'c':
+				switch (optarg[0]) {
+					break; case 'i': space = ColorIndexed;
+					break; case 'g': space = ColorGrayscale;
+					break; case 'r': space = ColorRGB;
+					break; default: return EX_USAGE;
+				}
+			break; case 'p': pal = optarg;
+			break; case 'e':
+				switch (optarg[0]) {
+					break; case 'l': byteOrder = EndianLittle;
+					break; case 'b': byteOrder = EndianBig;
+					break; default: return EX_USAGE;
+				}
+			break; case 'E':
+				switch (optarg[0]) {
+					break; case 'l': bitOrder = EndianLittle;
+					break; case 'b': bitOrder = EndianBig;
+					break; default: return EX_USAGE;
+				}
+			break; case 'b': {
+				if (strlen(optarg) < 4) return EX_USAGE;
+				for (int i = 0; i < 4; ++i) {
+					bits[i] = optarg[i] - '0';
+				}
+			}
+			break; case 'n': offset = strtoul(optarg, NULL, 0);
+			break; case 'f': flip ^= true;
+			break; case 'm': mirror ^= true;
+			break; case 'w': width = strtoul(optarg, NULL, 0);
+			break; case 'z': scale = strtoul(optarg, NULL, 0);
+			break; case 'o': prefix = optarg;
+			break; default: return EX_USAGE;
+		}
+	}
+	if (argc > optind) path = argv[optind];
+	if (!width || !scale) return EX_USAGE;
+
+	if (pal) {
+		FILE *file = fopen(pal, "r");
+		if (!file) err(EX_NOINPUT, "%s", pal);
+		fread(palette, 4, 256, file);
+		if (ferror(file)) err(EX_IOERR, "%s", pal);
+		fclose(file);
+	} else {
+		for (int i = 0; i < 256; ++i) {
+			double h = i / 256.0 * 6.0;
+			double x = 1.0 - fabs(fmod(h, 2.0) - 1.0);
+			double r = 255.0, g = 255.0, b = 255.0;
+			if      (h <= 1.0) { g *= x; b = 0.0; }
+			else if (h <= 2.0) { r *= x; b = 0.0; }
+			else if (h <= 3.0) { r = 0.0; b *= x; }
+			else if (h <= 4.0) { r = 0.0; g *= x; }
+			else if (h <= 5.0) { r *= x; g = 0.0; }
+			else if (h <= 6.0) { g = 0.0; b *= x; }
+			palette[i] = (uint32_t)r << 16 | (uint32_t)g << 8 | (uint32_t)b;
+		}
+	}
+
+	if (path) {
+		int fd = open(path, O_RDONLY);
+		if (fd < 0) err(EX_NOINPUT, "%s", path);
+
+		struct stat stat;
+		int error = fstat(fd, &stat);
+		if (error) err(EX_IOERR, "%s", path);
+		size = stat.st_size;
+
+		data = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+		if (data == MAP_FAILED) err(EX_IOERR, "%s", path);
+
+	} else {
+		size = 1024 * 1024;
+		data = malloc(size);
+		if (!data) err(EX_OSERR, "malloc(%zu)", size);
+
+		size = fread(data, 1, size, stdin);
+		if (ferror(stdin)) err(EX_IOERR, "(stdin)");
+	}
+
+	return EX_OK;
+}
+
+static char options[128];
+static void formatOptions(void) {
+	snprintf(
+		options, sizeof(options),
+		"gfxx -c %s -e%c -E%c -b %hhu%hhu%hhu%hhu -n 0x%zX %s%s-w %zu -z %zu",
+		ColorStr[space],
+		"lb"[byteOrder],
+		"lb"[bitOrder],
+		bits[Pad], bits[R], bits[G], bits[B],
+		offset,
+		flip ? "-f " : "",
+		mirror ? "-m " : "",
+		width,
+		scale
+	);
+}
+
+const char *status(void) {
+	formatOptions();
+	return options;
+}
+
+struct Iter {
+	uint32_t *buf;
+	size_t bufWidth;
+	size_t bufHeight;
+	size_t left;
+	size_t x;
+	size_t y;
+};
+
+static struct Iter iter(uint32_t *buf, size_t bufWidth, size_t bufHeight) {
+	struct Iter it = { .buf = buf, .bufWidth = bufWidth, .bufHeight = bufHeight };
+	if (mirror) it.x = width - 1;
+	if (flip) it.y = bufHeight / scale - 1;
+	return it;
+}
+
+static bool nextX(struct Iter *it) {
+	if (mirror) {
+		if (it->x == it->left) return false;
+		it->x--;
+	} else {
+		it->x++;
+		if (it->x == it->left + width) return false;
+	}
+	return true;
+}
+
+static bool nextY(struct Iter *it) {
+	if (flip) {
+		if (it->y == 0) {
+			it->left += width;
+			it->y = it->bufHeight / scale;
+		}
+		it->y--;
+	} else {
+		it->y++;
+		if (it->y == it->bufHeight / scale) {
+			it->left += width;
+			it->y = 0;
+		}
+	}
+	it->x = it->left;
+	if (mirror) it->x += width - 1;
+	return (it->left < it->bufWidth / scale);
+}
+
+static bool next(struct Iter *it) {
+	return nextX(it) || nextY(it);
+}
+
+static void put(const struct Iter *it, uint32_t pixel) {
+	size_t scaledX = it->x * scale;
+	size_t scaledY = it->y * scale;
+	for (size_t fillY = scaledY; fillY < scaledY + scale; ++fillY) {
+		if (fillY >= it->bufHeight) break;
+		for (size_t fillX = scaledX; fillX < scaledX + scale; ++fillX) {
+			if (fillX >= it->bufWidth) break;
+			it->buf[fillY * it->bufWidth + fillX] = pixel;
+		}
+	}
+}
+
+static uint8_t interp(uint8_t b, uint32_t n) {
+	if (b == 8) return n;
+	if (b == 0) return 0;
+	return n * MASK(8) / MASK(b);
+}
+
+static uint32_t interpolate(uint32_t orig) {
+	uint32_t r, g, b;
+	if (bitOrder == EndianLittle) {
+		b = orig & MASK(bits[B]);
+		g = (orig >>= bits[B]) & MASK(bits[G]);
+		r = (orig >>= bits[G]) & MASK(bits[R]);
+	} else {
+		r = orig & MASK(bits[R]);
+		g = (orig >>= bits[R]) & MASK(bits[G]);
+		b = (orig >>= bits[G]) & MASK(bits[B]);
+	}
+	return rgb(interp(bits[R], r), interp(bits[G], g), interp(bits[B], b));
+}
+
+static void drawBits(struct Iter *it) {
+	for (size_t i = offset; i < size; ++i) {
+		for (uint8_t b = 0; b < 8; b += bitsTotal()) {
+			uint8_t n;
+			if (byteOrder == EndianBig) {
+				n = data[i] >> (8 - bitsTotal() - b) & MASK(bitsTotal());
+			} else {
+				n = data[i] >> b & MASK(bitsTotal());
+			}
+
+			if (space == ColorIndexed) {
+				put(it, palette[n]);
+			} else if (space == ColorGrayscale) {
+				put(it, gray(interp(bitsColor(), n & MASK(bitsColor()))));
+			} else if (space == ColorRGB) {
+				put(it, interpolate(n));
+			}
+
+			if (!next(it)) return;
+		}
+	}
+}
+
+static void drawBytes(struct Iter *it) {
+	uint8_t bytes = (bitsTotal() + 7) / 8;
+	for (size_t i = offset; i + bytes <= size; i += bytes) {
+		uint32_t n = 0;
+		for (size_t b = 0; b < bytes; ++b) {
+			n <<= 8;
+			n |= (byteOrder == EndianBig) ? data[i + b] : data[i + bytes - b - 1];
+		}
+
+		if (space == ColorIndexed) {
+			put(it, palette[n & 0xFF]);
+		} else if (space == ColorGrayscale) {
+			put(it, gray(interp(bitsColor(), n & MASK(bitsColor()))));
+		} else if (space == ColorRGB) {
+			put(it, interpolate(n));
+		}
+
+		if (!next(it)) return;
+	}
+}
+
+static struct {
+	unsigned counter;
+	char path[FILENAME_MAX];
+	FILE *file;
+} out;
+static void outOpen(const char *ext) {
+	snprintf(out.path, sizeof(out.path), "%s%04u.%s", prefix, ++out.counter, ext);
+	out.file = fopen(out.path, "wx");
+	if (out.file) {
+		printf("%s\n", out.path);
+	} else {
+		warn("%s", out.path);
+	}
+}
+
+static void pngDump(uint32_t *src, size_t srcWidth, size_t srcHeight) {
+	int error;
+
+	size_t stride = 1 + 3 * srcWidth;
+	uint8_t data[stride * srcHeight];
+	for (size_t y = 0; y < srcHeight; ++y) {
+		data[y * stride] = 0;
+		for (size_t x = 0; x < srcWidth; ++x) {
+			uint8_t *p = &data[y * stride + 1 + 3 * x];
+			p[0] = src[y * srcWidth + x] >> 16;
+			p[1] = src[y * srcWidth + x] >> 8;
+			p[2] = src[y * srcWidth + x];
+		}
+	}
+
+	outOpen("png");
+	if (!out.file) return;
+
+	const char Software[] = "Software";
+	formatOptions();
+	uint8_t sbit[3] = { MAX(bits[R], 1), MAX(bits[G], 1), MAX(bits[B], 1) };
+
+	pngHead(out.file, srcWidth, srcHeight, 8, PNGTruecolor);
+
+	pngChunk(out.file, "tEXt", sizeof(Software) + strlen(options));
+	pngWrite(out.file, (uint8_t *)Software, sizeof(Software));
+	pngWrite(out.file, (uint8_t *)options, strlen(options));
+	pngInt32(out.file, ~pngCRC);
+
+	pngChunk(out.file, "sBIT", sizeof(sbit));
+	pngWrite(out.file, sbit, sizeof(sbit));
+	pngInt32(out.file, ~pngCRC);
+
+	pngData(out.file, data, sizeof(data));
+	pngTail(out.file);
+
+	error = fclose(out.file);
+	if (error) err(EX_IOERR, "%s", out.path);
+}
+
+static enum {
+	DumpNone,
+	DumpOne,
+	DumpAll,
+} dump;
+
+void draw(uint32_t *buf, size_t bufWidth, size_t bufHeight) {
+	memset(buf, 0, 4 * bufWidth * bufHeight);
+	struct Iter it = iter(buf, bufWidth, bufHeight);
+	if (bitsTotal() >= 8) {
+		drawBytes(&it);
+	} else {
+		drawBits(&it);
+	}
+	if (dump) pngDump(buf, bufWidth, bufHeight);
+	if (dump == DumpOne) dump = DumpNone;
+}
+
+static void palSample(void) {
+	size_t temp = scale;
+	scale = 1;
+	draw(palette, 256, 1);
+	scale = temp;
+}
+
+static void palDump(void) {
+	outOpen("dat");
+	if (!out.file) return;
+
+	fwrite(palette, 4, 256, out.file);
+	if (ferror(out.file)) err(EX_IOERR, "%s", out.path);
+
+	int error = fclose(out.file);
+	if (error) err(EX_IOERR, "%s", out.path);
+}
+
+static const uint8_t Presets[][4] = {
+	{ 0, 0, 1, 0 },
+	{ 0, 1, 1, 0 },
+	{ 1, 1, 1, 1 },
+	{ 2, 2, 2, 2 },
+	{ 0, 3, 3, 2 },
+	{ 4, 4, 4, 4 },
+	{ 1, 5, 5, 5 },
+	{ 0, 5, 6, 5 },
+	{ 0, 8, 8, 8 },
+	{ 8, 8, 8, 8 },
+};
+static const size_t PresetsLen = sizeof(Presets) / sizeof(Presets[0]);
+
+static uint8_t preset = PresetsLen - 1;
+static void setPreset(void) {
+	bits[Pad] = Presets[preset][Pad];
+	bits[R] = Presets[preset][R];
+	bits[G] = Presets[preset][G];
+	bits[B] = Presets[preset][B];
+}
+
+static void setBit(char in) {
+	static uint8_t bit = 0;
+	bits[bit++] = in - '0';
+	bit &= 3;
+}
+
+bool input(char in) {
+	size_t pixel = (bitsTotal() + 7) / 8;
+	size_t row = width * bitsTotal() / 8;
+	switch (in) {
+		break; case 'q': return false;
+		break; case 'x': dump = DumpOne;
+		break; case 'X': dump ^= DumpAll;
+		break; case 'o': formatOptions(); printf("%s\n", options);
+		break; case '[': if (!space--) space = ColorCount - 1;
+		break; case ']': if (++space == ColorCount) space = 0;
+		break; case 'p': palSample();
+		break; case 'P': palDump();
+		break; case '{': if (!preset--) preset = PresetsLen - 1; setPreset();
+		break; case '}': if (++preset == PresetsLen) preset = 0; setPreset();
+		break; case 'e': byteOrder ^= EndianBig;
+		break; case 'E': bitOrder ^= EndianBig;
+		break; case 'h': if (offset) offset--;
+		break; case 'j': offset += pixel;
+		break; case 'k': if (offset >= pixel) offset -= pixel;
+		break; case 'l': offset++;
+		break; case 'H': if (offset >= row) offset -= row;
+		break; case 'J': offset += width * row;
+		break; case 'K': if (offset >= width * row) offset -= width * row;
+		break; case 'L': offset += row;
+		break; case '.': width++;
+		break; case ',': if (width > 1) width--;
+		break; case '>': width *= 2;
+		break; case '<': if (width > 1) width /= 2;
+		break; case 'f': flip ^= true;
+		break; case 'm': mirror ^= true;
+		break; case '+': scale++;
+		break; case '-': if (scale > 1) scale--;
+		break; default: if (in >= '0' && in <= '9') setBit(in);
+	}
+	return true;
+}
diff --git a/bin/man1/brot.1 b/bin/man1/brot.1
new file mode 100644
index 00000000..7c1a4087
--- /dev/null
+++ b/bin/man1/brot.1
@@ -0,0 +1,63 @@
+.Dd September 8, 2018
+.Dt BROT 1
+.Os
+.
+.Sh NAME
+.Nm brot
+.Nd Mandelbrot renderer
+.
+.Sh SYNOPSIS
+.Nm
+.Op Fl i Ar depth
+.Op Fl s Ar samples
+.Op Fl t Ar a+bi
+.Op Fl f Ar a+bi
+.
+.Sh DESCRIPTION
+.Nm
+renders the Mandelbrot set
+in grayscale.
+.
+.Pp
+The arguments are as follows:
+.Bl -tag -width Ds
+.It Fl f Ar a+bi
+Set complex transform factor.
+.
+.It Fl i Ar depth
+Set number of iterations.
+.
+.It Fl s Ar samples
+Set number of samples per pixel.
+.
+.It Fl t Ar a+bi
+Set complex translation term.
+.El
+.
+.Pp
+The interactive keys are as follows:
+.Bl -tag -width Ds
+.It Ic q
+Quit.
+.
+.It Ic ,.
+Adjust depth.
+.
+.It Ic []
+Adjust samples.
+.
+.It Ic hl
+Adjust real component of translation.
+.
+.It Ic kj
+Adjust imaginary componenent of translation.
+.
+.It Ic ui
+Adjust rotation.
+.
+.It Ic -+
+Adjust scale.
+.
+.It Ic 0
+Reset translation and transform.
+.El
diff --git a/bin/man1/gfxx.1 b/bin/man1/gfxx.1
new file mode 100644
index 00000000..d3d1edbd
--- /dev/null
+++ b/bin/man1/gfxx.1
@@ -0,0 +1,138 @@
+.Dd September 8, 2018
+.Dt GFXX 1
+.Os
+.
+.Sh NAME
+.Nm gfxx
+.Nd graphics data explorer
+.
+.Sh SYNOPSIS
+.Nm
+.Op Fl fm
+.Op Fl c Ar space
+.Op Fl p Ar file
+.Op Fl e Ar lb
+.Op Fl E Ar lb
+.Op Fl b Ar bits
+.Op Fl n Ar offset
+.Op Fl w Ar width
+.Op Fl z Ar scale
+.Op Fl o Ar prefix
+.
+.Sh DESCRIPTION
+.Nm
+is an interactive graphics data explorer
+capable of dumping PNGs.
+.
+.Pp
+The arguments are as follows:
+.Bl -tag -width Ds
+.It Fl E Ar lb
+Set bit order to little or big endian.
+.
+.It Fl b Ar bits
+Set number of bits of padding,
+red,
+green
+and blue
+components.
+Four digits are required.
+.
+.It Fl c Ar space
+Set color space to one of
+.Cm indexed ,
+.Cm grayscale ,
+.Cm rgb .
+.
+.It Fl e Ar lb
+Set byte order to little or big endian.
+.
+.It Fl f
+Flip display vertically.
+.
+.It Fl m
+Mirror display horizontally.
+.
+.It Fl n Ar offset
+Set byte offset into data.
+.
+.It Fl o Ar prefix
+Set PNG output filename prefix.
+.
+.It Fl p Ar file
+Load indexed color palette from
+.Ar file .
+.
+.It Fl w Ar width
+Set display column width.
+.
+.It Fl z Ar scale
+Set display scale.
+.El
+.
+.Pp
+The interactive keys are as follows:
+.Bl -tag -width Ds
+.It Ic q
+Quit.
+.
+.It Ic x
+Export one frame as PNG.
+.
+.It Ic X
+Export each frame as PNG.
+.
+.It Ic o
+Print interactive options.
+.
+.It Ic []
+Select color space.
+.
+.It Ic p
+Sample indexed palette from top-left corner.
+.
+.It Ic P
+Export indexed palette as RGB data.
+.
+.It Ic {}
+Select bits preset.
+.
+.It Ic e
+Toggle byte order.
+.
+.It Ic E
+Toggle bit order.
+.
+.It Ic hl
+Adjust offset by byte.
+.
+.It Ic jk
+Adjust offset by pixel.
+.
+.It Ic HL
+Adjust offset by pixel row.
+.
+.It Ic JK
+Adjust offset by pixel row square.
+.
+.It Ic ,.
+Adjust display column width.
+.
+.It Ic <>
+Adjust display column width by half or double.
+.
+.It Ic f
+Toggle vertically flipped display.
+.
+.It Ic m
+Toggle horizontally mirrored display.
+.
+.It Ic -+
+Adjust display scale.
+.
+.It Ic 0-9
+Set bits.
+.El
+.
+.Sh SEE ALSO
+.Xr pngo 1