From f47bde1c4e2e804b5131857f45c35181fb6f9c05 Mon Sep 17 00:00:00 2001 From: nick black Date: Fri, 29 May 2020 01:24:34 -0400 Subject: [PATCH] Graceful fallback among blitters #637 If we're in ASCII mode, no blitter except for NCBLIT_1x1 is going to work. Whenever NCBLIT_DEFAULT is provided, select NCBLIT_1x1 if we're in ASCII mode. Add NCVISUAL_OPTIONS_MAYDEGRADE and NCPLOT_OPTIONS_MAYDEGRADE. Both serve to allow smooth degradation when a blitter other than NCBLIT_DEFAULT has been provided. Closes #637. Make calc_gradient_cell() static inline so our templated ncppplot implementation can use it (ugh). When using NCBLIT_1x1 for plots in ASCII mode, use space rather than full block, and invert colors. Use NCBLIT_DEFAULT in the demo for the FPS plot. --- doc/man/man3/notcurses_visual.3.md | 16 ++++- include/notcurses/notcurses.h | 11 +++- src/demo/hud.c | 1 - src/input/input.cpp | 2 +- src/lib/blit.c | 2 +- src/lib/blitset.h | 12 +++- src/lib/fill.c | 99 ++---------------------------- src/lib/internal.h | 96 +++++++++++++++++++++++++++++ src/lib/plot.h | 97 +++++++++++++++++++---------- src/lib/visual.cpp | 11 ++-- 10 files changed, 206 insertions(+), 141 deletions(-) diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md index bc8687fb6..bd4bf18d2 100644 --- a/doc/man/man3/notcurses_visual.3.md +++ b/doc/man/man3/notcurses_visual.3.md @@ -28,7 +28,21 @@ typedef enum { NCBLIT_SIXEL, // six rows, 1 column (RGB) } ncblitter_e; -typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*); +#define NCVISUAL_OPTIONS_MAYDEGRADE 0x0001 + +struct ncvisual_options { + struct ncplane* n; + ncscale_e scaling; + int y, x; + int begy, begx; // origin of rendered section + int leny, lenx; // size of rendered section + ncblitter_e blitter; // glyph set to use (maps input to output cells) + uint64_t flags; // bitmask over NCVISUAL_OPTIONS_* +}; + + + + typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*); ``` **bool notcurses_canopen_images(const struct notcurses* nc);** diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index 23dc029b6..2b84792b9 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -2142,6 +2142,8 @@ API nc_err_e ncvisual_rotate(struct ncvisual* n, double rads); // transformation, unless the size is unchanged. API nc_err_e ncvisual_resize(struct ncvisual* n, int rows, int cols); +#define NCVISUAL_OPTIONS_MAYDEGRADE 0x0001 // blitter can be worse than requested + struct ncvisual_options { // if no ncplane is provided, one will be created using the exact size // necessary to render the source with perfect fidelity (this might be @@ -2161,8 +2163,10 @@ struct ncvisual_options { // these numbers are all in terms of ncvisual pixels. int begy, begx; // origin of rendered section int leny, lenx; // size of rendered section + // use NCBLIT_DEFAULT if you don't care, to use NCBLIT_2x2 (assuming + // UTF8) or NCBLIT_1x1 (in an ASCII environment) ncblitter_e blitter; // glyph set to use (maps input to output cells) - uint64_t flags; // currently all zero + uint64_t flags; // bitmask over NCVISUAL_OPTIONS_* }; // Render the decoded frame to the specified ncplane (if one is not provided, @@ -2737,12 +2741,15 @@ API int ncmenu_destroy(struct ncmenu* n); #define NCPLOT_OPTIONS_LABELTICKSD 0x0001 // show labels for dependent axis #define NCPLOT_OPTIONS_EXPONENTIALD 0x0002 // exponential dependent axis #define NCPLOT_OPTIONS_VERTICALI 0x0004 // independent axis is vertical +#define NCPLOT_OPTIONS_MAYDEGRADE 0x0008 // blitter can be worse than requested typedef struct ncplot_options { // channels for the maximum and minimum levels. linear interpolation will be // applied across the domain between these two. uint64_t maxchannel; uint64_t minchannel; + // if you don't care, pass NCBLIT_DEFAULT and get NCBLIT_8x1 (assuming + // UTF8) or NCBLIT_1x1 (in an ASCII environment) ncblitter_e gridtype; // number of "pixels" per row x column // independent variable can either be a contiguous range, or a finite set // of keys. for a time range, say the previous hour sampled with second @@ -2756,7 +2763,7 @@ typedef struct ncplot_options { // The plot will make free use of the entirety of the plane. // for domain autodiscovery, set miny == maxy == 0. API struct ncuplot* ncuplot_create(struct ncplane* n, const ncplot_options* opts, - uint64_t miny, uint64_t maxy); + uint64_t miny, uint64_t maxy); API struct ncdplot* ncdplot_create(struct ncplane* n, const ncplot_options* opts, double miny, double maxy); diff --git a/src/demo/hud.c b/src/demo/hud.c index 0f0c056e1..aaa2b2390 100644 --- a/src/demo/hud.c +++ b/src/demo/hud.c @@ -549,7 +549,6 @@ int fpsgraph_init(struct notcurses* nc){ ncplot_options opts; memset(&opts, 0, sizeof(opts)); opts.flags = NCPLOT_OPTIONS_LABELTICKSD | NCPLOT_OPTIONS_EXPONENTIALD; - opts.gridtype = NCBLIT_8x1; channels_set_fg_rgb(&opts.minchannel, 0xff, 0x00, 0xff); channels_set_bg(&opts.minchannel, 0x201020); channels_set_bg_alpha(&opts.minchannel, CELL_ALPHA_BLEND); diff --git a/src/input/input.cpp b/src/input/input.cpp index 5df44e970..ba57dba03 100644 --- a/src/input/input.cpp +++ b/src/input/input.cpp @@ -218,7 +218,7 @@ int main(void){ ncpp::Plane pplane{PLOTHEIGHT, dimx, dimy - PLOTHEIGHT, 0, nullptr}; struct ncplot_options popts{}; // FIXME would be nice to switch over to exponential at some level - popts.flags = NCPLOT_OPTIONS_LABELTICKSD; + popts.flags = NCPLOT_OPTIONS_LABELTICKSD | NCPLOT_OPTIONS_MAYDEGRADE; popts.minchannel = popts.maxchannel = 0; channels_set_fg_rgb(&popts.minchannel, 0x40, 0x50, 0xb0); channels_set_fg_rgb(&popts.maxchannel, 0x40, 0xff, 0xd0); diff --git a/src/lib/blit.c b/src/lib/blit.c index 538e61271..4e21ebe12 100644 --- a/src/lib/blit.c +++ b/src/lib/blit.c @@ -283,7 +283,7 @@ braille_blit(ncplane* nc, int placey, int placex, int linesize, } // NCBLIT_DEFAULT is not included, as it has no defined properties. It ought -// be replaced with some real blitter implementation. +// be replaced with some real blitter implementation by the calling widget. const struct blitset geomdata[] = { { .geom = NCBLIT_8x1, .width = 1, .height = 8, .egcs = L" ▁▂▃▄▅▆▇█", .blit = NULL, .fill = false, }, diff --git a/src/lib/blitset.h b/src/lib/blitset.h index 843d50a2f..735d0abfe 100644 --- a/src/lib/blitset.h +++ b/src/lib/blitset.h @@ -1,8 +1,18 @@ #ifndef NOTCURSES_BLITSET #define NOTCURSES_BLITSET +#include "notcurses/notcurses.h" + static inline const struct blitset* -lookup_blitset(ncblitter_e setid) { +lookup_blitset(const struct notcurses* nc, ncblitter_e setid, bool may_degrade) { + // the only viable blitter in ASCII is NCBLIT_1x1 + if(!notcurses_canutf8(nc) && setid != NCBLIT_1x1){ + if(may_degrade){ + setid = NCBLIT_1x1; + }else{ + return NULL; + } + } const struct blitset* bset = geomdata; while(bset->egcs){ if(bset->geom == setid){ diff --git a/src/lib/fill.c b/src/lib/fill.c index 004d99ee4..06b0df49d 100644 --- a/src/lib/fill.c +++ b/src/lib/fill.c @@ -67,99 +67,6 @@ int ncplane_polyfill_yx(ncplane* n, int y, int x, const cell* c){ return ret; } -// Our gradient is a 2d lerp among the four corners of the region. We start -// with the observation that each corner ought be its exact specified corner, -// and the middle ought be the exact average of all four corners' components. -// Another observation is that if all four corners are the same, every cell -// ought be the exact same color. From this arises the observation that a -// perimeter element is not affected by the other three sides: -// -// a corner element is defined by itself -// a perimeter element is defined by the two points on its side -// an internal element is defined by all four points -// -// 2D equation of state: solve for each quadrant's contribution (min 2x2): -// -// X' = (xlen - 1) - X -// Y' = (ylen - 1) - Y -// TLC: X' * Y' * TL -// TRC: X * Y' * TR -// BLC: X' * Y * BL -// BRC: X * Y * BR -// steps: (xlen - 1) * (ylen - 1) [maximum steps away from origin] -// -// Then add TLC + TRC + BLC + BRC + steps / 2, and divide by steps (the -// steps / 2 is to work around truncate-towards-zero). -static int -calc_gradient_component(unsigned tl, unsigned tr, unsigned bl, unsigned br, - int y, int x, int ylen, int xlen){ - assert(y >= 0); - assert(y < ylen); - assert(x >= 0); - assert(x < xlen); - const int avm = (ylen - 1) - y; - const int ahm = (xlen - 1) - x; - if(xlen < 2){ - if(ylen < 2){ - return tl; - } - return (tl * avm + bl * y) / (ylen - 1); - } - if(ylen < 2){ - return (tl * ahm + tr * x) / (xlen - 1); - } - const int tlc = ahm * avm * tl; - const int blc = ahm * y * bl; - const int trc = x * avm * tr; - const int brc = y * x * br; - const int divisor = (ylen - 1) * (xlen - 1); - return ((tlc + blc + trc + brc) + divisor / 2) / divisor; -} - -// calculate one of the channels of a gradient at a particular point. -static inline uint32_t -calc_gradient_channel(uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr, - int y, int x, int ylen, int xlen){ - uint32_t chan = 0; - channel_set_rgb_clipped(&chan, - calc_gradient_component(channel_r(ul), channel_r(ur), - channel_r(ll), channel_r(lr), - y, x, ylen, xlen), - calc_gradient_component(channel_g(ul), channel_g(ur), - channel_g(ll), channel_g(lr), - y, x, ylen, xlen), - calc_gradient_component(channel_b(ul), channel_b(ur), - channel_b(ll), channel_b(lr), - y, x, ylen, xlen)); - channel_set_alpha(&chan, channel_alpha(ul)); // precondition: all αs are equal - return chan; -} - -// calculate both channels of a gradient at a particular point, storing them -// into `c`->channels. x and y ought be the location within the gradient. -static inline void -calc_gradient_channels(cell* c, uint64_t ul, uint64_t ur, uint64_t ll, - uint64_t lr, int y, int x, int ylen, int xlen){ - if(!channels_fg_default_p(ul)){ - cell_set_fchannel(c, calc_gradient_channel(channels_fchannel(ul), - channels_fchannel(ur), - channels_fchannel(ll), - channels_fchannel(lr), - y, x, ylen, xlen)); - }else{ - cell_set_fg_default(c); - } - if(!channels_bg_default_p(ul)){ - cell_set_bchannel(c, calc_gradient_channel(channels_bchannel(ul), - channels_bchannel(ur), - channels_bchannel(ll), - channels_bchannel(lr), - y, x, ylen, xlen)); - }else{ - cell_set_bg_default(c); - } -} - static bool check_gradient_channel_args(uint32_t ul, uint32_t ur, uint32_t bl, uint32_t br){ if(channel_default_p(ul) || channel_default_p(ur) || @@ -307,7 +214,8 @@ int ncplane_gradient(ncplane* n, const char* egc, uint32_t attrword, return -1; } targc->attrword = attrword; - calc_gradient_channels(targc, ul, ur, bl, br, y - yoff, x - xoff, ylen, xlen); + calc_gradient_channels(&targc->channels, ul, ur, bl, br, + y - yoff, x - xoff, ylen, xlen); ++total; } } @@ -340,7 +248,8 @@ int ncplane_stain(struct ncplane* n, int ystop, int xstop, for(int y = yoff ; y <= ystop ; ++y){ for(int x = xoff ; x <= xstop ; ++x){ cell* targc = ncplane_cell_ref_yx(n, y, x); - calc_gradient_channels(targc, tl, tr, bl, br, y - yoff, x - xoff, ylen, xlen); + calc_gradient_channels(&targc->channels, tl, tr, bl, br, + y - yoff, x - xoff, ylen, xlen); ++total; } } diff --git a/src/lib/internal.h b/src/lib/internal.h index 9f9c0c940..da38b8f41 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -663,6 +663,102 @@ ncplane_center(const ncplane* n, int* RESTRICT y, int* RESTRICT x){ int ncvisual_bounding_box(const struct ncvisual* ncv, int* leny, int* lenx, int* offy, int* offx); +// Our gradient is a 2d lerp among the four corners of the region. We start +// with the observation that each corner ought be its exact specified corner, +// and the middle ought be the exact average of all four corners' components. +// Another observation is that if all four corners are the same, every cell +// ought be the exact same color. From this arises the observation that a +// perimeter element is not affected by the other three sides: +// +// a corner element is defined by itself +// a perimeter element is defined by the two points on its side +// an internal element is defined by all four points +// +// 2D equation of state: solve for each quadrant's contribution (min 2x2): +// +// X' = (xlen - 1) - X +// Y' = (ylen - 1) - Y +// TLC: X' * Y' * TL +// TRC: X * Y' * TR +// BLC: X' * Y * BL +// BRC: X * Y * BR +// steps: (xlen - 1) * (ylen - 1) [maximum steps away from origin] +// +// Then add TLC + TRC + BLC + BRC + steps / 2, and divide by steps (the +// steps / 2 is to work around truncate-towards-zero). +static int +calc_gradient_component(unsigned tl, unsigned tr, unsigned bl, unsigned br, + int y, int x, int ylen, int xlen){ + assert(y >= 0); + assert(y < ylen); + assert(x >= 0); + assert(x < xlen); + const int avm = (ylen - 1) - y; + const int ahm = (xlen - 1) - x; + if(xlen < 2){ + if(ylen < 2){ + return tl; + } + return (tl * avm + bl * y) / (ylen - 1); + } + if(ylen < 2){ + return (tl * ahm + tr * x) / (xlen - 1); + } + const int tlc = ahm * avm * tl; + const int blc = ahm * y * bl; + const int trc = x * avm * tr; + const int brc = y * x * br; + const int divisor = (ylen - 1) * (xlen - 1); + return ((tlc + blc + trc + brc) + divisor / 2) / divisor; +} + +// calculate one of the channels of a gradient at a particular point. +static inline uint32_t +calc_gradient_channel(uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr, + int y, int x, int ylen, int xlen){ + uint32_t chan = 0; + channel_set_rgb_clipped(&chan, + calc_gradient_component(channel_r(ul), channel_r(ur), + channel_r(ll), channel_r(lr), + y, x, ylen, xlen), + calc_gradient_component(channel_g(ul), channel_g(ur), + channel_g(ll), channel_g(lr), + y, x, ylen, xlen), + calc_gradient_component(channel_b(ul), channel_b(ur), + channel_b(ll), channel_b(lr), + y, x, ylen, xlen)); + channel_set_alpha(&chan, channel_alpha(ul)); // precondition: all αs are equal + return chan; +} + +// calculate both channels of a gradient at a particular point, storing them +// into `channels'. x and y ought be the location within the gradient. +static inline void +calc_gradient_channels(uint64_t* channels, uint64_t ul, uint64_t ur, + uint64_t ll, uint64_t lr, int y, int x, + int ylen, int xlen){ + if(!channels_fg_default_p(ul)){ + channels_set_fchannel(channels, + calc_gradient_channel(channels_fchannel(ul), + channels_fchannel(ur), + channels_fchannel(ll), + channels_fchannel(lr), + y, x, ylen, xlen)); + }else{ + channels_set_fg_default(channels); + } + if(!channels_bg_default_p(ul)){ + channels_set_bchannel(channels, + calc_gradient_channel(channels_bchannel(ul), + channels_bchannel(ur), + channels_bchannel(ll), + channels_bchannel(lr), + y, x, ylen, xlen)); + }else{ + channels_set_bg_default(channels); + } +} + #ifdef __cplusplus } #endif diff --git a/src/lib/plot.h b/src/lib/plot.h index c47ff6a1a..d82e87b0b 100644 --- a/src/lib/plot.h +++ b/src/lib/plot.h @@ -24,7 +24,19 @@ class ncppplot { if(maxy < miny){ return false; } - auto bset = lookup_blitset(opts && opts->gridtype ? opts->gridtype : NCBLIT_8x1); + ncblitter_e blitter = opts ? opts->gridtype : NCBLIT_DEFAULT; + if(blitter == NCBLIT_DEFAULT){ + if(notcurses_canutf8(ncplane_notcurses(n))){ + blitter = NCBLIT_8x1; + }else{ + blitter = NCBLIT_1x1; + } + } + bool degrade_blitter = true; + if(opts && !(opts->flags & NCPLOT_OPTIONS_MAYDEGRADE)){ + degrade_blitter = false; + } + auto bset = lookup_blitset(ncplane_notcurses(n), blitter, degrade_blitter); if(bset == nullptr){ return false; } @@ -76,38 +88,11 @@ class ncppplot { return false; } - // Add to or set the value corresponding to this x. If x is beyond the current - // x window, the x window is advanced to include x, and values passing beyond - // the window are lost. The first call will place the initial window. The plot - // will be redrawn, but notcurses_render() is not called. - int add_sample(uint64_t x, T y){ - if(window_slide(x)){ - return -1; - } - update_sample(x, y, false); - if(update_domain(x)){ - return -1; - } - return redraw_plot(); - } - - int set_sample(uint64_t x, T y){ - if(window_slide(x)){ - return -1; - } - update_sample(x, y, true); - if(update_domain(x)){ - return -1; - } - return redraw_plot(); - } - void destroy(){ free(slots); } - // FIXME everything below here ought be private, but it busts unit tests - int redraw_plot(){ + int redraw_plot() { ncplane_erase(ncp); const int scale = bset->width; int dimy, dimx; @@ -122,7 +107,7 @@ class ncppplot { if(exponentiali){ if(maxy > miny){ interval = pow(maxy - miny, (double)1 / (dimy * states)); -//fprintf(stderr, "miny: %ju maxy: %ju dimy: %d states: %zu\n", miny, maxy, dimy, states); + //fprintf(stderr, "miny: %ju maxy: %ju dimy: %d states: %zu\n", miny, maxy, dimy, states); }else{ interval = 0; } @@ -158,6 +143,8 @@ class ncppplot { #define MAXWIDTH 2 int idx = slotstart; // idx holds the real slot index; we move backwards for(int x = finalx ; x >= startx ; --x){ + // a single column might correspond to more than 1 ('scale', up to + // MAXWIDTH) slot's worth of samples. prepare the working gval set. T gvals[MAXWIDTH]; // load it retaining the same ordering we have in the actual array for(int i = scale - 1 ; i >= 0 ; --i){ @@ -179,6 +166,10 @@ class ncppplot { double intervalbase = miny; const wchar_t* egc = bset->egcs; for(int y = 0 ; y < dimy ; ++y){ + uint64_t channels = 0; + calc_gradient_channels(&channels, maxchannel, maxchannel, + minchannel, minchannel, y, x, dimy, dimx); + ncplane_set_channels(ncp, channels); size_t egcidx = 0, sumidx = 0; // if we've got at least one interval's worth on the number of positions // times the number of intervals per position plus the starting offset, @@ -206,10 +197,26 @@ class ncppplot { egcidx = 0; } } - if(sumidx){ + // if we're not UTF8, we can only arrive here via NCBLIT_1x1 (otherwise + // we would have errored out during construction). even then, however, + // we need handle ASCII differently, since it can't print full block. + // in ASCII mode, egcidx != means swap colors and use space. + if(notcurses_canutf8(ncplane_notcurses(ncp)) || !sumidx){ if(ncplane_putwc_yx(ncp, dimy - y - 1, x, egc[sumidx]) <= 0){ return -1; } + }else{ + const uint64_t swapbg = channels_bchannel(channels); + const uint64_t swapfg = channels_fchannel(channels); + channels_set_bchannel(&channels, swapfg); + channels_set_fchannel(&channels, swapbg); + ncplane_set_channels(ncp, channels); + if(ncplane_putsimple_yx(ncp, dimy - y - 1, x, ' ') <= 0){ + return -1; + } + channels_set_bchannel(&channels, swapbg); + channels_set_fchannel(&channels, swapfg); + ncplane_set_channels(ncp, channels); } if(done){ break; @@ -224,11 +231,33 @@ class ncppplot { if(ncplane_cursor_move_yx(ncp, 0, 0)){ return -1; } - if(ncplane_stain(ncp, dimy - 1, dimx - 1, maxchannel, maxchannel, - minchannel, minchannel) <= 0){ + return 0; + } + + // Add to or set the value corresponding to this x. If x is beyond the current + // x window, the x window is advanced to include x, and values passing beyond + // the window are lost. The first call will place the initial window. The plot + // will be redrawn, but notcurses_render() is not called. + int add_sample(uint64_t x, T y) { + if(window_slide(x)){ return -1; } - return 0; + update_sample(x, y, false); + if(update_domain(x)){ + return -1; + } + return redraw_plot(); + } + + int set_sample(uint64_t x, T y) { + if(window_slide(x)){ + return -1; + } + update_sample(x, y, true); + if(update_domain(x)){ + return -1; + } + return redraw_plot(); } // if we're doing domain detection, update the domain to reflect the value we diff --git a/src/lib/visual.cpp b/src/lib/visual.cpp index b0ca45d07..fdddf9a2d 100644 --- a/src/lib/visual.cpp +++ b/src/lib/visual.cpp @@ -41,7 +41,7 @@ auto ncvisual_geom(const notcurses* nc, const ncvisual* n, ncblitter_e blitter, if(blitter == NCBLIT_DEFAULT){ blitter = ncvisual_default_blitter(nc); } - const struct blitset* bset = lookup_blitset(blitter); + const struct blitset* bset = lookup_blitset(nc, blitter, false); if(!bset){ return -1; } @@ -65,10 +65,11 @@ auto ncvisual_geom(const notcurses* nc, const ncvisual* n, ncblitter_e blitter, static const struct blitset* rgba_blitter(const notcurses* nc, const struct ncvisual_options* opts){ const struct blitset* bset; - if(opts && opts->blitter){ - bset = lookup_blitset(opts->blitter); + const bool maydegrade = !opts || (opts->flags & NCVISUAL_OPTIONS_MAYDEGRADE); + if(opts && opts->blitter != NCBLIT_DEFAULT){ + bset = lookup_blitset(nc, opts->blitter, maydegrade); }else{ - bset = lookup_blitset(ncvisual_default_blitter(nc)); + bset = lookup_blitset(nc, ncvisual_default_blitter(nc), maydegrade); } if(bset && !bset->blit){ // FIXME remove this once all blitters are enabled bset = nullptr; @@ -376,7 +377,7 @@ auto ncvisual_from_bgra(const void* bgra, int rows, int rowstride, auto ncvisual_render(notcurses* nc, ncvisual* ncv, const struct ncvisual_options* vopts) -> ncplane* { - if(vopts && vopts->flags){ + if(vopts && vopts->flags > NCVISUAL_OPTIONS_MAYDEGRADE){ return nullptr; } int lenx = vopts ? vopts->lenx : 0;