cosmopolitan/tool/viz/derasterize.c
Justine Tunney f4f4caab0e Add x86_64-linux-gnu emulator
I wanted a tiny scriptable meltdown proof way to run userspace programs
and visualize how program execution impacts memory. It helps to explain
how things like Actually Portable Executable works. It can show you how
the GCC generated code is going about manipulating matrices and more. I
didn't feel fully comfortable with Qemu and Bochs because I'm not smart
enough to understand them. I wanted something like gVisor but with much
stronger levels of assurances. I wanted a single binary that'll run, on
all major operating systems with an embedded GPL barrier ZIP filesystem
that is tiny enough to transpile to JavaScript and run in browsers too.

https://justine.storage.googleapis.com/emulator625.mp4
2020-08-25 04:43:42 -07:00

619 lines
21 KiB
C

/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2020 Justine Alexandra Roberts Tunney │
│ │
│ This program is free software; you can redistribute it and/or modify │
│ it under the terms of the GNU General Public License as published by │
│ the Free Software Foundation; version 2 of the License. │
│ │
│ 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 │
│ General Public License for more details. │
│ │
│ You should have received a copy of the GNU General Public License │
│ along with this program; if not, write to the Free Software │
│ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA │
│ 02110-1301 USA │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "dsp/tty/itoa8.h"
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/hefty/spawn.h"
#include "libc/calls/ioctl.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/termios.h"
#include "libc/conv/conv.h"
#include "libc/fmt/fmt.h"
#include "libc/limits.h"
#include "libc/log/check.h"
#include "libc/log/log.h"
#include "libc/macros.h"
#include "libc/math.h"
#include "libc/mem/mem.h"
#include "libc/nexgen32e/x86feature.h"
#include "libc/runtime/gc.h"
#include "libc/runtime/runtime.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/str/tpenc.h"
#include "libc/sysv/consts/ex.h"
#include "libc/sysv/consts/exit.h"
#include "libc/sysv/consts/fileno.h"
#include "libc/sysv/consts/madv.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
#include "libc/x/x.h"
#include "third_party/avir/lanczos.h"
#include "third_party/getopt/getopt.h"
#include "third_party/stb/stb_image.h"
#include "third_party/stb/stb_image_resize.h"
#define HELPTEXT \
"\n\
NAME\n\
\n\
derasterize - convert pictures to text using unicode ANSI art\n\
\n\
SYNOPSIS\n\
\n\
derasterize [FLAGS] [PNG|JPG|ETC]...\n\
\n\
DESCRIPTION\n\
\n\
This program converts pictures into unicode text and ANSI colors so\n\
that images can be displayed within a terminal. It performs lots of\n\
AVX2 optimized math to deliver the best quality on modern terminals\n\
with 24-bit color support, e.g. Kitty, Gnome Terminal, CMD.EXE, etc\n\
\n\
The default output if fullscreen but can be changed:\n\
-w X\n\
-x X\n\
If X is positive, hardcode the width in tty cells to X\n\
If X is negative, remove as much from the fullscreen width\n\
X may be specified as base 10 decimal, octal, binary, or hex\n\
-h Y\n\
-y Y\n\
If Y is positive, hardcode the height in tty cells to Y\n\
If Y is negative, remove as much from the fullscreen height\n\
May be specified as base 10 decimal, octal, binary, or hex\n\
-m\n\
Use ImageMagick `convert` command to load/scale graphics\n\
-?\n\
-H\n\
Show this help information\n\
\n\
EXAMPLES\n\
\n\
$ ./derasterize.com samples/wave.png > wave.uaart\n\
$ cat wave.uaart\n\
\n\
AUTHORS\n\
\n\
Csdvrx <csdvrx@outlook.com>\n\
Justine Tunney <jtunney@gmail.com>\n\
"
int m_; /* -m [use imagemagick] */
int x_; /* -x WIDTH [in flexidecimal] */
int y_; /* -y HEIGHT [in flexidecimal] */
#define BEST 0
#define FAST 1
#define FASTER 2
#define MODE BEST
#if MODE == BEST
#define MC 9u /* log2(#) of color combos to consider */
#define GN 35u /* # of glyphs to consider */
#elif MODE == FAST
#define MC 6u
#define GN 35u
#elif MODE == FASTER
#define MC 4u
#define GN 25u
#endif
#define CN 3u /* # channels (rgb) */
#define YS 8u /* row stride -or- block height */
#define XS 4u /* column stride -or- block width */
#define GT 44u /* total glyphs */
#define BN (YS * XS) /* # scalars in block/glyph plane */
#define PHIPRIME 0x9E3779B1u
extern const uint32_t kGlyphs[];
extern const char16_t kRunes[];
/*───────────────────────────────────────────────────────────────────────────│─╗
│ derasterize § encoding ─╬─│┼
╚────────────────────────────────────────────────────────────────────────────│*/
/**
* Formats Thompson-Pike variable length integer to array.
*
* @param p needs at least 8 bytes
* @return p + number of bytes written, cf. mempcpy
* @note no NUL-terminator is added
*/
static char *tptoa(char *p, wchar_t x) {
unsigned long w;
for (w = tpenc(x); w; w >>= 010) *p++ = w & 0xff;
return p;
}
/*───────────────────────────────────────────────────────────────────────────│─╗
│ derasterize § colors ─╬─│┼
╚────────────────────────────────────────────────────────────────────────────│*/
static float frgb2lin(float x) {
float r1, r2;
r1 = x / 12.92f;
r2 = pow((x + 0.055) / (1 + 0.055), 2.4);
return x < 0.04045f ? r1 : r2;
}
static float frgb2std(float x) {
float r1, r2;
r1 = x * 12.92f;
r2 = 1.055 * pow(x, 1 / 2.4) - 0.055;
return x < 0.0031308f ? r1 : r2;
}
/**
* Converts 8-bit RGB samples to floating point.
*/
static void rgb2float(unsigned n, float *f, const unsigned char *u) {
unsigned i;
for (i = 0; i < n; ++i) f[i] = u[i];
for (i = 0; i < n; ++i) f[i] /= 255;
}
/**
* Converts floating point RGB samples to 8-bit.
*/
static void float2rgb(unsigned n, unsigned char *u, float *f) {
unsigned i;
for (i = 0; i < n; ++i) f[i] *= 256;
for (i = 0; i < n; ++i) f[i] = roundf(f[i]);
for (i = 0; i < n; ++i) u[i] = MAX(0, MIN(255, f[i]));
}
/**
* Converts standard RGB to linear RGB.
*
* This makes subtraction look good by flattening out the bias curve
* that PC display manufacturers like to use.
*/
static noinline void rgb2lin(unsigned n, float *f, const unsigned char *u) {
unsigned i;
rgb2float(n, f, u);
for (i = 0; i < n; ++i) f[i] = frgb2lin(f[i]);
}
/**
* Converts linear RGB to standard RGB.
*/
static noinline void rgb2std(unsigned n, unsigned char *u, float *f) {
unsigned i;
for (i = 0; i < n; ++i) f[i] = frgb2std(f[i]);
float2rgb(n, u, f);
}
/*───────────────────────────────────────────────────────────────────────────│─╗
│ derasterize § blocks ─╬─│┼
╚────────────────────────────────────────────────────────────────────────────│*/
struct Cell {
char16_t rune;
unsigned char bg[CN], fg[CN];
};
/**
* Serializes ANSI background, foreground, and UNICODE glyph to wire.
*/
static char *celltoa(char *p, struct Cell cell) {
*p++ = 033;
*p++ = '[';
*p++ = '4';
*p++ = '8';
*p++ = ';';
*p++ = '2';
*p++ = ';';
p = itoa8(p, cell.bg[0]);
*p++ = ';';
p = itoa8(p, cell.bg[1]);
*p++ = ';';
p = itoa8(p, cell.bg[2]);
*p++ = ';';
*p++ = '3';
*p++ = '8';
*p++ = ';';
*p++ = '2';
*p++ = ';';
p = itoa8(p, cell.fg[0]);
*p++ = ';';
p = itoa8(p, cell.fg[1]);
*p++ = ';';
p = itoa8(p, cell.fg[2]);
*p++ = 'm';
p = tptoa(p, cell.rune);
return p;
}
/**
* Picks ≤2**MC unique (bg,fg) pairs from product of lb.
*/
static unsigned combinecolors(unsigned char bf[1u << MC][2],
const unsigned char bl[CN][YS * XS]) {
uint64_t hv, ht[(1u << MC) * 2];
unsigned i, j, n, b, f, h, hi, bu, fu;
memset(ht, 0, sizeof(ht));
for (n = b = 0; b < BN && n < (1u << MC); ++b) {
bu = bl[2][b] << 020 | bl[1][b] << 010 | bl[0][b];
hi = 0;
hi = (((bu >> 000) & 0xff) + hi) * PHIPRIME;
hi = (((bu >> 010) & 0xff) + hi) * PHIPRIME;
hi = (((bu >> 020) & 0xff) + hi) * PHIPRIME;
for (f = b + 1; f < BN && n < (1u << MC); ++f) {
fu = bl[2][f] << 020 | bl[1][f] << 010 | bl[0][f];
h = hi;
h = (((fu >> 000) & 0xff) + h) * PHIPRIME;
h = (((fu >> 010) & 0xff) + h) * PHIPRIME;
h = (((fu >> 020) & 0xff) + h) * PHIPRIME;
h = h & 0xffff;
h = MAX(1, h);
hv = 0;
hv <<= 030;
hv |= fu;
hv <<= 030;
hv |= bu;
hv <<= 020;
hv |= h;
for (i = 0;; ++i) {
j = (h + i * (i + 1) / 2) & (ARRAYLEN(ht) - 1);
if (!ht[j]) {
ht[j] = hv;
bf[n][0] = b;
bf[n][1] = f;
n++;
break;
} else if (ht[j] == hv) {
break;
}
}
}
}
return n;
}
/**
* Computes distance between synthetic block and actual.
*/
#define ADJUDICATE(SYMBOL, ARCH) \
ARCH static float SYMBOL(unsigned b, unsigned f, unsigned g, \
const float lb[CN][YS * XS]) { \
unsigned i, k, gu; \
float p[BN], q[BN], fu, bu, r; \
memset(q, 0, sizeof(q)); \
for (k = 0; k < CN; ++k) { \
gu = kGlyphs[g]; \
bu = lb[k][b]; \
fu = lb[k][f]; \
for (i = 0; i < BN; ++i) p[i] = (gu & (1u << i)) ? fu : bu; \
for (i = 0; i < BN; ++i) p[i] -= lb[k][i]; \
for (i = 0; i < BN; ++i) p[i] *= p[i]; \
for (i = 0; i < BN; ++i) q[i] += p[i]; \
} \
r = 0; \
for (i = 0; i < BN; ++i) q[i] = sqrtf(q[i]); \
for (i = 0; i < BN; ++i) r += q[i]; \
return r; \
}
ADJUDICATE(adjudicate$avx2, microarchitecture("avx2,fma"))
ADJUDICATE(adjudicate$avx, microarchitecture("avx"))
ADJUDICATE(adjudicate$default, )
static float (*adjudicate$hook)(unsigned, unsigned, unsigned,
const float[CN][YS * XS]);
static float adjudicate2(unsigned b, unsigned f, unsigned g,
const float lb[CN][YS * XS]) {
if (!adjudicate$hook) {
if (X86_HAVE(AVX2) && X86_HAVE(FMA)) {
adjudicate$hook = adjudicate$avx2;
} else if (X86_HAVE(AVX)) {
adjudicate$hook = adjudicate$avx;
} else {
adjudicate$hook = adjudicate$default;
}
}
return adjudicate$hook(b, f, g, lb);
}
static float adjudicate(unsigned b, unsigned f, unsigned g,
const float lb[CN][YS * XS]) {
unsigned i, k, gu;
float p[BN], q[BN], fu, bu, r;
memset(q, 0, sizeof(q));
for (k = 0; k < CN; ++k) {
gu = kGlyphs[g];
bu = lb[k][b];
fu = lb[k][f];
for (i = 0; i < BN; ++i) p[i] = (gu & (1u << i)) ? fu : bu;
for (i = 0; i < BN; ++i) p[i] -= lb[k][i];
for (i = 0; i < BN; ++i) p[i] *= p[i];
for (i = 0; i < BN; ++i) q[i] += p[i];
}
r = 0;
for (i = 0; i < BN; ++i) q[i] = sqrtf(q[i]);
for (i = 0; i < BN; ++i) r += q[i];
return r;
}
/**
* Converts tiny bitmap graphic into unicode glyph.
*/
static struct Cell derasterize(unsigned char block[CN][YS * XS]) {
struct Cell cell;
unsigned i, n, b, f, g;
float r, best, lb[CN][YS * XS];
unsigned char bf[1u << MC][2];
rgb2lin(CN * YS * XS, lb[0], block[0]);
n = combinecolors(bf, block);
best = -1u;
cell.rune = 0;
for (i = 0; i < n; ++i) {
b = bf[i][0];
f = bf[i][1];
for (g = 0; g < GN; ++g) {
r = adjudicate(b, f, g, lb);
if (r < best) {
best = r;
cell.rune = kRunes[g];
cell.bg[0] = block[0][b];
cell.bg[1] = block[1][b];
cell.bg[2] = block[2][b];
cell.fg[0] = block[0][f];
cell.fg[1] = block[1][f];
cell.fg[2] = block[2][f];
if (!r) return cell;
}
}
}
return cell;
}
/*───────────────────────────────────────────────────────────────────────────│─╗
│ derasterize § graphics ─╬─│┼
╚────────────────────────────────────────────────────────────────────────────│*/
/**
* Turns packed 8-bit RGB graphic into ANSI UNICODE text.
*/
static char *RenderImage(char *v, unsigned yn, unsigned xn,
const unsigned char srgb[yn][YS][xn][XS][CN]) {
unsigned y, x, i, j, k;
unsigned char copy[YS][XS][CN] aligned(32);
unsigned char block[CN][YS * XS] aligned(32);
DCHECK_ALIGNED(32, v);
DCHECK_ALIGNED(32, srgb);
for (y = 0; y < yn; ++y) {
if (y) {
*v++ = 033;
*v++ = '[';
*v++ = '0';
*v++ = 'm';
*v++ = '\n';
}
for (x = 0; x < xn; ++x) {
for (i = 0; i < YS; ++i) {
memcpy(copy[i], srgb[y][i][x], XS * CN);
}
for (i = 0; i < YS; ++i) {
for (j = 0; j < XS; ++j) {
for (k = 0; k < CN; ++k) {
block[k][i * XS + j] = copy[i][j][k];
}
}
}
v = celltoa(v, derasterize(block));
}
}
return v;
}
/*───────────────────────────────────────────────────────────────────────────│─╗
│ derasterize § systems ─╬─│┼
╚────────────────────────────────────────────────────────────────────────────│*/
static void PrintImage(unsigned yn, unsigned xn,
const unsigned char rgb[yn][YS][xn][XS][CN]) {
size_t size;
char *v, *vt;
size = yn * (xn * (32 + (2 + (1 + 3) * 3) * 2 + 1 + 3)) * 1 + 5 + 1;
size = ROUNDUP(size, FRAMESIZE);
CHECK_NE(MAP_FAILED, (vt = mapanon(size)));
v = RenderImage(vt, yn, xn, rgb);
*v++ = '\r';
*v++ = 033;
*v++ = '[';
*v++ = '0';
*v++ = 'm';
CHECK_NE(-1, xwrite(1, vt, v - vt));
CHECK_NE(-1, munmap(vt, size));
}
/**
* Determines dimensions of teletypewriter.
*/
static void GetTermSize(unsigned out_rows[1], unsigned out_cols[1]) {
struct winsize ws;
ws.ws_row = 24;
ws.ws_col = 80;
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1) {
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
}
out_rows[0] = ws.ws_row;
out_cols[0] = ws.ws_col;
}
static int ReadAll(int fd, void *data, size_t size) {
char *p;
ssize_t rc;
size_t got, n;
p = data;
n = size;
do {
if ((rc = read(fd, p, n)) == -1) return -1;
assert((got = rc) || !n);
p += got;
n -= got;
} while (n);
return 0;
}
/**
* Loads and scales image via ImageMagick `convert` command.
*
* @param path is filename of graphic
* @param yn is desired height
* @param xn is desired width
* @param rgb is memory allocated by caller for image
*/
static void LoadFileViaImageMagick(const char *path, unsigned yn, unsigned xn,
unsigned char rgb[yn][YS][xn][XS][CN]) {
const char *convert;
int pid, ws, fds[3] = {STDIN_FILENO, -1, STDERR_FILENO};
if (!(convert = commandv("convert"))) {
fputs("error: `convert` command not found\n"
"try: apt-get install imagemagick\n",
stderr);
exit(EXIT_FAILURE);
}
CHECK_NE(-1,
(pid = spawnve(
0, fds, convert,
(char *const[]){"convert", path, "-resize",
xasprintf("%ux%u!", xn * XS, yn * YS), "-depth",
"8", "-colorspace", "sRGB", "rgb:-", NULL},
environ)));
CHECK_NE(-1, ReadAll(fds[STDOUT_FILENO], rgb, yn * YS * xn * XS * CN));
CHECK_NE(-1, close(fds[STDOUT_FILENO]));
CHECK_NE(-1, waitpid(pid, &ws, 0));
CHECK_EQ(0, WEXITSTATUS(ws));
}
static void LoadFile(const char *path, size_t yn, size_t xn, void *rgb) {
struct stat st;
size_t data2size, data3size;
void *map, *data, *data2, *data3;
int fd, gotx, goty, channels_in_file;
CHECK_NE(-1, (fd = open(path, O_RDONLY)), "%s", path);
CHECK_NE(-1, fstat(fd, &st));
CHECK_GT(st.st_size, 0);
CHECK_LE(st.st_size, INT_MAX);
/* LOGIFNEG1(fadvise(fd, 0, 0, MADV_WILLNEED | MADV_SEQUENTIAL)); */
CHECK_NE(MAP_FAILED,
(map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0)));
CHECK_NOTNULL((data = stbi_load_from_memory(map, st.st_size, &gotx, &goty,
&channels_in_file, CN)),
"%s", path);
CHECK_NE(-1, munmap(map, st.st_size));
CHECK_NE(-1, close(fd));
#if 1
stbir_resize_uint8(data, gotx, goty, 0, rgb, xn * XS, yn * YS, 0, CN);
#else
CHECK_EQ(CN, 3);
data2size = ROUNDUP(sizeof(float) * goty * gotx * CN, FRAMESIZE);
data3size = ROUNDUP(sizeof(float) * yn * YS * xn * XS * CN, FRAMESIZE);
CHECK_NE(MAP_FAILED, (data2 = mapanon(data2size)));
CHECK_NE(MAP_FAILED, (data3 = mapanon(data3size)));
rgb2lin(goty * gotx * CN, data2, data);
lanczos3(yn * YS, xn * XS, data3, goty, gotx, data2, gotx * 3);
rgb2std(yn * YS * xn * XS * CN, rgb, data3);
CHECK_NE(-1, munmap(data2, data2size));
CHECK_NE(-1, munmap(data3, data3size));
#endif
free(data);
}
static int ParseNumberOption(const char *arg) {
long x;
x = strtol(arg, NULL, 0);
if (!(1 <= x && x <= INT_MAX)) {
fprintf(stderr, "invalid flexidecimal: %s\n\n", arg);
exit(EXIT_FAILURE);
}
return x;
}
static void PrintUsage(int rc, FILE *f) {
fputs(HELPTEXT, f);
exit(rc);
}
static void GetOpts(int argc, char *argv[]) {
int opt;
while ((opt = getopt(argc, argv, "?Hmx:y:w:h:")) != -1) {
switch (opt) {
case 'w':
case 'x':
x_ = ParseNumberOption(optarg);
break;
case 'h':
case 'y':
y_ = ParseNumberOption(optarg);
break;
case 'm':
m_ = 1;
break;
case '?':
case 'H':
PrintUsage(EXIT_SUCCESS, stdout);
default:
PrintUsage(EX_USAGE, stderr);
}
}
}
int main(int argc, char *argv[]) {
int i;
void *rgb;
size_t size;
char *option;
unsigned yd, xd;
__fast_math();
showcrashreports();
cancolor();
GetOpts(argc, argv);
// if sizes are given, 2 cases:
// - positive values: use that as the target size
// - negative values: add, for ex to offset the command prompt size
GetTermSize(&yd, &xd);
if (y_ <= 0) {
y_ += yd;
}
if (x_ <= 0) {
x_ += xd;
}
// FIXME: on the conversion stage should do 2Y because of halfblocks
// printf( "filename >%s<\tx >%d<\ty >%d<\n\n", filename, x_, y_);
size = y_ * YS * x_ * XS * CN;
CHECK_NE(MAP_FAILED, (rgb = mapanon(ROUNDUP(size, FRAMESIZE))));
for (i = optind; i < argc; ++i) {
if (!argv[i]) continue;
if (m_) {
LoadFileViaImageMagick(argv[i], y_, x_, rgb);
} else {
LoadFile(argv[i], y_, x_, rgb);
}
PrintImage(y_, x_, rgb);
}
munmap(rgb, ROUNDUP(size, FRAMESIZE));
return 0;
}