You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/lib/sixel.c

1794 lines
60 KiB
C

#include <math.h>
#include <stdatomic.h>
#include "internal.h"
#include "fbuf.h"
#define RGBSIZE 3
// number of worker threads
// FIXME fit to local machine, but more than 3 never seems to help
#define POPULATION 3
// a worker can have up to three qstates enqueued for work
#define WORKERDEPTH 3
// this palette entry is a sentinel for a transparent pixel (and thus caps
// the palette at 65535 other entries).
#define TRANS_PALETTE_ENTRY 65535
// bytes per element in the auxiliary vector
#define AUXVECELEMSIZE 2
// three scaled sixel [0..100x3] components plus a population count.
typedef struct qsample {
unsigned char comps[RGBSIZE];
uint32_t pop;
} qsample;
// lowest samples for each node. first-order nodes track 1000 points in
// sixelspace (10x10x10). there are eight possible second-order nodes from a
// fractured first-order node, covering 125 points each (5x5x5).
typedef struct qnode {
qsample q;
// cidx plays two roles. during merge, we select the active set, and extract
// them (since they'll be sorted, we can't operate directly on the octree).
// here, we use cidx to map back to the initial octree entry, as we need
// update them (from the active set) at the end of merging. afterwards, the
// high bit indicates that it was chosen, and the cidx is a valid index into
// the final color table. it is otherwise a link to the merged qnode.
// during initial filtering, qlink determines whether a node has fractured:
// if qlink is non-zero, it is a one-biased index to an onode.
// FIXME combine these once more, but for now to keep it easy, we have two.
// qlink links back into the octree.
uint16_t qlink;
uint16_t cidx;
} qnode;
// an octree-style node, used for fractured first-order nodes. the first
// bit is whether we're on the top or bottom of the R, then G, then B.
typedef struct onode {
qnode* q[8];
} onode;
// we set P2 based on whether there is any transparency in the sixel. if not,
// use SIXEL_P2_ALLOPAQUE (0), for faster drawing in certain terminals.
typedef enum {
SIXEL_P2_ALLOPAQUE = 0,
SIXEL_P2_TRANS = 1,
} sixel_p2_e;
// data for a single sixelband. a vector of sixel rows, one for each color
// represented within the band. we initially create a vector for every
// possible (quantized) color, and then collapse it.
typedef struct sixelband {
int size; // capacity FIXME if same for all, eliminate this
char** vecs; // array of vectors, many of which can be NULL
} sixelband;
// across the life of the sixel, we'll need to wipe and restore cells, without
// recourse to the original RGBA data. this is prohibitively expensive to do on
// the encoded data, since it might require expanding or collapsing sections in
// the middle (we could use a rope, but it would be annoying). instead, we keep
// for each sixelrow (i.e. for every 6 rows) a vector of colors and distinct
// encoded sections (i.e. *not* from some common long single allocation). this
// way, the encoded sections can be easily and cheaply changed (since they're
// small, and quickly indexed by sixelrow * color)). whenever we want to emit
// the sixel, we just gather all these dynamic sections and write them
// successively into the fbuf. this table can be built up in parallel, since
// it's isolated among sixelrows -- the sixelrow is then the natural work unit.
// this sixelmap is kept across the life of the sprixel; any longlived state
// must be here, whereas state necessary only for rendering ought be in qstate.
typedef struct sixelmap {
int colors;
int sixelbands;
sixelband* bands; // |sixelbands| collections of sixel vectors
sixel_p2_e p2; // set to SIXEL_P2_TRANS if we have transparent pixels
} sixelmap;
typedef struct qstate {
int refcount; // initialized to worker count
atomic_int bandbuilder; // threads take bands as their work unit
// we always work in terms of quantized colors (quantization is the first
// step of rendering), using indexes into the derived palette. the actual
// palette need only be stored during the initial render, since the sixel
// header can be preserved, and the palette is unchanged by wipes/restores.
unsigned char* table; // |colors| x RGBSIZE components
qnode* qnodes;
onode* onodes;
unsigned dynnodes_free;
unsigned dynnodes_total;
unsigned onodes_free;
unsigned onodes_total;
const struct blitterargs* bargs;
const uint32_t* data;
int linesize;
sixelmap* smap;
// these are the leny and lenx passed to sixel_blit(), which are likely
// different from those reachable through bargs->len{y,x}!
int leny, lenx;
} qstate;
// a work_queue per worker thread. if used == WORKERDEPTH, this thread is
// backed up, and we cannot enqueue to it. writeto wraps around the array.
typedef struct work_queue {
qstate* qstates[WORKERDEPTH];
unsigned writeto;
unsigned used;
struct sixel_engine* sengine;
} work_queue;
// we keep a few worker threads (POPULATION) spun up to assist with
// quantization. each has an array of up to WORKERDEPTH qstates to work on.
typedef struct sixel_engine {
pthread_mutex_t lock;
pthread_cond_t cond;
work_queue queues[POPULATION];
pthread_t tids[POPULATION];
bool done;
} sixel_engine;
// enqueue |qs| to any workers with available space. the number of workers with
// a reference will be stored in |qs|->refcount.
static void
enqueue_to_workers(sixel_engine* eng, qstate* qs){
if(eng == NULL){
return;
}
int usecount = 0;
pthread_mutex_lock(&eng->lock);
for(int i = 0 ; i < POPULATION ; ++i){
work_queue* wq = &eng->queues[i];
if(wq->used < WORKERDEPTH){
wq->qstates[wq->writeto] = qs;
++wq->used;
++usecount;
if(++wq->writeto == WORKERDEPTH){
wq->writeto = 0;
}
}
}
qs->refcount = usecount;
pthread_mutex_unlock(&eng->lock);
if(usecount){
pthread_cond_broadcast(&eng->cond);
}
}
// block until all workers have finished up with |qs|
static void
block_on_workers(sixel_engine* eng, qstate* qs){
if(eng == NULL){
return;
}
pthread_mutex_lock(&eng->lock);
while(qs->refcount){
pthread_cond_wait(&eng->cond, &eng->lock);
}
pthread_mutex_unlock(&eng->lock);
}
// returns the number of individual sixels necessary to represent the specified
// pixel geometry. these might encompass more pixel rows than |dimy| would
// suggest, up to the next multiple of 6 (i.e. a single row becomes a 6-row
// bitmap; as do two, three, four, five, or six rows). input is scaled geometry.
static inline int
sixelcount(int dimy, int dimx){
return (dimy + 5) / 6 * dimx;
}
// returns the number of sixel bands (horizontal series of sixels, aka 6 rows)
// for |dimy| source rows. sixels are encoded as a series of sixel bands.
static inline int
sixelbandcount(int dimy){
return sixelcount(dimy, 1);
}
// whip up a sixelmap sans data for the specified pixel geometry and color
// register count.
static sixelmap*
sixelmap_create(int dimy){
sixelmap* ret = malloc(sizeof(*ret));
if(ret){
ret->p2 = SIXEL_P2_ALLOPAQUE;
// they'll be initialized by their workers, possibly in parallel
ret->sixelbands = sixelbandcount(dimy);
ret->bands = malloc(sizeof(*ret->bands) * ret->sixelbands);
if(ret->bands == NULL){
free(ret);
return NULL;
}
for(int i = 0 ; i < ret->sixelbands ; ++i){
ret->bands[i].size = 0;
}
ret->colors = 0;
}
return ret;
}
static inline void
sixelband_free(sixelband* s){
for(int j = 0 ; j < s->size ; ++j){
free(s->vecs[j]);
}
free(s->vecs);
}
void sixelmap_free(sixelmap *s){
if(s){
for(int i = 0 ; i < s->sixelbands ; ++i){
sixelband_free(&s->bands[i]);
}
free(s->bands);
free(s);
}
}
// convert rgb [0..255] to sixel [0..99]
static inline unsigned
ss(unsigned c){
unsigned r = round(c * 100.0 / 255); // use real [0..100] scaling
return r > 99 ? 99: r;
}
// get the keys for an rgb point. the returned value is on [0..999], and maps
// to a static qnode. the second value is on [0..7], and maps within the
// fractured onode (if necessary).
static inline unsigned
qnode_keys(unsigned r, unsigned g, unsigned b, unsigned *skey){
unsigned ssr = ss(r);
unsigned ssg = ss(g);
unsigned ssb = ss(b);
unsigned ret = ssr / 10 * 100 + ssg / 10 * 10 + ssb / 10;
*skey = (((ssr % 10) / 5) << 2u) +
(((ssg % 10) / 5) << 1u) +
((ssb % 10) / 5);
//fprintf(stderr, "0x%02x 0x%02x 0x%02x %02u %02u %02u %u %u\n", r, g, b, ssr, ssg, ssb, ret, *skey);
return ret;
}
// have we been chosen for the color table?
static inline bool
chosen_p(const qnode* q){
return q->cidx & 0x8000u;
}
static inline unsigned
make_chosen(unsigned cidx){
return cidx | 0x8000u;
}
// get the cidx without the chosen bit
static inline unsigned
qidx(const qnode* q){
return q->cidx & ~0x8000u;
}
#define QNODECOUNT 1000
// create+zorch an array of QNODECOUNT qnodes. this is 1000 entries covering
// 1000 sixel colors each (we pretend 100 doesn't exist, working on [0..99],
// heh). in addition, at the end we allocate |colorregs| qnodes, to be used
// dynamically in "fracturing". the original QNODECOUNT nodes are a static
// octree, flattened into an array; the latter are used as an actual octree.
// we must have 8 dynnodes available for every onode we create, or we can run
// into a situation where we don't have an available dynnode
// (see insert_color()).
static qstate*
alloc_qstate(unsigned colorregs){
qstate* qs = malloc(sizeof(*qs));
if(qs){
qs->dynnodes_free = colorregs;
qs->dynnodes_total = qs->dynnodes_free;
if((qs->qnodes = malloc((QNODECOUNT + qs->dynnodes_total) * sizeof(qnode))) == NULL){
free(qs);
return NULL;
}
qs->onodes_free = qs->dynnodes_total / 8;
qs->onodes_total = qs->onodes_free;
if((qs->onodes = malloc(qs->onodes_total * sizeof(*qs->onodes))) == NULL){
free(qs->qnodes);
free(qs);
return NULL;
}
// don't technically need to clear the components, as we could
// check the pop, but it's hidden under the compulsory cache misses.
// we only initialize the static nodes, not the dynamic ones--we know
// when we pull a dynamic one that it needs its popcount initialized.
memset(qs->qnodes, 0, sizeof(qnode) * QNODECOUNT);
qs->table = NULL;
}
return qs;
}
// free internals of qstate object
static void
free_qstate(qstate *qs){
if(qs){
loginfo("freeing qstate");
free(qs->qnodes);
free(qs->onodes);
free(qs->table);
free(qs);
}
}
// insert a color from the source image into the octree.
static inline int
insert_color(qstate* qs, uint32_t pixel){
const unsigned r = ncpixel_r(pixel);
const unsigned g = ncpixel_g(pixel);
const unsigned b = ncpixel_b(pixel);
unsigned skey;
const unsigned key = qnode_keys(r, g, b, &skey);
assert(key < QNODECOUNT);
assert(skey < 8);
qnode* q = &qs->qnodes[key];
if(q->q.pop == 0 && q->qlink == 0){ // previously-unused node
q->q.comps[0] = r;
q->q.comps[1] = g;
q->q.comps[2] = b;
q->q.pop = 1;
++qs->smap->colors;
return 0;
}
onode* o;
// it's not a fractured node, but it's been used. check to see if we
// match the secondary key of what's here.
if(q->qlink == 0){
unsigned skeynat;
qnode_keys(q->q.comps[0], q->q.comps[1], q->q.comps[2], &skeynat);
if(skey == skeynat){
++q->q.pop; // pretty good match
return 0;
}
// we want to fracture. if we have no onodes, though, we can't.
// we also need at least one dynamic qnode. note that this means we might
// open an onode just to fail to insert our current lookup; that's fine;
// it's a symmetry between creation and extension.
if(qs->dynnodes_free == 0 || qs->onodes_free == 0){
//fprintf(stderr, "NO FREE ONES %u\n", key);
++q->q.pop; // not a great match, but we're already scattered
return 0;
}
// get the next free onode and zorch it out
o = qs->onodes + qs->onodes_total - qs->onodes_free;
//fprintf(stderr, "o: %p obase: %p %u\n", o, qs->onodes, qs->onodes_total - qs->onodes_free);
memset(o, 0, sizeof(*o));
// get the next free dynnode and assign it to o, account for dnode
o->q[skeynat] = &qs->qnodes[QNODECOUNT + qs->dynnodes_total - qs->dynnodes_free];
--qs->dynnodes_free;
// copy over our own details
memcpy(o->q[skeynat], q, sizeof(*q));
// set qlink to one-biased index of the onode, and account for onode
q->qlink = qs->onodes_total - qs->onodes_free + 1;
--qs->onodes_free;
// reset our own population count
q->q.pop = 0;
}else{
// the node has already been fractured
o = qs->onodes + (q->qlink - 1);
}
if(o->q[skey]){
// our subnode is already present, huzzah. increase its popcount.
++o->q[skey]->q.pop;
return 0;
}
// we try otherwise to insert ourselves into o. this requires a free dynnode.
if(qs->dynnodes_free == 0){
//fprintf(stderr, "NO DYNFREE %u\n", key);
// this should never happen, because we always ought have 8 dynnodes for
// every possible onode.
return -1;
}
// get the next free dynnode and assign it to o, account for dnode
o->q[skey] = &qs->qnodes[QNODECOUNT + qs->dynnodes_total - qs->dynnodes_free];
--qs->dynnodes_free;
o->q[skey]->q.pop = 1;
o->q[skey]->q.comps[0] = r;
o->q[skey]->q.comps[1] = g;
o->q[skey]->q.comps[2] = b;
o->q[skey]->qlink = 0;
o->q[skey]->cidx = 0;
++qs->smap->colors;
//fprintf(stderr, "INSERTED[%u]: %u %u %u\n", key, q->q.comps[0], q->q.comps[1], q->q.comps[2]);
return 0;
}
// resolve the input color to a color table index following any postprocessing
// of the octree.
static inline int
find_color(const qstate* qs, uint32_t pixel){
const unsigned r = ncpixel_r(pixel);
const unsigned g = ncpixel_g(pixel);
const unsigned b = ncpixel_b(pixel);
unsigned skey;
const unsigned key = qnode_keys(r, g, b, &skey);
const qnode* q = &qs->qnodes[key];
if(q->qlink && q->q.pop == 0){
if(qs->onodes[q->qlink - 1].q[skey]){
q = qs->onodes[q->qlink - 1].q[skey];
}else{
logpanic("internal error: no color for 0x%016x", pixel);
return -1;
}
}
return qidx(q);
}
// the P2 parameter on a sixel specifies how unspecified pixels are drawn.
// if P2 is 1, unspecified pixels are transparent. otherwise, they're drawn
// as something else. some terminals (e.g. foot) can draw more quickly if
// P2 is 0, so we set that when we have no transparent pixels -- i.e. when
// all TAM entries are 0. P2 is at a fixed location in the sixel header.
// obviously, the sixel must already exist.
static inline void
change_p2(char* sixel, sixel_p2_e value){
sixel[4] = value + '0';
}
static inline void
write_rle(char* vec, int* voff, int rle, int rep){
if(rle > 2){
*voff += sprintf(vec + *voff, "!%d", rle);
}else if(rle == 2){
vec[(*voff)++] = rep;
}
if(rle){
vec[(*voff)++] = rep;
}
vec[*voff] = '\0';
}
// one for each color in the band we're building. |rle| tracks the number of
// consecutive unwritten instances of the current non-0 rep, which is itself
// tracked in |rep|. |wrote| tracks the number of sixels written out for this
// color. whenever we get a new rep (this only happens for non-zero reps),
// we must write any old rle rep, plus any zero-reps since then.
struct band_extender {
int length; // current length of the vector
int rle; // current rep count of non-zero sixel for this color
int wrote; // number of sixels we've written out
int rep; // representation, 0..63
};
// add the supplied rle section to the appropriate vector, which might
// need to be created. we are writing out [bes->wrote, curx) (i.e. curx
// ought *not* describe the |bes| element, and ought equal |dimx| when
// finalizing the band). caller must update bes->wrote afterwards!
static inline char*
sixelband_extend(char* vec, struct band_extender* bes, int dimx, int curx){
assert(dimx >= bes->rle);
assert(0 <= bes->rle);
assert(0 <= bes->rep);
assert(64 > bes->rep);
if(vec == NULL){
// FIXME for now we make it as big as it could possibly need to be. ps,
// don't try to just base it off how far in we are; wipe/restore could
// change that!
if((vec = malloc(dimx + 1)) == NULL){
return NULL;
}
}
// rle will equal 0 if this is our first non-zero rep, at a non-zero x;
// in that case, rep is guaranteed to be 0; catch it at the bottom.
write_rle(vec, &bes->length, bes->rle, bes->rep + 63);
int clearlen = curx - (bes->rle + bes->wrote);
write_rle(vec, &bes->length, clearlen, '?');
return vec;
}
// write to this cell's auxvec, backing up the pixels cleared by a wipe. wipes
// are requested at cell granularity, broken down into sixelbands, broken down
// by color, and then finally effected at the sixel RLE level. we're thus in
// any given call handling a horizontal contiguous range of sixels for a single
// color. the x range is wholly within the cell to be wiped, but the y range
// might not be, since cells and bands don't necessarily line up. |y| ought be
// the row of the first pixel of the *band*.
//
// we thus need:
// - the starting and ending true x positions for the *portion of this sixel
// contained within the wiped cell*.
// - the true y position at which the sixel starts.
// - the previous sixel rep and the masked sixel rep--the difference between
// the two tells us which rows (offset from y) need be written. they ought
// be the binary forms, not the presentation forms (i.e. [0..63]).
// - the cell-pixel geometry, necessary for computing offset into the auxvec.
// - the color.
//
// precondition: mask is a bitwise proper subset of rep
//
// we find which [1..6] of six rows are affected by examining the difference
// between |rep| and |masked|, the sixel's row within the cell by taking |y|
// modulo |cellpxy|, and the position within the auxvec by multiplying that
// result by |cellpxx| and adding |x| modulo |cellpxx|. we set |len| pixels.
static inline void
write_auxvec(uint8_t* auxvec, uint16_t color, int endy, int y, int x, int len,
char rep, char masked, int cellpxy, int cellpxx){
rep -= 63;
masked -= 63;
const char diff = rep ^ masked;
//fprintf(stderr, "AUXVEC WRITE[%hu] ey: %d y/x: %d/%d:%d r: 0x%x m: 0x%x d: 0x%x total %d\n", color, endy, y, x, len, rep, masked, diff, cellpxy * cellpxx);
const int xoff = x % cellpxx;
const int yoff = y % cellpxy;
int dy = 0;
for(char bitselector = 1 ; bitselector < 0x40 ; bitselector <<= 1u, ++dy){
if((diff & bitselector) == 0){
//if(diff == 0x20)fprintf(stderr, "diff: 0x%x bs: %d\n", diff, bitselector);
continue;
}
if(yoff + dy == endy){ // reached the next cell below
//if(diff == 0x20)fprintf(stderr, "BOUNCING! 0x%x bs: %d %d > %d\n", diff, bitselector, yoff + dy, cellpxy);
break;
}
//fprintf(stderr, " writing to auxrow %d (%d)\n", yoff + dy, bitselector);
const int idx = (((yoff + dy) % cellpxy) * cellpxx + xoff) * AUXVECELEMSIZE;
//fprintf(stderr, " xoff: %d yoff: %d dy: %d ydy: %d idx: %d\n", xoff, yoff, dy, yoff + dy, idx);
for(int i = 0 ; i < len ; ++i){
memcpy(&auxvec[idx + i * AUXVECELEMSIZE], &color, AUXVECELEMSIZE);
}
}
}
// wipe the color within this band from startx to endx - 1, from starty to
// endy - 1 (0-offset in the band, a cell-sized region), writing out the
// auxvec. mask is the allowable sixel, y-wise. returns a positive number if
// pixels were wiped.
static inline int
wipe_color(sixelband* b, int color, int y, int endy, int startx, int endx,
char mask, int dimx, uint8_t* auxvec,
int cellpxy, int cellpxx){
const char* vec = b->vecs[color];
if(vec == NULL){
return 0; // no work to be done here
}
int wiped = 0;
char* newvec = malloc(dimx + 1);
if(newvec == NULL){
return -1;
}
//fprintf(stderr, "color: %d Y: %d-%d X: %d-%d\n", color, starty, endy, startx, endx);
//fprintf(stderr, "s/e: %d/%d mask: %02x WIPE: [%s]\n", starty, endy, mask, vec);
// we decode the color within the sixelband, and rebuild it without the
// wiped pixels.
int rle = 0; // the repetition number for this element
// the x coordinate through which we've checked this band. if x + rle is
// less than startx, this element cannot be affected by the wipe.
// otherwise, starting at startx, it can be affected. once x >= endx, we
// are done, and can copy any remaining elements blindly.
int x = 0;
int voff = 0;
while(*vec){
if(isdigit(*vec)){
rle *= 10;
rle += (*vec - '0');
}else if(*vec == '!'){
rle = 0;
}else{
if(rle == 0){
rle = 1;
}
char rep = *vec;
char masked = ((rep - 63) & mask) + 63;
//fprintf(stderr, "X/RLE/ENDX: %d %d %d\n", x, rle, endx);
if(x + rle <= startx){ // not wiped material; reproduce as-is
write_rle(newvec, &voff, rle, rep);
x += rle;
}else if(masked == rep){ // not changed by wipe; reproduce as-is
write_rle(newvec, &voff, rle, rep);
x += rle;
}else{ // changed by wipe; might have to break it up
wiped = 1;
if(x < startx){
write_rle(newvec, &voff, startx - x, rep);
rle -= startx - x;
x = startx;
}
if(x + rle >= endx){
// FIXME this might equal the prev/next rep, and we ought combine
//fprintf(stderr, "************************* %d %d %d\n", endx - x, x, rle);
write_rle(newvec, &voff, endx - x, masked);
write_auxvec(auxvec, color, endy, y, x, endx - x, rep, masked, cellpxy, cellpxx);
rle -= endx - x;
x = endx;
}else{
write_rle(newvec, &voff, rle, masked);
write_auxvec(auxvec, color, endy, y, x, rle, rep, masked, cellpxy, cellpxx);
x += rle;
rle = 0;
}
if(rle){
write_rle(newvec, &voff, rle, rep);
x += rle;
}
}
rle = 0;
}
++vec;
if(x >= endx){
strcpy(newvec + voff, vec); // there is always room
break;
}
}
//if(strcmp(newvec, b->vecs[color])) fprintf(stderr, "WIPED %d y [%d..%d) x [%d..%d) mask: %d [%s]\n", color, starty, endy, startx, endx, mask, newvec);
free(b->vecs[color]);
if(voff == 0){
// FIXME check for other null vectors; free such, and assign NULL
free(newvec);
newvec = NULL;
}
b->vecs[color] = newvec;
return wiped;
}
// wipe the band from startx to endx - 1, from starty to endy - 1. returns the
// number of pixels actually wiped.
static inline int
wipe_band(sixelmap* smap, int band, int startx, int endx,
int starty, int endy, int dimx, int cellpxy, int cellpxx,
uint8_t* auxvec){
int wiped = 0;
// get 0-offset start and end row bounds for our band.
const int sy = band * 6 < starty ? starty - band * 6 : 0;
const int ey = (band + 1) * 6 > endy ? 6 - ((band + 1) * 6 - endy) : 6;
// we've got a mask that we'll AND with the decoded sixels; set it to
// 0 wherever we're wiping.
unsigned char mask = 63;
// knock out a bit for each row we're wiping within the band
for(int i = 0 ; i < 6 ; ++i){
if(i >= sy && i < ey){
mask &= ~(1u << i);
}
}
//fprintf(stderr, "******************** BAND %d MASK 0x%x ********************8\n", band, mask);
sixelband* b = &smap->bands[band];
// offset into map->data where our color starts
for(int i = 0 ; i < b->size ; ++i){
wiped += wipe_color(b, i, band * 6, endy, startx, endx, mask,
dimx, auxvec, cellpxy, cellpxx);
}
return wiped;
}
// we return -1 because we're not doing a proper wipe -- that's not possible
// using sixel. we just mark it as partially transparent, so that if it's
// redrawn, it's redrawn using P2=1.
int sixel_wipe(sprixel* s, int ycell, int xcell){
//fprintf(stderr, "WIPING %d/%d\n", ycell, xcell);
uint8_t* auxvec = sixel_trans_auxvec(ncplane_pile(s->n));
if(auxvec == NULL){
return -1;
}
const int cellpxy = ncplane_pile(s->n)->cellpxy;
const int cellpxx = ncplane_pile(s->n)->cellpxx;
sixelmap* smap = s->smap;
const int startx = xcell * cellpxx;
const int starty = ycell * cellpxy;
int endx = ((xcell + 1) * cellpxx);
if(endx >= s->pixx){
endx = s->pixx;
}
int endy = ((ycell + 1) * cellpxy);
if(endy >= s->pixy){
endy = s->pixy;
}
const int startband = starty / 6;
const int endband = (endy - 1) / 6;
//fprintf(stderr, "y/x: %d/%d bands: %d-%d start: %d/%d end: %d/%d\n", ycell, xcell, startband, endband - 1, starty, startx, endy, endx);
// walk through each color, and wipe the necessary sixels from each band
int w = 0;
for(int b = startband ; b <= endband ; ++b){
w += wipe_band(smap, b, startx, endx, starty, endy, s->pixx,
cellpxy, cellpxx, auxvec);
}
if(w){
s->wipes_outstanding = true;
}
change_p2(s->glyph.buf, SIXEL_P2_TRANS);
assert(NULL == s->n->tam[s->dimx * ycell + xcell].auxvector);
s->n->tam[s->dimx * ycell + xcell].auxvector = auxvec;
// FIXME this invalidation ought not be necessary, since we're simply
// wiping, and thus a glyph is going to be printed over whatever we've
// just destroyed. in alacritty, however, this isn't sufficient to knock
// out a graphic; we need repaint with the transparency.
// see https://github.com/dankamongmen/notcurses/issues/2142
int absx, absy;
ncplane_abs_yx(s->n, &absy, &absx);
sprixel_invalidate(s, absy, absx);
return 0;
}
// rebuilds the auxiliary vectors, and scrubs the actual pixels, following
// extraction of the palette. doing so allows the new frame's pixels to
// contribute to the solved palette, even if they were wiped in the previous
// frame. pixels ought thus have been set up in sixel_blit(), despite TAM
// entries in the ANNIHILATED state.
static int
scrub_color_table(sprixel* s){
if(s->n && s->n->tam){
// we use the sprixel cell geometry rather than the plane's because this
// is called during our initial blit, before we've resized the plane.
for(unsigned y = 0 ; y < s->dimy ; ++y){
for(unsigned x = 0 ; x < s->dimx ; ++x){
unsigned txyidx = y * s->dimx + x;
sprixcell_e state = s->n->tam[txyidx].state;
if(state == SPRIXCELL_ANNIHILATED || state == SPRIXCELL_ANNIHILATED_TRANS){
//fprintf(stderr, "POSTEXTRACT WIPE %d/%d\n", y, x);
sixel_wipe(s, y, x);
}
}
}
}
return 0;
}
// goes through the needs_refresh matrix, and damages cells needing refreshing.
void sixel_refresh(const ncpile* p, sprixel* s){
if(s->needs_refresh == NULL){
return;
}
int absy, absx;
ncplane_abs_yx(s->n, &absy, &absx);
for(unsigned y = 0 ; y < s->dimy ; ++y){
const unsigned yy = absy + y;
for(unsigned x = 0 ; x < s->dimx ; ++x){
unsigned idx = y * s->dimx + x;
if(s->needs_refresh[idx]){
const unsigned xx = absx + x;
if(xx < p->dimx && yy < p->dimy){
unsigned ridx = yy * p->dimx + xx;
struct crender *r = &p->crender[ridx];
r->s.damaged = 1;
}
}
}
}
free(s->needs_refresh);
s->needs_refresh = NULL;
}
// when we first cross into a new cell, we check its old state, and if it
// was transparent, set the rmatrix low. otherwise, set it high. this should
// only be called for the first pixel in each cell.
static inline void
update_rmatrix(unsigned char* rmatrix, int txyidx, const tament* tam){
if(rmatrix == NULL){
return;
}
sprixcell_e state = tam[txyidx].state;
if(state == SPRIXCELL_TRANSPARENT || state > SPRIXCELL_ANNIHILATED){
rmatrix[txyidx] = 0;
}else{
rmatrix[txyidx] = 1;
}
}
static int
qnodecmp(const void* q0, const void* q1){
const qnode* qa = q0;
const qnode* qb = q1;
return qa->q.pop < qb->q.pop ? -1 : qa->q.pop == qb->q.pop ? 0 : 1;
}
// from the initial set of QNODECOUNT qnodes, extract the number of active
// ones -- our initial (reduced) color count -- and sort. heap allocation.
// precondition: colors > 0
static qnode*
get_active_set(qstate* qs, uint32_t colors){
qnode* act = malloc(sizeof(*act) * colors);
unsigned targidx = 0;
// filter the initial qnodes for pop != 0
unsigned total = QNODECOUNT + (qs->dynnodes_total - qs->dynnodes_free);
//fprintf(stderr, "TOTAL IS %u WITH %u COLORS\n", total, colors);
for(unsigned z = 0 ; z < total && targidx < colors ; ++z){
//fprintf(stderr, "EXTRACT? [%04u] pop %u\n", z, qs->qnodes[z].q.pop);
if(qs->qnodes[z].q.pop){
memcpy(&act[targidx], &qs->qnodes[z], sizeof(*act));
// link it back to the original node's position in the octree
//fprintf(stderr, "LINKING %u to %u\n", targidx, z);
act[targidx].qlink = z;
++targidx;
}else if(qs->qnodes[z].qlink){
const struct onode* o = &qs->onodes[qs->qnodes[z].qlink - 1];
// FIXME i don't think we need the second conditional? in a perfect world?
for(unsigned s = 0 ; s < 8 && targidx < colors ; ++s){
//fprintf(stderr, "o: %p qlink: %u\n", o, qs->qnodes[z].qlink - 1);
if(o->q[s]){
memcpy(&act[targidx], o->q[s], sizeof(*act));
//fprintf(stderr, "O-LINKING %u to %ld[%u]\n", targidx, o->q[s] - qs->qnodes, s);
act[targidx].qlink = o->q[s] - qs->qnodes;
++targidx;
}
}
}
}
//fprintf(stderr, "targidx: %u colors: %u\n", targidx, colors);
assert(targidx == colors);
qsort(act, colors, sizeof(*act), qnodecmp);
return act;
}
static inline int
find_next_lowest_chosen(const qstate* qs, int z, int i, const qnode** hq){
//fprintf(stderr, "FIRST CHOSEN: %u %d\n", z, i);
do{
const qnode* h = &qs->qnodes[z];
//fprintf(stderr, "LOOKING AT %u POP %u QLINK %u CIDX %u\n", z, h->q.pop, h->qlink, h->cidx);
if(h->q.pop == 0 && h->qlink){
const onode* o = &qs->onodes[h->qlink - 1];
while(i >= 0){
h = o->q[i];
if(h && chosen_p(h)){
*hq = h;
//fprintf(stderr, "NEW HQ: %p RET: %u\n", *hq, z * 8 + i);
return z * 8 + i;
}
if(++i == 8){
break;
}
}
}else{
if(chosen_p(h)){
*hq = h;
//fprintf(stderr, "NEW HQ: %p RET: %u\n", *hq, z * 8);
return z * 8;
}
}
++z;
i = 0;
}while(z < QNODECOUNT);
//fprintf(stderr, "RETURNING -1\n");
return -1;
}
static inline void
choose(qstate* qs, qnode* q, int z, int i, int* hi, int* lo,
const qnode** hq, const qnode** lq){
if(!chosen_p(q)){
//fprintf(stderr, "NOT CHOSEN: %u %u %u %u\n", z, qs->qnodes[z].qlink, qs->qnodes[z].q.pop, qs->qnodes[z].cidx);
if(z * 8 > *hi){
*hi = find_next_lowest_chosen(qs, z, i, hq);
}
int cur = z * 8 + (i >= 0 ? i : 4);
if(*lo == -1){
q->cidx = qidx(*hq);
}else if(*hi == -1 || cur - *lo < *hi - cur){
q->cidx = qidx(*lq);
}else{
q->cidx = qidx(*hq);
}
}else{
*lq = q;
*lo = z * 8;
}
}
// we must reduce the number of colors until we're using less than or equal
// to the number of color registers.
static inline int
merge_color_table(qstate* qs){
if(qs->smap->colors == 0){
return 0;
}
qnode* qactive = get_active_set(qs, qs->smap->colors);
if(qactive == NULL){
return -1;
}
// assign color table entries to the most popular colors. use the lowest
// color table entries for the most popular ones, as they're the shortest
// (this is not necessarily an optimizing huristic, but it'll do for now).
int cidx = 0;
//fprintf(stderr, "colors: %u cregs: %u\n", qs->colors, colorregs);
for(int z = qs->smap->colors - 1 ; z >= 0 ; --z){
if(qs->smap->colors >= qs->bargs->u.pixel.colorregs){
if(cidx == qs->bargs->u.pixel.colorregs){
break; // we just ran out of color registers
}
}
qs->qnodes[qactive[z].qlink].cidx = make_chosen(cidx);
++cidx;
}
free(qactive);
if(qs->smap->colors > qs->bargs->u.pixel.colorregs){
// tend to those which couldn't get a color table entry. we start with two
// values, lo and hi, initialized to -1. we iterate over the *static* qnodes,
// descending into onodes to check their qnodes. we thus iterate over all
// used qnodes, in order (and also unused static qnodes). if the node is
// empty, continue. if it is chosen, replace lo. otherwise, if hi is less
// than z, we need find the next lowest chosen one. if there is no next
// lowest, hi is reset to -1. otherwise, set hi. once we have the new hi > z,
// determine which of hi and lo are closer to z, discounting -1 values, and
// link te closer one to z. a toplevel node is worth 8 in terms of distance;
// and lowlevel node is worth 1.
int lo = -1;
int hi = -1;
const qnode* lq = NULL;
const qnode* hq = NULL;
for(int z = 0 ; z < QNODECOUNT ; ++z){
if(qs->qnodes[z].q.pop == 0){
if(qs->qnodes[z].qlink == 0){
continue; // unused
}
// process the onode
const onode* o = &qs->onodes[qs->qnodes[z].qlink - 1];
for(int i = 0 ; i < 8 ; ++i){
if(o->q[i]){
choose(qs, o->q[i], z, i, &hi, &lo, &hq, &lq);
}
}
}else{
choose(qs, &qs->qnodes[z], z, -1, &hi, &lo, &hq, &lq);
}
}
qs->smap->colors = qs->bargs->u.pixel.colorregs;
}
return 0;
}
static inline void
load_color_table(const qstate* qs){
int loaded = 0;
int total = QNODECOUNT + (qs->dynnodes_total - qs->dynnodes_free);
for(int z = 0 ; z < total && loaded < qs->smap->colors ; ++z){
const qnode* q = &qs->qnodes[z];
if(chosen_p(q)){
qs->table[RGBSIZE * qidx(q) + 0] = ss(q->q.comps[0]);
qs->table[RGBSIZE * qidx(q) + 1] = ss(q->q.comps[1]);
qs->table[RGBSIZE * qidx(q) + 2] = ss(q->q.comps[2]);
++loaded;
}
}
//fprintf(stderr, "loaded: %u colors: %u\n", loaded, qs->colors);
assert(loaded == qs->smap->colors);
}
// build up a sixel band from (up to) 6 rows of the source RGBA.
static inline int
build_sixel_band(qstate* qs, int bnum){
//fprintf(stderr, "building band %d\n", bnum);
sixelband* b = &qs->smap->bands[bnum];
b->size = qs->smap->colors;
size_t bsize = sizeof(*b->vecs) * b->size;
size_t mlen = qs->smap->colors * sizeof(struct band_extender);
struct band_extender* meta = malloc(mlen);
if(meta == NULL){
return -1;
}
b->vecs = malloc(bsize);
if(b->vecs == NULL){
free(meta);
return -1;
}
memset(b->vecs, 0, bsize);
memset(meta, 0, mlen);
const int ystart = qs->bargs->begy + bnum * 6;
const int endy = (bnum + 1 == qs->smap->sixelbands ?
qs->leny - qs->bargs->begy : ystart + 6);
struct {
int color; // 0..colormax
int rep; // non-zero representation, 1..63
} active[6];
// we're going to advance horizontally through the sixelband
int x;
// FIXME we could greatly clean this up by tracking, for each color, the active
// rep and the number of times we've seen it...but only write it out either (a)
// when the rep changes (b) when we get the color again after a gap or (c) at the
// end. that way we wouldn't need maintain these prevactive/active sets...
for(x = qs->bargs->begx ; x < (qs->bargs->begx + qs->lenx) ; ++x){ // pixel column
// there are at most 6 colors represented in any given sixel. at each
// sixel, we need to *start tracking* new colors, and colors which changed
// their representation. we also write out what we previously tracked for
// this color: possibly a non-zero rep, possibly followed by a zero-rep (we
// can have zero, either, or both).
int activepos = 0; // number of active entries used
for(int y = ystart ; y < endy ; ++y){
const uint32_t* rgb = (qs->data + (qs->linesize / 4 * y) + x);
if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
continue;
}
int cidx = find_color(qs, *rgb);
if(cidx < 0){
free(meta);
return -1;
}
int act;
for(act = 0 ; act < activepos ; ++act){
if(active[act].color == cidx){
active[act].rep |= (1u << (y - ystart));
break;
}
}
if(act == activepos){ // didn't find it; create new entry
active[activepos].color = cidx;
active[activepos].rep = (1u << (y - ystart));
++activepos;
}
}
// we now have the active set. check to see if they extend existing RLEs,
// and if not, write out whatever came before us.
for(int i = 0 ; i < activepos ; ++i){
const int c = active[i].color;
if(meta[c].rep == active[i].rep && meta[c].rle + meta[c].wrote == x){
++meta[c].rle;
}else{
b->vecs[c] = sixelband_extend(b->vecs[c], &meta[c], qs->lenx, x);
if(b->vecs[c] == NULL){
free(meta);
return -1;
}
meta[c].rle = 1;
meta[c].wrote = x;
meta[c].rep = active[i].rep;
}
}
}
for(int i = 0 ; i < qs->smap->colors ; ++i){
if(meta[i].rle){ // color was wholly unused iff rle == 0 at end
b->vecs[i] = sixelband_extend(b->vecs[i], &meta[i], qs->lenx, x);
if(b->vecs[i] == NULL){
free(meta);
return -1;
}
}else{
b->vecs[i] = NULL;
}
}
free(meta);
return 0;
}
static int
bandworker(qstate* qs){
int b;
while((b = qs->bandbuilder++) < qs->smap->sixelbands){
if(build_sixel_band(qs, b) < 0){
return -1;
}
}
return 0;
}
// we have converged upon some number of colors. we now run over the pixels
// once again, and get the actual (color-indexed) sixels.
static inline int
build_data_table(sixel_engine* sengine, qstate* qs){
sixelmap* smap = qs->smap;
if(smap->sixelbands == 0){
logerror("no sixels");
return -1;
}
qs->bandbuilder = 0;
enqueue_to_workers(sengine, qs);
size_t tsize = RGBSIZE * smap->colors;
qs->table = malloc(tsize);
if(qs->table == NULL){
return -1;
}
load_color_table(qs);
bandworker(qs);
block_on_workers(sengine, qs);
return 0;
}
static inline int
extract_cell_color_table(qstate* qs, long cellid){
const int ccols = qs->bargs->u.pixel.spx->dimx;
const long x = cellid % ccols;
const long y = cellid / ccols;
const int cdimy = qs->bargs->u.pixel.cellpxy;
const int cdimx = qs->bargs->u.pixel.cellpxx;
const int begy = qs->bargs->begy;
const int begx = qs->bargs->begx;
const int leny = qs->leny;
const int lenx = qs->lenx;
const int cstartx = begx + x * cdimx; // starting pixel col for cell
const int cstarty = begy + y * cdimy; // starting pixel row for cell
typeof(qs->bargs->u.pixel.spx->needs_refresh) rmatrix = qs->bargs->u.pixel.spx->needs_refresh;
tament* tam = qs->bargs->u.pixel.spx->n->tam;
int cendy = cstarty + cdimy; // one past last pixel row for cell
if(cendy > begy + leny){
cendy = begy + leny;
}
int cendx = cstartx + cdimx; // one past last pixel col for cell
if(cendx > begx + lenx){
cendx = begx + lenx;
}
// we initialize the TAM entry based on the first pixel. if it's transparent,
// initialize as transparent, and otherwise as opaque. following that, any
// transparent pixel takes opaque to mixed, and any filled pixel takes
// transparent to mixed.
if(cstarty >= cendy){ // we're entirely transparent sixel overhead
tam[cellid].state = SPRIXCELL_TRANSPARENT;
qs->smap->p2 = SIXEL_P2_TRANS; // even one forces P2=1
// FIXME need we set rmatrix?
return 0;
}
const uint32_t* rgb = (qs->data + (qs->linesize / 4 * cstarty) + cstartx);
if(tam[cellid].state == SPRIXCELL_ANNIHILATED || tam[cellid].state == SPRIXCELL_ANNIHILATED_TRANS){
if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
update_rmatrix(rmatrix, cellid, tam);
tam[cellid].state = SPRIXCELL_ANNIHILATED_TRANS;
free(tam[cellid].auxvector);
tam[cellid].auxvector = NULL;
}else{
update_rmatrix(rmatrix, cellid, tam);
free(tam[cellid].auxvector);
tam[cellid].auxvector = NULL;
}
}else{
if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
update_rmatrix(rmatrix, cellid, tam);
tam[cellid].state = SPRIXCELL_TRANSPARENT;
}else{
update_rmatrix(rmatrix, cellid, tam);
tam[cellid].state = SPRIXCELL_OPAQUE_SIXEL;
}
}
for(int visy = cstarty ; visy < cendy ; ++visy){ // current abs pixel row
for(int visx = cstartx ; visx < cendx ; ++visx){ // current abs pixel col
rgb = (qs->data + (qs->linesize / 4 * visy) + visx);
// we do *not* exempt already-wiped pixels from palette creation. once
// we're done, we'll call sixel_wipe() on these cells. so they remain
// one of SPRIXCELL_ANNIHILATED or SPRIXCELL_ANNIHILATED_TRANS.
// intentional bitwise or, to avoid dependency
if(tam[cellid].state != SPRIXCELL_ANNIHILATED){
if(tam[cellid].state == SPRIXCELL_ANNIHILATED_TRANS){
if(!rgba_trans_p(*rgb, qs->bargs->transcolor)){
tam[cellid].state = SPRIXCELL_ANNIHILATED;
}
}else{
if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
if(tam[cellid].state == SPRIXCELL_OPAQUE_SIXEL){
tam[cellid].state = SPRIXCELL_MIXED_SIXEL;
}
}else{
if(tam[cellid].state == SPRIXCELL_TRANSPARENT){
tam[cellid].state = SPRIXCELL_MIXED_SIXEL;
}
}
}
}
//fprintf(stderr, "vis: %d/%d\n", visy, visx);
if(rgba_trans_p(*rgb, qs->bargs->transcolor)){
continue;
}
if(insert_color(qs, *rgb)){
return -1;
}
}
}
// if we're opaque, we needn't clear the old cell with a glyph
if(tam[cellid].state == SPRIXCELL_OPAQUE_SIXEL){
rmatrix[cellid] = 0;
}else{
qs->smap->p2 = SIXEL_P2_TRANS; // even one forces P2=1
}
return 0;
}
// we have a 4096-element array that takes the 4-5-3 MSBs from the RGB
// components. once it's complete, we might need to either merge some
// chunks, or expand them, converging towards the available number of
// color registers. |ccols| is cell geometry; |leny| and |lenx| are pixel
// geometry, and *do not* include sixel padding.
static int
extract_color_table(sixel_engine* sengine, qstate* qs){
const blitterargs* bargs = qs->bargs;
// use the cell geometry as computed by the visual layer; leny doesn't
// include any mandatory sixel padding.
const int crows = bargs->u.pixel.spx->dimy;
const int ccols = bargs->u.pixel.spx->dimx;
typeof(bargs->u.pixel.spx->needs_refresh) rmatrix;
rmatrix = malloc(sizeof(*rmatrix) * crows * ccols);
if(rmatrix == NULL){
return -1;
}
bargs->u.pixel.spx->needs_refresh = rmatrix;
long cellid = 0;
for(int y = 0 ; y < crows ; ++y){ // cell row
for(int x = 0 ; x < ccols ; ++x){ // cell column
if(extract_cell_color_table(qs, cellid)){
return -1;
}
++cellid;
}
}
loginfo("octree got %"PRIu32" entries", qs->smap->colors);
if(merge_color_table(qs)){
return -1;
}
if(build_data_table(sengine, qs)){
return -1;
}
loginfo("final palette: %u/%u colors", qs->smap->colors, qs->bargs->u.pixel.colorregs);
return 0;
}
static inline int
write_sixel_intro(fbuf* f, sixel_p2_e p2, int leny, int lenx){
int rr, r = fbuf_puts(f, "\x1bP0;");
if(r < 0){
return -1;
}
if((rr = fbuf_putint(f, p2)) < 0){
return -1;
}
r += rr;
if((rr = fbuf_puts(f, ";0q\"1;1;")) < 0){
return -1;
}
r += rr;
if((rr = fbuf_putint(f, lenx)) < 0){
return -1;
}
r += rr;
if(fbuf_putc(f, ';') != 1){
return -1;
}
++r;
if((rr = fbuf_putint(f, leny)) < 0){
return -1;
}
r += rr;
return r;
}
// write a single color register. rc/gc/bc are on [0..100].
static inline int
write_sixel_creg(fbuf* f, int idx, int rc, int gc, int bc){
int rr, r = 0;
if(fbuf_putc(f, '#') != 1){
return -1;
}
++r;
if((rr = fbuf_putint(f, idx)) < 0){
return -1;
}
r += rr;
if((rr = fbuf_puts(f, ";2;")) < 0){
return -1;
}
r += rr;
if((rr = fbuf_putint(f, rc)) < 0){
return -1;
}
r += rr;
if(fbuf_putc(f, ';') != 1){
return -1;
}
++r;
if((rr = fbuf_putint(f, gc)) < 0){
return -1;
}
r += rr;
if(fbuf_putc(f, ';') != 1){
return -1;
}
++r;
if((rr = fbuf_putint(f, bc)) < 0){
return -1;
}
r += rr;
return r;
}
// write the escape which opens a Sixel, plus the palette table. returns the
// number of bytes written, so that this header can be directly copied in
// future reencodings. |leny| and |lenx| are output pixel geometry.
// returns the number of bytes written, so it can be stored at *parse_start.
static int
write_sixel_header(qstate* qs, fbuf* f, int leny){
if(leny % 6){
return -1;
}
// Set Raster Attributes - pan/pad=1 (pixel aspect ratio), Ph=qs->lenx, Pv=leny
int r = write_sixel_intro(f, qs->smap->p2, leny, qs->lenx);
if(r < 0){
return -1;
}
for(int i = 0 ; i < qs->smap->colors ; ++i){
const unsigned char* rgb = qs->table + i * RGBSIZE;
//fprintf(fp, "#%d;2;%u;%u;%u", i, rgb[0], rgb[1], rgb[2]);
int rr = write_sixel_creg(f, i, rgb[0], rgb[1], rgb[2]);
if(rr < 0){
return -1;
}
r += rr;
}
return r;
}
static int
write_sixel_payload(fbuf* f, const sixelmap* map){
for(int j = 0 ; j < map->sixelbands ; ++j){
int needclosure = 0;
const sixelband* band = &map->bands[j];
for(int i = 0 ; i < band->size ; ++i){
if(band->vecs[i]){
if(needclosure){
if(fbuf_putc(f, '$') != 1){ // end previous one
return -1;
}
}else{
needclosure = 1;
}
if(fbuf_putc(f, '#') != 1){
return -1;
}
if(fbuf_putint(f, i) < 0){
return -1;
}
if(fbuf_puts(f, band->vecs[i]) < 0){
return -1;
}
}
}
if(fbuf_putc(f, '-') != 1){
return -1;
}
}
if(fbuf_puts(f, "\e\\") < 0){
return -1;
}
return 0;
}
// once per render cycle (if needed), make the actual payload match the TAM. we
// don't do these one at a time due to the complex (expensive) process involved
// in regenerating a sixel (we can't easily do it in-place). anything newly
// ANNIHILATED (state is ANNIHILATED, but no auxvec present) is dropped from
// the payload, and an auxvec is generated. anything newly restored (state is
// OPAQUE_SIXEL or MIXED_SIXEL, but an auxvec is present) is restored to the
// payload, and the auxvec is freed. none of this takes effect until the sixel
// is redrawn, and annihilated sprixcells still require a glyph to be emitted.
static inline int
sixel_reblit(sprixel* s){
fbuf_chop(&s->glyph, s->parse_start);
if(write_sixel_payload(&s->glyph, s->smap) < 0){
return -1;
}
change_p2(s->glyph.buf, s->smap->p2);
return 0;
}
// write out the sixel header after having quantized the palette.
static inline int
sixel_blit_inner(qstate* qs, sixelmap* smap, const blitterargs* bargs, tament* tam){
fbuf f;
if(fbuf_init(&f)){
return -1;
}
sprixel* s = bargs->u.pixel.spx;
const int cellpxy = bargs->u.pixel.cellpxy;
const int cellpxx = bargs->u.pixel.cellpxx;
int outy = qs->leny;
if(outy % 6){
outy += 6 - (qs->leny % 6);
smap->p2 = SIXEL_P2_TRANS;
}
int parse_start = write_sixel_header(qs, &f, outy);
if(parse_start < 0){
fbuf_free(&f);
return -1;
}
// we don't write out the payload yet -- set wipes_outstanding high, and
// it'll be emitted via sixel_reblit(), taking into account any wipes that
// occurred before it was displayed. otherwise, such a wipe would require
// two emissions, one of which would be thrown away.
scrub_tam_boundaries(tam, outy, qs->lenx, cellpxy, cellpxx);
// take ownership of buf on success
if(plane_blit_sixel(s, &f, outy, qs->lenx, parse_start, tam, SPRIXEL_INVALIDATED) < 0){
fbuf_free(&f);
return -1;
}
s->smap = smap;
return 1;
}
// |leny| and |lenx| are the scaled output geometry. we take |leny| up to the
// nearest multiple of six greater than or equal to |leny|.
int sixel_blit(ncplane* n, int linesize, const void* data, int leny, int lenx,
const blitterargs* bargs){
if(bargs->u.pixel.colorregs >= TRANS_PALETTE_ENTRY){
logerror("palette too large %d", bargs->u.pixel.colorregs);
return -1;
}
sixelmap* smap = sixelmap_create(leny - bargs->begy);
if(smap == NULL){
return -1;
}
assert(n->tam);
qstate* qs;
if((qs = alloc_qstate(bargs->u.pixel.colorregs)) == NULL){
logerror("couldn't allocate qstate");
sixelmap_free(smap);
return -1;
}
qs->bargs = bargs;
qs->data = data;
qs->linesize = linesize;
qs->smap = smap;
qs->leny = leny;
qs->lenx = lenx;
sixel_engine* sengine = ncplane_pile(n) ? ncplane_notcurses(n)->tcache.sixelengine : NULL;
if(extract_color_table(sengine, qs)){
free(bargs->u.pixel.spx->needs_refresh);
bargs->u.pixel.spx->needs_refresh = NULL;
sixelmap_free(smap);
free_qstate(qs);
return -1;
}
// takes ownership of sixelmap on success
int r = sixel_blit_inner(qs, smap, bargs, n->tam);
free_qstate(qs);
if(r < 0){
sixelmap_free(smap);
// FIXME free refresh table?
}
scrub_color_table(bargs->u.pixel.spx);
// we haven't actually emitted the body of the sixel yet. instead, we'll emit
// it at sixel_redraw(), thus avoiding a double emission in the case of wipes
// taking place before it's visible.
bargs->u.pixel.spx->wipes_outstanding = 1;
return r;
}
// to destroy a sixel, we damage all cells underneath it. we might not have
// to, though, if we've got a new sixel ready to go where the old sixel was
// (though we'll still need to if the new sprixcell not opaque, and the
// old and new sprixcell are different in any transparent pixel).
int sixel_scrub(const ncpile* p, sprixel* s){
loginfo("%d state %d at %d/%d (%d/%d)", s->id, s->invalidated, s->movedfromy, s->movedfromx, s->dimy, s->dimx);
int starty = s->movedfromy;
int startx = s->movedfromx;
for(int yy = starty ; yy < starty + (int)s->dimy && yy < (int)p->dimy ; ++yy){
for(int xx = startx ; xx < startx + (int)s->dimx && xx < (int)p->dimx ; ++xx){
int ridx = yy * p->dimx + xx;
struct crender *r = &p->crender[ridx];
if(!s->n){
// need this to damage cells underneath a sprixel we're removing
r->s.damaged = 1;
continue;
}
sprixel* trues = r->sprixel ? r->sprixel : s;
if(yy >= (int)trues->n->leny || yy - trues->n->absy < 0){
r->s.damaged = 1;
continue;
}
if(xx >= (int)trues->n->lenx || xx - trues->n->absx < 0){
r->s.damaged = 1;
continue;
}
sprixcell_e state = sprixel_state(trues, yy, xx);
//fprintf(stderr, "CHECKING %d/%d state: %d %d/%d\n", yy - s->movedfromy - s->n->absy, xx - s->movedfromx - s->n->absx, state, yy, xx);
if(state == SPRIXCELL_TRANSPARENT || state == SPRIXCELL_MIXED_SIXEL){
r->s.damaged = 1;
}else if(s->invalidated == SPRIXEL_MOVED){
// ideally, we wouldn't damage our annihilated sprixcells, but if
// we're being annihilated only during this cycle, we need to go
// ahead and damage it.
r->s.damaged = 1;
}
}
}
return 1;
}
// returns the number of bytes written
int sixel_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
int yoff, int xoff){
(void)ti;
// if we've wiped or rebuilt any cells, effect those changes now, or else
// we'll get flicker when we move to the new location.
if(s->wipes_outstanding){
if(sixel_reblit(s)){
return -1;
}
s->wipes_outstanding = false;
}
if(p){
const int targy = s->n->absy + yoff;
const int targx = s->n->absx + xoff;
if(goto_location(p->nc, f, targy, targx, NULL)){
return -1;
}
if(s->invalidated == SPRIXEL_MOVED){
for(int yy = s->movedfromy ; yy < s->movedfromy + (int)s->dimy && yy < (int)p->dimy ; ++yy){
if(yy < 0){
continue;
}
for(int xx = s->movedfromx ; xx < s->movedfromx + (int)s->dimx && xx < (int)p->dimx ; ++xx){
if(xx < 0){
continue;
}
struct crender *r = &p->crender[yy * p->dimx + xx];
if(!r->sprixel || sprixel_state(r->sprixel, yy, xx) != SPRIXCELL_OPAQUE_SIXEL){
r->s.damaged = 1;
}
}
}
}
}
if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){
return -1;
}
s->invalidated = SPRIXEL_QUIESCENT;
return s->glyph.used;
}
// a quantization worker.
static void *
sixel_worker(void* v){
work_queue* wq = v;
sixel_engine *sengine = wq->sengine;
qstate* qs = NULL;
unsigned bufpos = 0; // index into worker queue
do{
pthread_mutex_lock(&sengine->lock);
while(wq->used == 0 && !sengine->done){
pthread_cond_wait(&sengine->cond, &sengine->lock);
}
if(!sengine->done){
qs = wq->qstates[bufpos];
}else{
qs = NULL;
}
pthread_mutex_unlock(&sengine->lock);
if(qs == NULL){
break;
}
bandworker(qs);
bool sendsignal = false;
pthread_mutex_lock(&sengine->lock);
--wq->used;
if(--qs->refcount == 0){
sendsignal = true;
}
pthread_mutex_unlock(&sengine->lock);
if(sendsignal){
pthread_cond_broadcast(&sengine->cond);
}
if(++bufpos == WORKERDEPTH){
bufpos = 0;
}
}while(1);
return NULL;
}
static int
sixel_init_core(tinfo* ti, const char* initstr, int fd){
if((ti->sixelengine = malloc(sizeof(sixel_engine))) == NULL){
return -1;
}
sixel_engine* sengine = ti->sixelengine;
pthread_mutex_init(&sengine->lock, NULL);
pthread_cond_init(&sengine->cond, NULL);
sengine->done = false;
const int workers_wanted = sizeof(sengine->tids) / sizeof(*sengine->tids);
for(int w = 0 ; w < workers_wanted ; ++w){
sengine->queues[w].sengine = sengine;
sengine->queues[w].writeto = 0;
sengine->queues[w].used = 0;
if(pthread_create(&sengine->tids[w], NULL, sixel_worker, &sengine->queues[w])){
logerror("couldn't spin up sixel worker %d/%d", w, workers_wanted);
// FIXME kill any created workers
return -1;
}
}
return tty_emit(initstr, fd);
}
// private mode 80 (DECSDM) manages "Sixel Scrolling Mode" vs "Sixel Display
// Mode". when 80 is enabled (i.e. DECSDM mode), images are displayed at the
// upper left, and clipped to the window. we don't want either of those things
// to happen, so we explicitly disable DECSDM.
// private mode 8452 places the cursor at the end of a sixel when it's
// emitted. we don't need this for rendered mode, but we do want it for
// direct mode. it causes us no problems, so always set it.
int sixel_init_forcesdm(tinfo* ti, int fd){
return sixel_init_core(ti, "\e[?80l\e[?8452h", fd);
}
int sixel_init_inverted(tinfo* ti, int fd){
// some terminals, at some versions, invert the sense of DECSDM. for those,
// we must use 80h rather than the correct 80l. this grows out of a
// misunderstanding in XTerm through patchlevel 368, which was widely
// copied into other terminals.
return sixel_init_core(ti, "\e[?80h\e[?8452h", fd);
}
// if we aren't sure of the semantics of the terminal we're speaking with,
// don't touch DECSDM at all. it's almost certainly set up the way we want.
int sixel_init(tinfo* ti, int fd){
return sixel_init_core(ti, "\e[?8452h", fd);
}
// restore the |yoff|th bit of the sixel at |xoff| for the specified vec
// FIXME this is a very dopey implementation yuck, use RLE at least
static int
restore_vec(sixelband* b, int color, int bit, int xoff, int dimx){
if(color >= b->size){
logpanic("illegal color %d >= %d", color, b->size);
return -1;
}
char* v = NULL;
const char* vec = b->vecs[color]; // might be NULL
if(vec == NULL){ // write this sixel, and we're done
struct band_extender bes = {
.rle = 1,
.rep = bit,
};
if((v = sixelband_extend(v, &bes, dimx, xoff)) == NULL){
return -1;
}
}else{
int rle = 0; // the repetition number for this element
int x = 0;
int voff = 0;
if((v = malloc(dimx + 1)) == NULL){
return -1;
}
while(*vec){
if(isdigit(*vec)){
rle *= 10;
rle += (*vec - '0');
}else if(*vec == '!'){
rle = 0;
}else{
if(rle == 0){
rle = 1;
}
char rep = *vec;
//fprintf(stderr, "X/RLE/ENDX: %d %d %d\n", x, rle, endx);
if(x + rle <= xoff){ // not wiped material; reproduce as-is
write_rle(v, &voff, rle, rep);
x += rle;
}else if(x > xoff){
write_rle(v, &voff, rle, rep);
x += rle;
}else{
if(x < xoff){
write_rle(v, &voff, xoff - x, rep);
rle -= xoff - x;
x = xoff;
}
write_rle(v, &voff, 1, ((rep - 63) | bit) + 63);
--rle;
++x;
if(rle){
write_rle(v, &voff, rle, rep);
x += rle;
}
}
rle = 0;
}
++vec;
if(x > xoff){
strcpy(v + voff, vec); // there is always room
break;
}
}
}
free(b->vecs[color]);
b->vecs[color] = v;
//fprintf(stderr, "SET NEW VEC (%zu) [%s]\n", strlen(v), v);
return 0;
}
// rebuild the portion of some cell which is within this band, having stored
// the pixels into the auxvec when the cell was wiped (and updated them if we
// loaded another frame). we go through the auxvec to the right and down,
// within the area covered by our band. if the entry is transparent, do
// nothing. otherwise, it is some color; collect other instances of the color,
// marking them transparent as we do so, and update that color's band. in
// the worst case (all pixels different colors), this will be p^2 =\ FIXME.
//
// returns the number of source-transparent pixels (i.e. pixels which weren't
// restored), which will be used to update the TAM state.
static inline int
restore_band(sixelmap* smap, int band, int startx, int endx,
int starty, int endy, int dimx, int cellpxy, int cellpxx,
uint8_t* auxvec){
int restored = 0;
const int sy = band * 6 < starty ? starty - band * 6 : 0;
const int ey = (band + 1) * 6 > endy ? 6 - ((band + 1) * 6 - endy) : 6;
const int width = endx - startx;
const int height = ey - sy;
const int totalpixels = width * height;
sixelband* b = &smap->bands[band];
//fprintf(stderr, "RESTORING band %d (%d->%d (%d->%d), %d->%d) %d pixels\n", band, sy, ey, starty, endy, startx, endx, totalpixels);
int yoff = ((band * 6) + sy - starty) % cellpxy; // we start off on this row of the auxvec
int xoff = startx % cellpxx;
for(int dy = sy ; dy < ey ; ++dy, ++yoff){
const int idx = (yoff * cellpxx + xoff) * AUXVECELEMSIZE;
const int bit = 1 << dy;
//fprintf(stderr, " looking at bandline %d (auxvec row %d idx %d, dy %d)\n", dy, yoff, idx, dy);
for(int dx = 0 ; startx + dx < endx ; ++dx){
uint16_t color;
memcpy(&color, &auxvec[idx + dx * AUXVECELEMSIZE], AUXVECELEMSIZE);
//fprintf(stderr, " idx %d (dx %d x %d): %hu\n", idx, dx, dx + startx, color);
if(color != TRANS_PALETTE_ENTRY){
restore_vec(b, color, bit, startx + dx, dimx);
++restored;
}
}
}
(void)smap;
return totalpixels - restored;
}
// only called for cells in SPRIXCELL_ANNIHILATED[_TRANS]. just post to
// wipes_outstanding, so the Sixel gets regenerated the next render cycle,
// just like wiping. this is necessary due to the complex nature of
// modifying a Sixel -- we want to do them all in one batch.
int sixel_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
//fprintf(stderr, "REBUILDING %d/%d\n", ycell, xcell);
if(auxvec == NULL){
return -1;
}
const int cellpxy = ncplane_pile(s->n)->cellpxy;
const int cellpxx = ncplane_pile(s->n)->cellpxx;
sixelmap* smap = s->smap;
const int startx = xcell * cellpxx;
const int starty = ycell * cellpxy;
int endx = ((xcell + 1) * cellpxx);
if(endx >= s->pixx){
endx = s->pixx;
}
int endy = ((ycell + 1) * cellpxy);
if(endy >= s->pixy){
endy = s->pixy;
}
const int startband = starty / 6;
const int endband = (endy - 1) / 6;
//fprintf(stderr, "%d/%d start: %d/%d end: %d/%d bands: %d-%d\n", ycell, xcell, starty, startx, endy, endx, starty / 6, endy / 6);
// walk through each color, and wipe the necessary sixels from each band
int w = 0;
for(int b = startband ; b <= endband ; ++b){
w += restore_band(smap, b, startx, endx, starty, endy, s->pixx,
cellpxy, cellpxx, auxvec);
}
s->wipes_outstanding = true;
sprixcell_e newstate;
if(w == cellpxx * cellpxy){
newstate = SPRIXCELL_TRANSPARENT;
}else if(w){
newstate = SPRIXCELL_MIXED_SIXEL;
}else{
newstate = SPRIXCELL_OPAQUE_SIXEL;
}
s->n->tam[s->dimx * ycell + xcell].state = newstate;
return 1;
}
void sixel_cleanup(tinfo* ti){
sixel_engine* sengine = ti->sixelengine;
const unsigned tids = POPULATION;
pthread_mutex_lock(&sengine->lock);
sengine->done = 1;
pthread_mutex_unlock(&sengine->lock);
pthread_cond_broadcast(&sengine->cond);
loginfo("joining %u sixel thread%s", tids, tids == 1 ? "" : "s");
for(unsigned t = 0 ; t < tids ; ++t){
pthread_join(sengine->tids[t], NULL);
}
pthread_mutex_destroy(&sengine->lock);
pthread_cond_destroy(&sengine->cond);
free(sengine);
loginfo("reaped sixel engine");
ti->sixelengine = NULL;
// no way to know what the state was before; we ought use XTSAVE/XTRESTORE
}
// create an auxiliary vector suitable for a Sixel sprixcell, and zero it out.
// there are two bytes per pixel in the cell: a palette index of up to 65534,
// or 65535 to indicate transparency.
uint8_t* sixel_trans_auxvec(const ncpile* p){
const size_t slen = AUXVECELEMSIZE * p->cellpxy * p->cellpxx;
uint8_t* a = malloc(slen);
if(a){
memset(a, 0xff, slen);
}
return a;
}