From 9b81de3789d203c88adbde9e461290859d211b0a Mon Sep 17 00:00:00 2001 From: Nick Black Date: Sun, 22 Dec 2019 08:08:53 -0500 Subject: [PATCH] O(1) passive damage detection #189 (#197) * notcurses: set up lastframe #189 * render: o(1) take no prisoners damage detection #189 --- doc/man/man1/notcurses-demo.1 | 6 +- src/demo/demo.c | 30 ++++- src/demo/demo.h | 2 +- src/demo/{bleachworm.c => witherworm.c} | 155 ++++++++++++--------- src/lib/egcpool.h | 6 + src/lib/fade.c | 2 - src/lib/internal.h | 52 +++---- src/lib/libav.c | 1 - src/lib/notcurses.c | 108 ++------------- src/lib/render.c | 172 +++++++++++++++++------- 10 files changed, 289 insertions(+), 245 deletions(-) rename src/demo/{bleachworm.c => witherworm.c} (92%) diff --git a/doc/man/man1/notcurses-demo.1 b/doc/man/man1/notcurses-demo.1 index 04821d856..304b0eced 100644 --- a/doc/man/man1/notcurses-demo.1 +++ b/doc/man/man1/notcurses-demo.1 @@ -6,6 +6,7 @@ notcurses-demo \- Show off some notcurses features [ \fB\-p \fIpath \fR] [ \fB\-d \fIdelaymult \fR] [ \fB\-k \fR] +[ \fB\-c \fR] [ \fB\-h / \fB\-\-help \fR] .IR demospec .SH OPTIONS @@ -20,6 +21,9 @@ Apply a (floating-point) multiplier to the standard delay of 1s. Inhibit use of the alternate screen. Necessary if you want the output left on your terminal after the program exits. .TP +.BR \-c +Do not attempt to seed the PRNG. This is useful when benchmarking. +.TP .BR \-h ", " \-\-help Print a usage message, and exit with success. .TP @@ -45,7 +49,7 @@ contains a set of text-based demonstrations of capabilities from the notcurses l .P (s)liders—a missing-piece puzzle made up of colorful blocks .P -(b)leachworm—a great Nothing slowly robs the world of color +(w)hiteworm—a great Nothing slowly robs the world of color .P (v)iew—images and a video are rendered as text .P diff --git a/src/demo/demo.c b/src/demo/demo.c index a3b297802..ef59df680 100644 --- a/src/demo/demo.c +++ b/src/demo/demo.c @@ -10,6 +10,10 @@ #include #include "demo.h" +// ansi terminal definition-4-life +static const int MIN_SUPPORTED_ROWS = 25; +static const int MIN_SUPPORTED_COLS = 80; + static const char DEFAULT_DEMO[] = "iemlubgswvpo"; static char datadir[PATH_MAX] = "/usr/share/notcurses"; // FIXME @@ -46,11 +50,12 @@ struct timespec demodelay = { static void usage(const char* exe, int status){ FILE* out = status == EXIT_SUCCESS ? stdout : stderr; - fprintf(out, "usage: %s [ -h ] [ -k ] [ -d mult ] [ -f renderfile ] demospec\n", exe); + fprintf(out, "usage: %s [ -h ] [ -k ] [ -d mult ] [ -c ] [ -f renderfile ] demospec\n", exe); fprintf(out, " -h: this message\n"); fprintf(out, " -k: keep screen; do not switch to alternate\n"); fprintf(out, " -d: delay multiplier (float)\n"); fprintf(out, " -f: render to file in addition to stdout\n"); + fprintf(out, " -c: constant PRNG seed, useful for benchmarking\n"); fprintf(out, "all demos are run if no specification is provided\n"); fprintf(out, " b: run box\n"); fprintf(out, " e: run eagles\n"); @@ -63,7 +68,7 @@ usage(const char* exe, int status){ fprintf(out, " s: run shuffle\n"); fprintf(out, " u: run uniblock\n"); fprintf(out, " v: run view\n"); - fprintf(out, " w: run bleachworm\n"); + fprintf(out, " w: run witherworm\n"); exit(status); } @@ -183,7 +188,7 @@ ext_demos(struct notcurses* nc, const char* demos){ case 'l': ret = luigi_demo(nc); break; case 'v': ret = view_demo(nc); break; case 'e': ret = eagle_demo(nc); break; - case 'w': ret = bleachworm_demo(nc); break; + case 'w': ret = witherworm_demo(nc); break; case 'p': ret = panelreel_demo(nc); break; default: fprintf(stderr, "Unknown demo specification: %c\n", *demos); @@ -208,13 +213,17 @@ ext_demos(struct notcurses* nc, const char* demos){ // if it's NULL, there were valid options, but no spec. static const char* handle_opts(int argc, char** argv, notcurses_options* opts){ + bool constant_seed = false; int c; memset(opts, 0, sizeof(*opts)); - while((c = getopt(argc, argv, "hkd:f:p:")) != EOF){ + while((c = getopt(argc, argv, "hckd:f:p:")) != EOF){ switch(c){ case 'h': usage(*argv, EXIT_SUCCESS); break; + case 'c': + constant_seed = true; + break; case 'k': opts->inhibit_alternate_screen = true; break; @@ -244,6 +253,9 @@ handle_opts(int argc, char** argv, notcurses_options* opts){ usage(*argv, EXIT_FAILURE); } } + if(!constant_seed){ + srand(time(NULL)); // a classic blunder lol + } const char* demos = argv[optind]; return demos; } @@ -252,7 +264,6 @@ handle_opts(int argc, char** argv, notcurses_options* opts){ int main(int argc, char** argv){ struct notcurses* nc; notcurses_options nopts; - struct ncplane* ncp; if(!setlocale(LC_ALL, "")){ fprintf(stderr, "Couldn't set locale based on user preferences\n"); return EXIT_FAILURE; @@ -267,8 +278,9 @@ int main(int argc, char** argv){ if((nc = notcurses_init(&nopts, stdout)) == NULL){ return EXIT_FAILURE; } - if((ncp = notcurses_stdplane(nc)) == NULL){ - fprintf(stderr, "Couldn't get standard plane\n"); + int dimx, dimy; + notcurses_term_dim_yx(nc, &dimy, &dimx); + if(dimy < MIN_SUPPORTED_ROWS || dimx < MIN_SUPPORTED_COLS){ goto err; } // no one cares about the leaderscreen. 1s max. @@ -306,6 +318,10 @@ int main(int argc, char** argv){ return EXIT_SUCCESS; err: + notcurses_term_dim_yx(nc, &dimy, &dimx); notcurses_stop(nc); + if(dimy < MIN_SUPPORTED_ROWS || dimx < MIN_SUPPORTED_COLS){ + fprintf(stderr, "At least an 80x25 terminal is required (current: %dx%d)\n", dimx, dimy); + } return EXIT_FAILURE; } diff --git a/src/demo/demo.h b/src/demo/demo.h index e68f04d23..be778b762 100644 --- a/src/demo/demo.h +++ b/src/demo/demo.h @@ -16,7 +16,7 @@ extern struct timespec demodelay; char* find_data(const char* datum); int unicodeblocks_demo(struct notcurses* nc); -int bleachworm_demo(struct notcurses* nc); +int witherworm_demo(struct notcurses* nc); int box_demo(struct notcurses* nc); int maxcolor_demo(struct notcurses* nc); int grid_demo(struct notcurses* nc); diff --git a/src/demo/bleachworm.c b/src/demo/witherworm.c similarity index 92% rename from src/demo/bleachworm.c rename to src/demo/witherworm.c index d1c3ca24f..aa680e172 100644 --- a/src/demo/bleachworm.c +++ b/src/demo/witherworm.c @@ -160,81 +160,112 @@ lightup_surrounding_cells(struct ncplane* n, const cell* cells, int y, int x){ return 0; } +typedef struct snake { + cell lightup[13]; + int x, y; + uint64_t channels; + int prevx, prevy; +} snake; + +static void +init_snake(snake* s, int dimy, int dimx){ + for(size_t i = 0 ; i < sizeof(s->lightup) / sizeof(*s->lightup) ; ++i){ + cell_init(&s->lightup[i]); + } + // start it in the lower center of the screen + s->x = (random() % (dimx / 2)) + (dimx / 4); + s->y = (random() % (dimy * 2 / 3)) + (dimy / 3); + s->channels = 0; + channels_set_fg_rgb(&s->channels, 255, 255, 255); + channels_set_bg_rgb(&s->channels, 20, 20, 20); + s->prevx = 0; + s->prevy = 0; +} + +static int +snakey_top(struct notcurses* nc, snake* s){ + struct ncplane* n = notcurses_stdplane(nc); + get_surrounding_cells(n, s->lightup, s->y, s->x); + ncplane_cursor_move_yx(n, s->y, s->x); + if(lightup_surrounding_cells(n, s->lightup, s->y, s->x)){ + return -1; + } + return 0; +} + +static int +snakey(struct notcurses* nc, snake* s, int dimy, int dimx, const struct timespec* iterdelay){ + struct ncplane* n = notcurses_stdplane(nc); + int oldy, oldx; + clock_nanosleep(CLOCK_MONOTONIC, 0, iterdelay, NULL); + cell c = CELL_TRIVIAL_INITIALIZER; + do{ // force a move + oldy = s->y; + oldx = s->x; + // FIXME he ought be weighted to avoid light; he's a snake after all + int direction = random() % 4; + switch(direction){ + case 0: --s->y; break; + case 1: ++s->x; break; + case 2: ++s->y; break; + case 3: --s->x; break; + } + // keep him away from the sides due to width irregularities + if(s->x < (dimx / 4)){ + s->x = dimx / 4; + }else if(s->x >= dimx * 3 / 4){ + s->x = dimx * 3 / 4; + } + if(s->y < 0){ + s->y = 0; + }else if(s->y >= dimy){ + s->y = dimy - 1; + } + ncplane_cursor_move_yx(n, s->y, s->x); + ncplane_at_cursor(n, &c); + // don't allow the snake into the summary zone (test for walls) + if(wall_p(n, &c)){ + s->x = oldx; + s->y = oldy; + } + }while((oldx == s->x && oldy == s->y) || (s->x == s->prevx && s->y == s->prevy)); + s->prevy = oldy; + s->prevx = oldx; + cell_release(n, &c); + return 0; +} + // each snake wanders around aimlessly, prohibited from entering the summary // section. it ought light up the cells around it; to do this, we keep an array // of 13 cells with the original colors, which we tune up for the duration of // our colocality (unless they're summary area walls). static void * snake_thread(void* vnc){ + const int snakecount = 3; // FIXME base count off area struct notcurses* nc = vnc; struct ncplane* n = notcurses_stdplane(nc); - cell lightup[13]; - size_t i; - for(i = 0 ; i < sizeof(lightup) / sizeof(*lightup) ; ++i){ - cell_init(&lightup[i]); - } int dimy, dimx; ncplane_dim_yx(n, &dimy, &dimx); - int x, y; - // start it in the lower center of the screen - x = (random() % (dimx / 2)) + (dimx / 4); - y = (random() % (dimy / 2)) + (dimy / 2); - cell head = CELL_TRIVIAL_INITIALIZER; - uint64_t channels = 0; - channels_set_fg_rgb(&channels, 255, 255, 255); - channels_set_bg_rgb(&channels, 20, 20, 20); - cell_prime(n, &head, "א", 0, channels); - cell c = CELL_TRIVIAL_INITIALIZER; + snake snakes[snakecount]; + for(int s = 0 ; s < snakecount ; ++s){ + init_snake(&snakes[s], dimy, dimx); + } struct timespec iterdelay = { .tv_sec = 0, .tv_nsec = 1000000000ul / 20, }; - int prevx = 0, prevy = 0; while(true){ pthread_testcancel(); - get_surrounding_cells(n, lightup, y, x); - ncplane_cursor_move_yx(n, y, x); - ncplane_at_cursor(n, &c); - if(lightup_surrounding_cells(n, lightup, y, x)){ + for(int s = 0 ; s < snakecount ; ++s){ + if(snakey_top(nc, &snakes[s])){ + return NULL; + } + } + if(notcurses_render(nc)){ return NULL; } - notcurses_render(nc); - int oldy, oldx; - clock_nanosleep(CLOCK_MONOTONIC, 0, &iterdelay, NULL); - do{ // force a move - oldy = y; - oldx = x; - // FIXME he ought be weighted to avoid light; he's a snake after all - int direction = random() % 4; - switch(direction){ - case 0: --y; break; - case 1: ++x; break; - case 2: ++y; break; - case 3: --x; break; + for(int s = 0 ; s < snakecount ; ++s){ + if(snakey(nc, &snakes[s], dimy, dimx, &iterdelay)){ + return NULL; } - // keep him away from the sides due to width irregularities - if(x < (dimx / 4)){ - x = dimx / 4; - }else if(x >= dimx * 3 / 4){ - x = dimx * 3 / 4; - } - if(y < 0){ - y = 0; - }else if(y >= dimy){ - y = dimy - 1; - } - ncplane_cursor_move_yx(n, y, x); - ncplane_at_cursor(n, &c); - // don't allow the snake into the summary zone (test for walls) - if(wall_p(n, &c)){ - x = oldx; - y = oldy; - } - }while((oldx == x && oldy == y) || (x == prevx && y == prevy)); - prevy = oldy; - prevx = oldx; - } - cell_release(n, &head); // FIXME won't be released when cancelled - cell_release(n, &c); // FIXME won't be released when cancelled - for(i = 0 ; i < sizeof(lightup) / sizeof(*lightup) ; ++i){ - cell_release(n, &lightup[i]); + } } return NULL; } @@ -306,7 +337,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, } // Much of this text comes from http://kermitproject.org/utf8.html -int bleachworm_demo(struct notcurses* nc){ +int witherworm_demo(struct notcurses* nc){ static const char* strs[] = { "Война и мир", "Бра́тья Карама́зовы", @@ -537,8 +568,8 @@ int bleachworm_demo(struct notcurses* nc){ NULL }; const char** s; - const int steps[] = { 0x100, 0x100, 0x40000, 0x10001, }; - const int starts[] = { 0x004000, 0x000040, 0x010101, 0x400040, }; + const int steps[] = { 0x10040, 0x100, 0x100, 0x10001, }; + const int starts[] = { 0x10101, 0x004000, 0x000040, 0x400040, }; struct ncplane* n = notcurses_stdplane(nc); size_t i; diff --git a/src/lib/egcpool.h b/src/lib/egcpool.h index 22281a3fd..5e97235c6 100644 --- a/src/lib/egcpool.h +++ b/src/lib/egcpool.h @@ -230,6 +230,12 @@ egcpool_dump(egcpool* pool){ pool->poolused = 0; } +static inline const char* +egcpool_extended_gcluster(const egcpool* pool, const cell* c){ + uint32_t idx = cell_egc_idx(c); + return pool->pool + idx; +} + #ifdef __cplusplus } #endif diff --git a/src/lib/fade.c b/src/lib/fade.c index 97a6011f0..2ecc4a65d 100644 --- a/src/lib/fade.c +++ b/src/lib/fade.c @@ -134,7 +134,6 @@ int ncplane_fadein(ncplane* n, const struct timespec* ts){ } } } - ncplane_updamage(n); notcurses_render(n->nc); uint64_t nextwake = (iter + 1) * nanosecs_step + startns; struct timespec sleepspec; @@ -204,7 +203,6 @@ int ncplane_fadeout(ncplane* n, const struct timespec* ts){ } } } - ncplane_updamage(n); cell* c = &n->defcell; if(!cell_fg_default_p(c)){ channels_get_fg_rgb(pp.channels[pp.cols * y], &r, &g, &b); diff --git a/src/lib/internal.h b/src/lib/internal.h index c67611377..74e9755f7 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -51,7 +51,6 @@ typedef struct ncplane { uint32_t attrword; // same deal as in a cell void* userptr; // slot for the user to stick some opaque pointer cell defcell; // cell written anywhere that fb[i].gcluster == 0 - unsigned char* damage;// damage map, one per row struct notcurses* nc; // notcurses object of which we are a part } ncplane; @@ -80,20 +79,32 @@ typedef struct ncvisual { typedef struct notcurses { pthread_mutex_t lock; - int ttyfd; // file descriptor for controlling tty, from opts->ttyfp - FILE* ttyfp; // FILE* for controlling tty, from opts->ttyfp - FILE* ttyinfp; // FILE* for processing input - unsigned char* damage; // damage map (row granularity) + ncplane* top; // the contents of our topmost plane (initially entire screen) + ncplane* stdscr;// aliases some plane from the z-buffer, covers screen + + // we keep a copy of the last rendered frame. this facilitates O(1) + // notcurses_at_yx() and O(1) damage detection (at the cost of some memory). + cell* lastframe;// last rendered framebuffer, NULL until first render + int lfdimx; // dimensions of lastframe, unchanged by screen resize + int lfdimy; // lfdimx/lfdimy are 0 until first render + egcpool pool; // duplicate EGCs into this pool + + // we assemble the encoded output in a POSIX memstream, and keep it around + // between uses. this could be a problem if it ever tremendously spiked, but + // that's a highly unlikely situation. char* mstream; // buffer for rendering memstream, see open_memstream(3) FILE* mstreamfp;// FILE* for rendering memstream size_t mstrsize;// size of rendering memstream - int colors; // number of colors usable for this screen + ncstats stats; // some statistics across the lifetime of the notcurses ctx ncstats stashstats; // cumulative stats, unaffected by notcurses_reset_stats() + // We verify that some terminfo capabilities exist. These needn't be checked // before further use; just use tiparm() directly. + int colors; // number of colors terminfo reported usable for this screen char* cup; // move cursor - bool RGBflag; // terminfo-reported "RGB" flag for 24bpc directcolor + char* cuf; // move n cells right + char* cuf1; // move 1 cell right char* civis; // hide cursor // These might be NULL, and we can more or less work without them. Check! char* clearscr; // erase screen and home cursor @@ -117,12 +128,15 @@ typedef struct notcurses { char* italoff; // CELL_STYLE_ITALIC (disable) char* smkx; // enter keypad transmit mode (keypad_xmit) char* rmkx; // leave keypad transmit mode (keypad_local) - struct termios tpreserved; // terminal state upon entry - bool suppress_banner; // from notcurses_options + bool RGBflag; // terminfo-reported "RGB" flag for 24bpc directcolor bool CCCflag; // terminfo-reported "CCC" flag for palette set capability - ncplane* top; // the contents of our topmost plane (initially entire screen) - ncplane* stdscr;// aliases some plane from the z-buffer, covers screen + + int ttyfd; // file descriptor for controlling tty, from opts->ttyfp + FILE* ttyfp; // FILE* for controlling tty, from opts->ttyfp + FILE* ttyinfp; // FILE* for processing input FILE* renderfp; // debugging FILE* to which renderings are written + struct termios tpreserved; // terminal state upon entry + bool suppress_banner; // from notcurses_options unsigned char inputbuf[BUFSIZ]; // we keep a wee ringbuffer of input queued up for delivery. if // inputbuf_occupied == sizeof(inputbuf), there is no room. otherwise, data @@ -161,19 +175,6 @@ fbcellidx(const ncplane* n, int row, int col){ return row * n->lenx + col; } -// set all elements of a damage map true or false -static inline void -flash_damage_map(unsigned char* damage, int count, bool val){ - if(val){ - memset(damage, 0xff, sizeof(*damage) * count); - }else{ - memset(damage, 0, sizeof(*damage) * count); - } -} - -// mark all lines of the notcurses object touched by this plane as damaged -void ncplane_updamage(ncplane* n); - // For our first attempt, O(1) uniform conversion from 8-bit r/g/b down to // ~2.4-bit 6x6x6 cube + greyscale (assumed on entry; I know no way to // even semi-portably recover the palette) proceeds via: map each 8-bit to @@ -223,8 +224,7 @@ term_emit(const char* name __attribute__ ((unused)), const char* seq, static inline const char* extended_gcluster(const ncplane* n, const cell* c){ - uint32_t idx = cell_egc_idx(c); - return n->pool.pool + idx; + return egcpool_extended_gcluster(&n->pool, c); } #define NANOSECS_IN_SEC 1000000000 diff --git a/src/lib/libav.c b/src/lib/libav.c index 3b3ad8aa3..edd2ecafb 100644 --- a/src/lib/libav.c +++ b/src/lib/libav.c @@ -354,7 +354,6 @@ int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx) cell_release(ncv->ncp, &c); } } - flash_damage_map(ncv->ncp->damage + ncv->placey, y - ncv->placey, true); return 0; } diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index f4f90f8bc..3b570529e 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -262,9 +262,7 @@ term_verify_seq(char** gseq, const char* name){ static void free_plane(ncplane* p){ if(p){ - ncplane_updamage(p); egcpool_dump(&p->pool); - free(p->damage); free(p->fb); free(p); } @@ -287,13 +285,6 @@ ncplane_create(notcurses* nc, int rows, int cols, int yoff, int xoff){ return NULL; } memset(p->fb, 0, fbsize); - p->damage = malloc(sizeof(*p->damage) * rows); - if(p->damage == NULL){ - free(p->fb); - free(p); - return NULL; - } - flash_damage_map(p->damage, rows, false); p->userptr = NULL; p->leny = rows; p->lenx = cols; @@ -341,8 +332,7 @@ ncplane* notcurses_newplane(notcurses* nc, int rows, int cols, return n; } -// can be used on stdscr, unlike ncplane_resize() which prohibits it. sets all -// members of the plane's damage map to damaged. +// can be used on stdscr, unlike ncplane_resize() which prohibits it. static int ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, int keeplenx, int yoff, int xoff, int ylen, int xlen){ @@ -377,19 +367,6 @@ ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, if(fb == NULL){ return -1; } - // if we're the standard plane, the updamage can be charged at the end. it's - // unsafe to call now anyway, because if we shrank, the notcurses damage map - // has already been shrunk down - if(n != n->nc->stdscr){ - ncplane_updamage(n); // damage any lines we were on - } - unsigned char* tmpdamage; - if((tmpdamage = realloc(n->damage, sizeof(*n->damage) * ylen)) == NULL){ - free(fb); - return -1; - } - n->damage = tmpdamage; - flash_damage_map(n->damage, ylen, true); // update the cursor, if it would otherwise be off-plane if(n->y >= ylen){ n->y = ylen - 1; @@ -411,7 +388,6 @@ ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, n->lenx = xlen; n->leny = ylen; free(preserved); - ncplane_updamage(n); // damage any lines we're now on return 0; } // we currently have maxy rows of maxx cells each. we will be keeping rows @@ -448,7 +424,6 @@ ncplane_resize_internal(ncplane* n, int keepy, int keepx, int keepleny, n->lenx = xlen; n->leny = ylen; free(preserved); - ncplane_updamage(n); // damage any lines we're now on return 0; } @@ -489,19 +464,6 @@ int notcurses_resize(notcurses* n, int* rows, int* cols){ if(keepx > oldcols){ keepx = oldcols; } - unsigned char* tmpdamage; - if((tmpdamage = malloc(sizeof(*n->damage) * *rows)) == NULL){ - return -1; - } - if(oldcols < *cols){ // all are busted if rows got bigger - free(n->damage); - flash_damage_map(tmpdamage, *rows, true); - }else if(oldrows <= *rows){ // new rows are pre-busted, old are straight - memcpy(tmpdamage, n->damage, oldrows * sizeof(*tmpdamage)); - flash_damage_map(tmpdamage + oldrows, *rows - oldrows, true); - free(n->damage); - } - n->damage = tmpdamage; if(ncplane_resize_internal(n->stdscr, 0, 0, keepy, keepx, 0, 0, *rows, *cols)){ return -1; } @@ -593,6 +555,8 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){ term_verify_seq(&nc->clearscr, "clear"); term_verify_seq(&nc->cleareol, "el"); term_verify_seq(&nc->clearbol, "el1"); + term_verify_seq(&nc->cuf, "cuf"); // n non-destructive spaces + term_verify_seq(&nc->cuf1, "cuf1"); // non-destructive space if(prep_special_keys(nc)){ return -1; } @@ -725,6 +689,10 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ ret->ttyinfp = stdin; // FIXME ret->mstream = NULL; ret->mstrsize = 0; + ret->lastframe = NULL; + ret->lfdimy = 0; + ret->lfdimx = 0; + egcpool_init(&ret->pool); if(make_nonblocking(ret->ttyinfp)){ free(ret); return NULL; @@ -783,16 +751,10 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ free_plane(ret->top); goto err; } - if((ret->damage = malloc(sizeof(*ret->damage) * ret->stdscr->leny)) == NULL){ - free_plane(ret->top); - goto err; - } if((ret->mstreamfp = open_memstream(&ret->mstream, &ret->mstrsize)) == NULL){ - free(ret->damage); free_plane(ret->top); goto err; } - flash_damage_map(ret->damage, ret->stdscr->leny, false); // term_emit("clear", ret->clear, ret->ttyfp, false); ret->suppress_banner = opts->suppress_bannner; if(!opts->suppress_bannner){ @@ -856,10 +818,11 @@ int notcurses_stop(notcurses* nc){ nc->top = p->z; free_plane(p); } - free(nc->damage); if(nc->mstreamfp){ fclose(nc->mstreamfp); } + egcpool_dump(&nc->pool); + free(nc->lastframe); free(nc->mstream); input_free_esctrie(&nc->inputescapes); ret |= pthread_mutex_destroy(&nc->lock); @@ -955,7 +918,6 @@ int ncplane_set_default(ncplane* ncp, const cell* c){ if(ret < 0){ return -1; } - ncplane_updamage(ncp); return ret; } @@ -991,7 +953,6 @@ int ncplane_move_above_unsafe(ncplane* restrict n, ncplane* restrict above){ *an = n->z; // splice n out n->z = above; // attach above below n *aa = n; // spline n in above - ncplane_updamage(n); // conservative (we might not actually be visible) return 0; } @@ -1004,7 +965,6 @@ int ncplane_move_below_unsafe(ncplane* restrict n, ncplane* restrict below){ *an = n->z; // splice n out n->z = below->z; // reattach subbelow list to n below->z = n; // splice n in below - ncplane_updamage(n); // conservative (we might not actually be visible) return 0; } @@ -1016,7 +976,6 @@ int ncplane_move_top(ncplane* n){ *an = n->z; // splice n out n->z = n->nc->top; n->nc->top = n; - ncplane_updamage(n); return 0; } @@ -1032,7 +991,6 @@ int ncplane_move_bottom(ncplane* n){ } *an = n; n->z = NULL; - ncplane_updamage(n); return 0; } @@ -1062,24 +1020,6 @@ ncplane_cursor_stuck(const ncplane* n){ return (n->x == n->lenx && n->y == n->leny); } -int cell_duplicate(ncplane* n, cell* targ, const cell* c){ - cell_release(n, targ); - targ->attrword = c->attrword; - targ->channels = c->channels; - if(cell_simple_p(c)){ - targ->gcluster = c->gcluster; - return !!c->gcluster; - } - size_t ulen = strlen(extended_gcluster(n, c)); -// fprintf(stderr, "[%s] (%zu)\n", extended_gcluster(n, c), strlen(extended_gcluster(n, c))); - int eoffset = egcpool_stash(&n->pool, extended_gcluster(n, c), ulen); - if(eoffset < 0){ - return -1; - } - targ->gcluster = eoffset + 0x80; - return ulen; -} - static inline void cell_set_wide(cell* c){ c->channels |= CELL_WIDEASIAN_MASK; @@ -1129,7 +1069,6 @@ int ncplane_putc(ncplane* n, const cell* c){ cell_release(n, candidate); } } - n->damage[n->y] = true; advance_cursor(n, cols); ncplane_unlock(n); return cols; @@ -1174,13 +1113,6 @@ int ncplane_cursor_at(const ncplane* n, cell* c, char** gclust){ return 0; } -void cell_release(ncplane* n, cell* c){ - if(!cell_simple_p(c)){ - egcpool_release(&n->pool, cell_egc_idx(c)); - c->gcluster = 0; // don't subject ourselves to double-release problems - } -} - int cell_load(ncplane* n, cell* c, const char* gcluster){ cell_release(n, c); int bytes; @@ -1536,33 +1468,12 @@ int ncplane_box(ncplane* n, const cell* ul, const cell* ur, return 0; } -// mark all lines of the notcurses object touched by this plane as damaged -void ncplane_updamage(ncplane* n){ - int drangelow = n->absy; - int drangehigh = n->absy + n->leny; - if(drangehigh > n->nc->stdscr->leny){ - drangehigh = n->nc->stdscr->leny; - } - if(drangelow < n->nc->stdscr->absy){ - drangelow = n->nc->stdscr->absy; - } - if(drangelow > n->nc->stdscr->absy + n->nc->stdscr->leny - 1){ - drangelow = n->nc->stdscr->absy + n->nc->stdscr->leny - 1; - } - flash_damage_map(n->nc->damage + drangelow, drangehigh - drangelow, true); -} - int ncplane_move_yx(ncplane* n, int y, int x){ if(n == n->nc->stdscr){ return -1; } - ncplane_updamage(n); // damage any lines we are currently on - bool movedy = n->absy != y; n->absy = y; n->absx = x; - if(movedy){ - ncplane_updamage(n); - } return 0; } @@ -1602,7 +1513,6 @@ void ncplane_erase(ncplane* n){ egcpool_init(&n->pool); cell_load(n, &n->defcell, egc); free(egc); - ncplane_updamage(n); ncplane_unlock(n); } diff --git a/src/lib/render.c b/src/lib/render.c index dff9fc0bc..e8eef21f9 100644 --- a/src/lib/render.c +++ b/src/lib/render.c @@ -1,4 +1,5 @@ #include +#include #include #include #include "internal.h" @@ -98,24 +99,33 @@ prep_optimized_palette(notcurses* nc, FILE* out __attribute__ ((unused))){ return 0; } -/* // reshape the shadow framebuffer to match the stdplane's dimensions, throwing // away the old one. static int reshape_shadow_fb(notcurses* nc){ - const size_t size = sizeof(nc->shadowbuf) * nc->stdscr->leny * nc->stdscr->lenx; - cell* fb = malloc(size); + if(nc->lfdimx == nc->stdscr->lenx && nc->lfdimy == nc->stdscr->leny){ + return 0; // no change + } + const size_t size = sizeof(*nc->lastframe) * nc->stdscr->leny * nc->stdscr->lenx; + cell* fb = realloc(nc->lastframe, size); if(fb == NULL){ + free(nc->lastframe); + nc->lastframe = NULL; + nc->lfdimx = 0; + nc->lfdimy = 0; return -1; } - free(nc->shadowbuf); - nc->shadowbuf = fb; - nc->shadowy = nc->stdscr->leny; - nc->shadowx = nc->stdscr->lenx; + nc->lastframe = fb; + // FIXME more memset()tery than we need, both wasting work and wrecking + // damage detection for the upcoming render + memset(nc->lastframe, 0, size); + nc->lastframe = fb; + nc->lfdimy = nc->stdscr->leny; + nc->lfdimx = nc->stdscr->lenx; memset(fb, 0, size); + egcpool_dump(&nc->pool); return 0; } -*/ // Find the topmost cell for this coordinate by walking down the z-buffer, // looking for an intersecting ncplane. Once we've found one, check it for @@ -135,9 +145,8 @@ reshape_shadow_fb(notcurses* nc){ // whichever one occurs at the top with a non-transparent α (α < 3). To effect // tail recursion, though, we instead write first, and then recurse, blending // as we descend. α <= 0 is opaque. α >= 3 is fully transparent. -static ncplane* -dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, - bool* damage){ +static inline ncplane* +dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha){ // once we decide on our glyph, it cannot be changed by anything below, so // lock in this plane for the actual cell return. ncplane* glyphplane = NULL; @@ -164,20 +173,12 @@ dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, c->attrword = vis->attrword; cell_set_fchannel(c, cell_get_fchannel(vis)); // FIXME blend it in falpha -= (CELL_ALPHA_TRANSPARENT - nalpha); // FIXME blend it in - if(p->damage[poffy]){ - *damage = true; - p->damage[poffy] = false; - } } } } if(balpha > 0 && (nalpha = cell_get_bg_alpha(vis)) < CELL_ALPHA_TRANSPARENT){ cell_set_bchannel(c, cell_get_bchannel(vis)); // FIXME blend it in balpha -= (CELL_ALPHA_TRANSPARENT - nalpha); - if(p->damage[poffy]){ - *damage = true; - p->damage[poffy] = false; - } } if((falpha <= 0 && balpha <= 0) || !p->z){ // done! return glyphplane ? glyphplane : p; @@ -191,10 +192,9 @@ dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, } static inline ncplane* -visible_cell(cell* c, int y, int x, ncplane* n, bool* damage){ +visible_cell(cell* c, int y, int x, ncplane* n){ cell_init(c); - return dig_visible_cell(c, y, x, n, CELL_ALPHA_TRANSPARENT, - CELL_ALPHA_TRANSPARENT, damage); + return dig_visible_cell(c, y, x, n, CELL_ALPHA_TRANSPARENT, CELL_ALPHA_TRANSPARENT); } // write the cell's UTF-8 grapheme cluster to the provided FILE*. returns the @@ -385,6 +385,74 @@ term_fg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ return 0; } +static inline void +pool_release(egcpool* pool, cell* c){ + if(!cell_simple_p(c)){ + egcpool_release(pool, cell_egc_idx(c)); + c->gcluster = 0; // don't subject ourselves to double-release problems + } +} + +void cell_release(ncplane* n, cell* c){ + pool_release(&n->pool, c); +} + +// Duplicate one cell onto another, possibly crossing ncplanes. +static inline int +cell_duplicate_far(egcpool* tpool, cell* targ, const ncplane* splane, const cell* c){ + pool_release(tpool, targ); + targ->attrword = c->attrword; + targ->channels = c->channels; + if(cell_simple_p(c)){ + targ->gcluster = c->gcluster; + return !!c->gcluster; + } + size_t ulen = strlen(extended_gcluster(splane, c)); +// fprintf(stderr, "[%s] (%zu)\n", extended_gcluster(n, c), strlen(extended_gcluster(n, c))); + int eoffset = egcpool_stash(tpool, extended_gcluster(splane, c), ulen); + if(eoffset < 0){ + return -1; + } + targ->gcluster = eoffset + 0x80; + return ulen; +} + +// Duplicate one cell onto another when they share a plane. Convenience wrapper. +int cell_duplicate(ncplane* n, cell* targ, const cell* c){ + return cell_duplicate_far(&n->pool, targ, n, c); +} + +// the heart of damage detection. compare two cells (from two different planes) +// for equality. if they are equal, return 0. otherwise, dup the second onto +// the first and return non-zero. +static int +cellcmp_and_dupfar(egcpool* dampool, cell* damcell, const ncplane* srcplane, + const cell* srccell){ + if(damcell->attrword == srccell->attrword){ + if(damcell->channels == srccell->channels){ + bool damsimple = cell_simple_p(damcell); + bool srcsimple = cell_simple_p(srccell); + if(damsimple == srcsimple){ + if(damsimple){ + if(damcell->gcluster == srccell->gcluster){ + return 0; // simple match + } + }else{ + const char* damegc = egcpool_extended_gcluster(dampool, damcell); + const char* srcegc = extended_gcluster(srcplane, srccell); + assert(strcmp(damegc, "三体")); + assert(strcmp(srcegc, "三体")); + if(strcmp(damegc, srcegc) == 0){ + return 0; // EGC match + } + } + } + } + } + cell_duplicate_far(dampool, damcell, srcplane, srccell); + return 1; +} + static inline int notcurses_render_internal(notcurses* nc){ int ret = 0; @@ -405,40 +473,55 @@ notcurses_render_internal(notcurses* nc){ // cells and the current cell uses no defaults, or if both the current and // the last used both defaults. bool fgelidable = false, bgelidable = false, defaultelidable = false; - /*if(nc->stdscr->leny != nc->shadowy || nc->stdscr->lenx != nc->shadowx){ - reshape_shadow_fb(nc); - }*/ + // if this fails, struggle bravely on. we can live without a lastframe. + reshape_shadow_fb(nc); for(y = 0 ; y < nc->stdscr->leny ; ++y){ - bool linedamaged = false; // have we repositioned the cursor to start line? - bool newdamage = nc->damage[y]; -// fprintf(stderr, "nc->damage[%d] (%p) = %u\n", y, nc->damage + y, nc->damage[y]); - if(newdamage){ - nc->damage[y] = 0; - } - // move to the beginning of the line, in case our accounting was befouled - // by wider- (or narrower-) than-reported characters + // how many characters have we elided? it's not worthwhile to invoke a + // cursor movement with cup if we only elided one or two. set to INT_MAX + // whenever we're on a new line. + int needmove = INT_MAX; for(x = 0 ; x < nc->stdscr->lenx ; ++x){ unsigned r, g, b, br, bg, bb; ncplane* p; cell c; // no need to initialize - p = visible_cell(&c, y, x, nc->top, &newdamage); - assert(p); + p = visible_cell(&c, y, x, nc->top); // don't try to print a wide character on the last column; it'll instead // be printed on the next line. they probably shouldn't be admitted, but // we can end up with one due to a resize. + // FIXME but...print what, exactly, instead? if((x + 1 >= nc->stdscr->lenx && cell_double_wide_p(&c))){ - continue; + continue; // needmove will be reset as we restart the line } - if(!linedamaged){ - if(newdamage){ - term_emit("cup", tiparm(nc->cup, y, x), out, false); - nc->stats.cellelisions += x; - nc->stats.cellemissions += (nc->stdscr->lenx - x); - linedamaged = true; - }else{ + // lastframe has already been sized to match the current size, so no need + // to check whether we're within its bounds. just check the cell. + if(nc->lastframe){ + cell* oldcell = &nc->lastframe[fbcellidx(nc->stdscr, y, x)]; + if(cellcmp_and_dupfar(&nc->pool, oldcell, p, &c) == 0){ + // no need to emit a cell; what we rendered appears to already be + // here. no updates are performed to elision state nor lastframe. + ++nc->stats.cellelisions; + if(needmove < INT_MAX){ + ++needmove; + } + if(cell_double_wide_p(&c)){ + if(needmove < INT_MAX){ + ++needmove; + } + ++nc->stats.cellelisions; + ++x; + } continue; } } + ++nc->stats.cellemissions; + if(needmove > 8){ // FIXME cuf and cuf1 aren't guaranteed! + term_emit("cup", tiparm(nc->cup, y, x), out, false); + }else if(needmove > 1){ + term_emit("cuf", tiparm(nc->cuf, needmove), out, false); + }else if(needmove){ + term_emit("cuf1", tiparm(nc->cuf1), out, false); + } + needmove = 0; // set the style. this can change the color back to the default; if it // does, we need update our elision possibilities. bool normalized; @@ -498,9 +581,6 @@ notcurses_render_internal(notcurses* nc){ ++x; } } - if(linedamaged == false){ - nc->stats.cellelisions += x; - } } ret |= fflush(out); fflush(nc->ttyfp);