From de082c7ba20234c7b8a5a9fa83ced8442bfe721c Mon Sep 17 00:00:00 2001 From: nick black Date: Sun, 22 Nov 2020 07:48:38 -0500 Subject: [PATCH] ncpile creation/destruction #1078 --- src/lib/debug.c | 23 ++++-- src/lib/internal.h | 52 +++++++----- src/lib/notcurses.c | 190 ++++++++++++++++++++++++++++++++------------ src/lib/render.c | 2 +- tests/ncplane.cpp | 1 + 5 files changed, 189 insertions(+), 79 deletions(-) diff --git a/src/lib/debug.c b/src/lib/debug.c index f8cad6ede..ed1b6200f 100644 --- a/src/lib/debug.c +++ b/src/lib/debug.c @@ -1,10 +1,10 @@ #include "internal.h" -void notcurses_debug(notcurses* nc, FILE* debugfp){ - const ncplane* n = nc->top; +static void +ncpile_debug(const ncpile* p, FILE* debugfp){ + const ncplane* n = p->top; const ncplane* prev = NULL; int planeidx = 0; - fprintf(debugfp, " ************************** notcurses debug state *****************************\n"); while(n){ fprintf(debugfp, "%04d off y: %3d x: %3d geom y: %3d x: %3d curs y: %3d x: %3d %p %.8s\n", planeidx, n->absy, n->absx, n->leny, n->lenx, n->y, n->x, n, n->name); @@ -12,9 +12,6 @@ void notcurses_debug(notcurses* nc, FILE* debugfp){ fprintf(debugfp, " bound %p → %p ← %p binds %p\n", n->boundto, n->bnext, n->bprev, n->blist); } - if(n->bnext == n || (n->boundto == n && n != nc->stdplane) || n->blist == n){ - fprintf(debugfp, "WARNING: bound pointers target self\n"); - } if(n->bprev && (*n->bprev != n)){ fprintf(stderr, " WARNING: expected *->bprev %p, got %p\n", n, *n->bprev); } @@ -25,8 +22,18 @@ void notcurses_debug(notcurses* nc, FILE* debugfp){ n = n->below; ++planeidx; } - if(nc->bottom != prev){ - fprintf(stderr, " WARNING: expected ->bottom %p, got %p\n", prev, nc->bottom); + if(p->bottom != prev){ + fprintf(stderr, " WARNING: expected ->bottom %p, got %p\n", prev, p->bottom); } +} + +void notcurses_debug(notcurses* nc, FILE* debugfp){ + const ncpile* p = ncplane_pile(nc->stdplane); + fprintf(debugfp, " ************************** notcurses debug state *****************************\n"); + const ncpile* p0 = p; + do{ + ncpile_debug(p0, debugfp); + p0 = p0->next; + }while(p != p0); fprintf(debugfp, " ******************************************************************************\n"); } diff --git a/src/lib/internal.h b/src/lib/internal.h index aa2c4fa46..c941834be 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -46,16 +46,11 @@ struct esctrie; // and we can't go throwing C++ syntax into this header. so it goes. // A plane is memory for some rectilinear virtual window, plus current cursor -// state for that window. A notcurses context describes a single terminal, and -// has a z-order of planes (I see no advantage to maintaining a poset, and we -// instead just use a list, top-to-bottom). Every cell on the terminal is part -// of at least one plane, and at least one plane covers the entirety of the -// terminal (this plane is created during initialization). -// -// Functions update these virtual planes over a series of API calls. Eventually, -// notcurses_render() is called. We then do a depth buffer blit of updated -// cells. A cell is updated if the topmost plane including that cell updates it, -// not simply if any plane updates it. +// state for that window, and part of a pile. Each pile has a total order along +// its z-axis. Functions update these virtual planes over a series of API +// calls. Eventually, notcurses_render() is called. We then do a depth buffer +// blit of updated cells. A cell is updated if the topmost plane including that +// cell updates it, not simply if any plane updates it. // // A plane may be partially or wholly offscreen--this might occur if the // screen is resized, for example. Offscreen portions will not be rendered. @@ -63,7 +58,7 @@ struct esctrie; // // The framebuffer 'fb' is a set of rows. For scrolling, we interpret it as a // circular buffer of rows. 'logrow' is the index of the row at the logical top -// of the plane. +// of the plane. It only changes from 0 if the plane is scrollable. typedef struct ncplane { cell* fb; // "framebuffer" of character cells int logrow; // logical top row, starts at 0, add one for each scroll @@ -89,7 +84,7 @@ typedef struct ncplane { void* userptr; // slot for the user to stick some opaque pointer int (*resizecb)(struct ncplane*); // callback after parent is resized cell basecell; // cell written anywhere that fb[i].gcluster == 0 - struct notcurses* nc; // notcurses object of which we are a part + struct ncpile* pile; // pile of which we are a part char* name; // used only for debugging ncalign_e align; // relative to parent plane, for automatic realignment uint16_t stylemask; // same deal as in a cell @@ -303,23 +298,31 @@ typedef struct ncdirect { uint64_t flags; // copied in ncdirect_init() from param } ncdirect; -typedef struct notcurses { +typedef struct ncpile { ncplane* top; // topmost plane, never NULL ncplane* bottom; // bottommost plane, never NULL - ncplane* stdplane;// standard plane, covers screen + ncplane* root; // first plane of the root set + struct notcurses* nc; // notcurses context + struct ncpile *prev, *next; // circular list +} ncpile; + +// the standard pile can be reached through ->stdplane. +typedef struct notcurses { + ncplane* stdplane; // standard plane, covers screen // the style state of the terminal is carried across render runs renderstate rstate; // 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 + cell* lastframe;// last rasterized framebuffer, NULL until first rasterization + egcpool pool; // egcpool for lastframe + 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 + int lfdimy; // lfdimx/lfdimy are 0 until first rasterization - int cursory; // desired cursor placement according to user. -1 is a don't- - int cursorx; // care, otherwise moved here after each render. + int cursory; // desired cursor placement according to user. + int cursorx; // -1 is don't-care, otherwise moved here after each render. ncstats stats; // some statistics across the lifetime of the notcurses ctx ncstats stashstats; // cumulative stats, unaffected by notcurses_stats_reset() @@ -333,6 +336,7 @@ typedef struct notcurses { FILE* renderfp; // debugging FILE* to which renderings are written tinfo tcache; // terminfo cache struct termios tpreserved; // terminal state upon entry + pthread_mutex_t pilelock; // guards pile list bool suppress_banner; // from notcurses_options // desired margins (best-effort only), copied in from notcurses_options @@ -375,6 +379,16 @@ mbstr_find_codepoint(const char* s, char32_t cp, int* col){ return -1; } +static inline ncpile* +ncplane_pile(ncplane* n){ + return n->pile; +} + +static inline const ncpile* +ncplane_pile_const(const ncplane* n){ + return n->pile; +} + static inline ncplane* ncplane_stdplane(ncplane* n){ return notcurses_stdplane(ncplane_notcurses(n)); diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index ca253b2fc..28ed886bd 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -271,12 +271,28 @@ int update_term_dimensions(int fd, int* rows, int* cols){ return 0; } +// destroy an empty ncpile. only call with pilelock held. +static void +ncpile_destroy(ncpile* pile){ + if(pile){ + pile->prev->next = pile->next; + pile->next->prev = pile->prev; + free(pile); + } +} + void free_plane(ncplane* p){ if(p){ - // ncdirect fakes an ncplane with no ->nc - if(ncplane_notcurses(p)){ + // ncdirect fakes an ncplane with no ->pile + if(ncplane_pile(p)){ --ncplane_notcurses(p)->stats.planes; ncplane_notcurses(p)->stats.fbbytes -= sizeof(*p->fb) * p->leny * p->lenx; + if(p->above == NULL && p->below == NULL){ + struct notcurses* nc = ncplane_notcurses(p); + pthread_mutex_lock(&nc->pilelock); + ncpile_destroy(ncplane_pile(p)); + pthread_mutex_unlock(&nc->pilelock); + } } egcpool_dump(&p->pool); free(p->name); @@ -285,17 +301,42 @@ void free_plane(ncplane* p){ } } +// create a new ncpile. only call with pilelock held. +static ncpile* +ncpile_create(notcurses* nc){ + ncpile* ret = malloc(sizeof(*ret)); + if(ret){ + ret->nc = nc; + ret->top = NULL; + ret->bottom = NULL; + ret->root = NULL; + if(nc->stdplane){ + ret->prev = ncplane_pile(nc->stdplane)->prev; + ncplane_pile(nc->stdplane)->prev->next = ret; + ret->next = ncplane_pile(nc->stdplane); + ncplane_pile(nc->stdplane)->prev = ret; + }else{ + ret->prev = ret; + ret->next = ret; + } + } + return ret; +} + // create a new ncplane at the specified location (relative to the true screen, // having origin at 0,0), having the specified size, and put it at the top of // the planestack. its cursor starts at its origin; its style starts as null. // a plane may exceed the boundaries of the screen, but must have positive -// size in both dimensions. bind the plane to 'n', which may be NULL. if bound -// to a plane, this plane moves when that plane moves, and move targets are -// relative to that plane. -// there's a denormalized case we also must handle, that of the "fake" isolated -// ncplane created by ncdirect for rendering visuals. in that case (and only in -// that case), nc is NULL. -ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* nopts){ +// size in both dimensions. bind the plane to 'n', which may be NULL to create +// a new pile. if bound to a plane instead, this plane moves when that plane +// moves, and coordinates to move to are relative to that plane. +// there are two denormalized case we also must handle, that of the "fake" +// isolated ncplane created by ncdirect for rendering visuals. in that case +// (and only in that case), nc is NULL (as is n). there's also creation of the +// initial standard plane, in which case nc is not NULL, but nc->stdplane *is* +// (as once more is n). +ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, + const ncplane_options* nopts){ if(nopts->flags > NCPLANE_OPTION_HORALIGNED){ logwarn(nc, "Provided unsupported flags %016lx\n", nopts->flags); } @@ -327,6 +368,7 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* p->align = nopts->x; }else{ p->absx = nopts->x; + p->align = NCALIGN_UNALIGNED; } p->absx += n->absx; p->absy = nopts->y + n->absy; @@ -335,8 +377,7 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* } p->bprev = &n->blist; *p->bprev = p; - }else{ // new standard plane - assert(!(nopts->flags & NCPLANE_OPTION_HORALIGNED)); + }else{ // new root plane, new pile assert(0 == nopts->y); assert(0 == nopts->x); p->absx = (nc ? nc->margin_l : 0); @@ -344,6 +385,8 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* p->bnext = NULL; p->bprev = NULL; p->boundto = p; + p->absx = nopts->x; + p->align = NCALIGN_UNALIGNED; } p->resizecb = nopts->resizecb; p->stylemask = 0; @@ -352,17 +395,27 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* cell_init(&p->basecell); p->userptr = nopts->userptr; p->above = NULL; - if( (p->nc = nc) ){ // every plane has a notcurses object - if( (p->below = nc->top) ){ // always happens save initial plane - nc->top->above = p; - }else{ - nc->bottom = p; - } - nc->top = p; - nc->stats.fbbytes += fbsize; - ++nc->stats.planes; - }else{ // fake ncplane backing ncdirect object + if(nc == NULL){ // fake ncplane backing ncdirect object p->below = NULL; + }else{ + pthread_mutex_lock(&nc->pilelock); + ncpile* pile = n ? ncplane_pile(n) : NULL; + if( (p->pile = pile) ){ // existing pile + if( (p->below = pile->top) ){ // always happens save initial plane + pile->top->above = p; + }else{ + pile->bottom = p; + } + pile->top = p; + nc->stats.fbbytes += fbsize; + ++nc->stats.planes; + }else{ // new pile + p->pile = ncpile_create(nc); + p->pile->top = p; + p->pile->bottom = p; + p->below = NULL; + } + pthread_mutex_unlock(&nc->pilelock); } loginfo(nc, "Created new %dx%d plane \"%s\" @ %dx%d\n", nopts->rows, nopts->cols, p->name, p->absy, p->absx); @@ -392,6 +445,10 @@ const ncplane* notcurses_stdplane_const(const notcurses* nc){ } ncplane* ncplane_create(ncplane* n, const ncplane_options* nopts){ + if((nopts->flags & NCPLANE_OPTION_HORALIGNED) && !n){ + logerror(ncplane_notcurses(n), "Can't align a root plane"); + return NULL; + } return ncplane_new_internal(ncplane_notcurses(n), n, nopts); } @@ -620,12 +677,12 @@ int ncplane_destroy(ncplane* ncp){ if(ncp->above){ ncp->above->below = ncp->below; }else{ - ncplane_notcurses(ncp)->top = ncp->below; + ncplane_pile(ncp)->top = ncp->below; } if(ncp->below){ ncp->below->above = ncp->above; }else{ - ncplane_notcurses(ncp)->bottom = ncp->above; + ncplane_pile(ncp)->bottom = ncp->above; } if(ncp->bprev){ if( (*ncp->bprev = ncp->bnext) ){ @@ -921,6 +978,11 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ if(outfp == NULL){ outfp = stdout; } + if(pthread_mutex_init(&ret->pilelock, NULL)){ + fprintf(stderr, "Couldn't initialize pile mutex\n"); + free(ret); + return NULL; + } ret->margin_t = opts->margin_t; ret->margin_b = opts->margin_b; ret->margin_l = opts->margin_l; @@ -1002,25 +1064,25 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ terminfostr(&ret->tcache.smcup, "smcup"); terminfostr(&ret->tcache.rmcup, "rmcup"); } - ret->bottom = ret->top = ret->stdplane = NULL; if(ncvisual_init(ffmpeg_log_level(ret->loglevel))){ goto err; } + ret->stdplane = NULL; if((ret->stdplane = create_initial_ncplane(ret, dimy, dimx)) == NULL){ goto err; } if(ret->ttyfd >= 0){ if(ret->tcache.smkx && tty_emit("smkx", ret->tcache.smkx, ret->ttyfd)){ - free_plane(ret->top); + free_plane(ret->stdplane); goto err; } if(ret->tcache.civis && tty_emit("civis", ret->tcache.civis, ret->ttyfd)){ - free_plane(ret->top); + free_plane(ret->stdplane); goto err; } } if((ret->rstate.mstreamfp = open_memstream(&ret->rstate.mstream, &ret->rstate.mstrsize)) == NULL){ - free_plane(ret->top); + free_plane(ret->stdplane); goto err; } ret->rstate.x = ret->rstate.y = -1; @@ -1029,7 +1091,7 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ if(ret->ttyfd >= 0){ if(ret->tcache.smcup){ if(tty_emit("smcup", ret->tcache.smcup, ret->ttyfd)){ - free_plane(ret->top); + free_plane(ret->stdplane); goto err; } // explicit clear even though smcup *might* clear @@ -1048,6 +1110,7 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ return ret; err: + fprintf(stderr, "Alas, you will not be going to space today.\n"); // FIXME looks like we have some memory leaks on this error path? tcsetattr(ret->ttyfd, TCSANOW, &ret->tpreserved); drop_signals(ret); @@ -1055,8 +1118,10 @@ err: return NULL; } -void notcurses_drop_planes(notcurses* nc){ - ncplane* p = nc->top; +// updates *pile to point at (*pile)->next, frees all but standard pile/plane +static void +ncpile_drop(notcurses* nc, ncpile** pile){ + ncplane* p = (*pile)->top; while(p){ ncplane* tmp = p->below; if(nc->stdplane != p){ @@ -1064,7 +1129,24 @@ void notcurses_drop_planes(notcurses* nc){ } p = tmp; } - nc->top = nc->bottom = nc->stdplane; + ncpile* tmp = (*pile)->next; + if(*pile != ncplane_pile(nc->stdplane)){ + ncpile_destroy(*pile); + } + *pile = tmp; +} + +// drop all piles and all planes, save the standard plane and its pile +void notcurses_drop_planes(notcurses* nc){ + pthread_mutex_lock(&nc->pilelock); + ncpile* p = ncplane_pile(nc->stdplane); + ncpile* p0 = p; + do{ + ncpile_drop(nc, &p); + }while(p0 != p); + pthread_mutex_unlock(&nc->pilelock); + ncplane_pile(nc->stdplane)->top = nc->stdplane; + ncplane_pile(nc->stdplane)->bottom = nc->stdplane; nc->stdplane->above = nc->stdplane->below = NULL; } @@ -1072,10 +1154,9 @@ int notcurses_stop(notcurses* nc){ int ret = 0; if(nc){ ret |= notcurses_stop_minimal(nc); - while(nc->top){ - ncplane* p = nc->top->below; - free_plane(nc->top); - nc->top = p; + if(nc->stdplane){ + notcurses_drop_planes(nc); + free_plane(nc->stdplane); } if(nc->rstate.mstreamfp){ fclose(nc->rstate.mstreamfp); @@ -1135,6 +1216,7 @@ int notcurses_stop(notcurses* nc){ } } del_curterm(cur_term); + ret |= pthread_mutex_destroy(&nc->pilelock); free(nc); } return ret; @@ -1248,22 +1330,25 @@ int ncplane_move_above(ncplane* restrict n, ncplane* restrict above){ if(n == above){ return -1; } + if(ncplane_pile(n) != ncplane_pile(above)){ // can't move among piles + return -1; + } if(n->below != above){ // splice out 'n' if(n->below){ n->below->above = n->above; }else{ - ncplane_notcurses(n)->bottom = n->above; + ncplane_pile(n)->bottom = n->above; } if(n->above){ n->above->below = n->below; }else{ - ncplane_notcurses(n)->top = n->below; + ncplane_pile(n)->top = n->below; } if( (n->above = above->above) ){ above->above->below = n; }else{ - ncplane_notcurses(n)->top = n; + ncplane_pile(n)->top = n; } above->above = n; n->below = above; @@ -1276,21 +1361,24 @@ int ncplane_move_below(ncplane* restrict n, ncplane* restrict below){ if(n == below){ return -1; } + if(ncplane_pile(n) != ncplane_pile(below)){ // can't move among piles + return -1; + } if(n->above != below){ if(n->below){ n->below->above = n->above; }else{ - ncplane_notcurses(n)->bottom = n->above; + ncplane_pile(n)->bottom = n->above; } if(n->above){ n->above->below = n->below; }else{ - ncplane_notcurses(n)->top = n->below; + ncplane_pile(n)->top = n->below; } if( (n->below = below->below) ){ below->below->above = n; }else{ - ncplane_notcurses(n)->bottom = n; + ncplane_pile(n)->bottom = n; } below->below = n; n->above = below; @@ -1303,13 +1391,13 @@ void ncplane_move_top(ncplane* n){ if( (n->above->below = n->below) ){ n->below->above = n->above; }else{ - ncplane_notcurses(n)->bottom = n->above; + ncplane_pile(n)->bottom = n->above; } n->above = NULL; - if( (n->below = ncplane_notcurses(n)->top) ){ + if( (n->below = ncplane_pile(n)->top) ){ n->below->above = n; } - ncplane_notcurses(n)->top = n; + ncplane_pile(n)->top = n; } } @@ -1318,13 +1406,13 @@ void ncplane_move_bottom(ncplane* n){ if( (n->below->above = n->above) ){ n->above->below = n->below; }else{ - ncplane_notcurses(n)->top = n->below; + ncplane_pile(n)->top = n->below; } n->below = NULL; - if( (n->above = ncplane_notcurses(n)->bottom) ){ + if( (n->above = ncplane_pile(n)->bottom) ){ n->above->below = n; } - ncplane_notcurses(n)->bottom = n; + ncplane_pile(n)->bottom = n; } } @@ -1898,11 +1986,11 @@ void ncplane_erase(ncplane* n){ } ncplane* notcurses_top(notcurses* n){ - return n->top; + return ncplane_pile(n->stdplane)->top; } ncplane* notcurses_bottom(notcurses* n){ - return n->bottom; + return ncplane_pile(n->stdplane)->bottom; } ncplane* ncplane_below(ncplane* n){ @@ -2021,11 +2109,11 @@ void ncplane_translate(const ncplane* src, const ncplane* dst, } notcurses* ncplane_notcurses(ncplane* n){ - return n->nc; + return ncplane_pile(n)->nc; } const notcurses* ncplane_notcurses_const(const ncplane* n){ - return n->nc; + return ncplane_pile_const(n)->nc; } ncplane* ncplane_parent(ncplane* n){ diff --git a/src/lib/render.c b/src/lib/render.c index cf4339859..a912ad1b7 100644 --- a/src/lib/render.c +++ b/src/lib/render.c @@ -1039,7 +1039,7 @@ static int notcurses_render_internal(notcurses* nc, struct crender* rvec){ int dimy, dimx; ncplane_dim_yx(nc->stdplane, &dimy, &dimx); - ncplane* p = nc->top; + ncplane* p = ncplane_pile(nc->stdplane)->top; while(p){ paint(p, rvec, nc->stdplane->leny, nc->stdplane->lenx, nc->stdplane->absy, nc->stdplane->absx); diff --git a/tests/ncplane.cpp b/tests/ncplane.cpp index 7bed018cd..ef2fac00c 100644 --- a/tests/ncplane.cpp +++ b/tests/ncplane.cpp @@ -724,6 +724,7 @@ TEST_CASE("NCPlane") { // FIXME verify with ncplane_at_cursor_cell() CHECK(0 == ncplane_destroy(ncp)); } + SUBCASE("MoveToLowerRight") { int ncols, nrows; ncplane_dim_yx(n_, &nrows, &ncols);