diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e6d89f8..ae8722c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,18 @@ rearrangements of Notcurses. * 1.3.4 (not yet released) * `notcurses_lex_margins()` has been added to lex margins expressed in either - of two canonical formats. Hopefully this will lead to more programs - supporting margins. + of two canonical formats. Hopefully this will lead to more programs + supporting margins. * `ncvisual_open_plane()` has been renamed `ncvisual_from_file()`. The former - has been retained as a deprecated alias. It will be removed by 1.6/2.0. + has been retained as a deprecated alias. It will be removed by 1.6/2.0. + * `ncvisual_from_rgba()` and `ncvisual_from_bgra()` have been added to + support creation of `ncvisual`s from memory, requiring no file. + * `ncvisual_rotate_cw()` and `ncvisual_rotate_ccw()` have been added, having + the same semantics as `ncplane_rotate_cw()` and `ncplane_rotate_ccw()`. + They will likely be augmented in the future to support arbitrary rotations. + * `ncvisual_from_plane()` has been added to support "promotion" of an + `ncplane` to an `ncvisual`. The source plane may contain only spaces, + half blocks, and full blocks. * 1.3.3 (2020-04-26) * The `ncdplot` type has been added for plots based on `double`s rather than diff --git a/USAGE.md b/USAGE.md index 5a05d5aa4..5968bcbd1 100644 --- a/USAGE.md +++ b/USAGE.md @@ -2437,8 +2437,8 @@ struct ncplane* ncvisual_plane(struct ncvisual* ncv); char* ncvisual_subtitle(const struct ncvisual* ncv); // Rotate the visual π/2 radians clockwise or counterclockwise. -int ncplane_rotate_cw(struct ncplane* n); -int ncplane_rotate_ccw(struct ncplane* n); +int ncvisual_rotate_cw(struct ncvisual* n); +int ncvisual_rotate_ccw(struct ncvisual* n); ``` It is also possible to seed an `ncvisual` directly from memory, without involving @@ -2447,10 +2447,10 @@ a file. Both RGBA and BGRA 8bpc arrangements can be used. ```c // Prepare an ncvisual, and its underlying plane, based off RGBA content in // memory at 'rgba'. 'rgba' must be a flat array of 32-bit 8bpc RGBA pixels. -// These must be arranged in 'rowstride' * 4b lines, where the first 'cols' -// * 4b are actual data. There must be 'rows' lines. The total size of 'rgba' -// must thus be at least (rows * rowstride * 4) bytes, of which (rows * cols -// * 4) bytes are actual data. The resulting plane will be 'rows' / 2 x 'cols'. +// These must be arranged in 'rowstride' lines, where the first 'cols' * 4b +// are actual data. There must be 'rows' lines. The total size of 'rgba' +// must thus be at least (rows * rowstride) bytes, of which (rows * cols * 4) +// bytes are actual data. The resulting plane will be ceil('rows'/2)x'cols'. ncvisual* ncvisual_from_rgba(notcurses* nc, const void* rgba, int rows, int rowstride, int cols); // ncvisual_from_rgba(), but for BGRA. diff --git a/doc/man/man3/notcurses_plane.3.md b/doc/man/man3/notcurses_plane.3.md index 146e449ee..b3b807354 100644 --- a/doc/man/man3/notcurses_plane.3.md +++ b/doc/man/man3/notcurses_plane.3.md @@ -134,6 +134,10 @@ notcurses_plane - operations on ncplanes **bool ncplane_set_scrolling(struct ncplane* n, bool scrollp);** +**int ncplane_rotate_cw(struct ncplane* n);** + +**int ncplane_rotate_ccw(struct ncplane* n);** + ## DESCRIPTION Ncplanes are the fundamental drawing object of notcurses. All output functions diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md index fd1ba8c70..18a5c9d20 100644 --- a/doc/man/man3/notcurses_visual.3.md +++ b/doc/man/man3/notcurses_visual.3.md @@ -46,9 +46,9 @@ typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*); **struct ncplane* ncvisual_plane(struct ncvisual* ncv);** -**int ncplane_rotate_cw(struct ncplane* n);** +**int ncvisual_rotate_cw(struct ncvisual* n);** -**int ncplane_rotate_ccw(struct ncplane* n);** +**int ncvisual_rotate_ccw(struct ncvisual* n);** **char* ncvisual_subtitle(const struct ncvisual* ncv);** @@ -68,15 +68,15 @@ It is possible to create an **ncvisual** from memory, rather than from a disk, but only using raw RGBA/BGRA 8bpc content. For RGBA, use **ncvisual_from_rgba**. For BGRA, use **ncvisual_from_bgra**. Both require a number of **rows**, a number of image columns **cols**, and a virtual row -length of **rowstride** columns. The input data must provide 32 bits per -pixel, and thus must be at least **rowstride** * **rows** * 4 bytes, of +length of **rowstride** / 4 columns. The input data must provide 32 bits per +pixel, and thus must be at least **rowstride** * **rows** bytes, of which a **cols** * **rows** * 4-byte subset is used. It is not possible to **mmap(2)** an image file and use it directly--decompressed, decoded data -is necessary. The resulting plane will be **rows** / 2 rows, and **cols** +is necessary. The resulting plane will be ceil(**rows**/2) rows, and **cols** columns. It will not be necessary to call **ncvisual_decode**, but it is still necessary to call **ncvisual_render**. -Both **ncplane_rotate_cw** and **ncplane_rotate_ccw** execute a rotation of +Both **ncvisual_rotate_cw** and **ncvisual_rotate_ccw** execute a rotation of π/2 radians, in the clockwise or counterclockwise direction respectively. **ncvisual_subtitle** will return a UTF-8-encoded subtitle corresponding to diff --git a/include/ncpp/Plane.hh b/include/ncpp/Plane.hh index 67c6cca5b..c8d6d329f 100644 --- a/include/ncpp/Plane.hh +++ b/include/ncpp/Plane.hh @@ -979,6 +979,16 @@ namespace ncpp return error_guard (ncplane_translate_abs (plane, y, x), false); } + bool rotate_cw () const NOEXCEPT_MAYBE + { + return error_guard (ncplane_rotate_cw (plane), -1); + } + + bool rotate_ccw () const noexcept + { + return error_guard (ncplane_rotate_ccw (plane), -1); + } + // Upstream call doesn't take ncplane* but we put it here for parity with has_no_background below bool has_no_foreground (Cell &cell) const noexcept { diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index d2f3fac79..ed76961bb 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -1129,6 +1129,13 @@ ncplane_move_below(struct ncplane* n, struct ncplane* below){ // Return the plane below this one, or NULL if this is at the bottom. API struct ncplane* ncplane_below(struct ncplane* n); +// Rotate the plane π/2 radians clockwise or counterclockwise. This cannot +// be performed on arbitrary planes, because glyphs cannot be arbitrarily +// rotated. The glyphs which can be rotated are limited: line-drawing +// characters, spaces, half blocks, and full blocks. +API int ncplane_rotate_cw(struct ncplane* n); +API int ncplane_rotate_ccw(struct ncplane* n); + // Rotate the visual π/2 radians clockwise or counterclockwise. This cannot // be performed on arbitrary planes, because glyphs cannot be arbitrarily rotated. API int ncvisual_rotate_cw(struct ncvisual* n); @@ -2065,10 +2072,10 @@ ncvisual_open_plane(struct notcurses* nc, const char* file, nc_err_e* ncerr, // Prepare an ncvisual, and its underlying plane, based off RGBA content in // memory at 'rgba'. 'rgba' must be a flat array of 32-bit 8bpc RGBA pixels. -// These must be arranged in 'rowstride' * 4b lines, where the first 'cols' -// * 4b are actual data. There must be 'rows' lines. The total size of 'rgba' -// must thus be at least (rows * rowstride * 4) bytes, of which (rows * cols -// * 4) bytes are actual data. The resulting plane will be 'rows' / 2 x 'cols'. +// These must be arranged in 'rowstride' lines, where the first 'cols' * 4b +// are actual data. There must be 'rows' lines. The total size of 'rgba' +// must thus be at least (rows * rowstride) bytes, of which (rows * cols * 4) +// bytes are actual data. The resulting plane will be ceil('rows'/2)x'cols'. API struct ncvisual* ncvisual_from_rgba(struct notcurses* nc, const void* rgba, int rows, int rowstride, int cols); @@ -2709,7 +2716,9 @@ typedef struct ncfdplane_options { // ownership of the file descriptor, which will be closed in ncfdplane_destroy(). API struct ncfdplane* ncfdplane_create(struct ncplane* n, const ncfdplane_options* opts, int fd, ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn); + API struct ncplane* ncfdplane_plane(struct ncfdplane* n); + API int ncfdplane_destroy(struct ncfdplane* n); typedef struct ncsubproc_options { @@ -2727,7 +2736,9 @@ API struct ncsubproc* ncsubproc_createvp(struct ncplane* n, const ncsubproc_opti API struct ncsubproc* ncsubproc_createvpe(struct ncplane* n, const ncsubproc_options* opts, const char* bin, char* const arg[], char* const env[], ncfdplane_callback cbfxn, ncfdplane_done_cb donecbfxn); + API struct ncplane* ncsubproc_plane(struct ncsubproc* n); + API int ncsubproc_destroy(struct ncsubproc* n); // Draw a QR code at the current position on the plane. If there is insufficient diff --git a/python/src/notcurses/build_notcurses.py b/python/src/notcurses/build_notcurses.py index 28e7b192a..559145a26 100644 --- a/python/src/notcurses/build_notcurses.py +++ b/python/src/notcurses/build_notcurses.py @@ -427,6 +427,8 @@ int ncplane_putegc_stainable(struct ncplane* n, const char* gclust, int* sbytes) int ncplane_putwegc_stainable(struct ncplane* n, const wchar_t* gclust, int* sbytes); int ncplane_format(struct ncplane* n, int ystop, int xstop, uint32_t attrword); int ncplane_stain(struct ncplane* n, int ystop, int xstop, uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr); +int ncplane_rotate_cw(struct ncplane* n); +int ncplane_rotate_ccw(struct ncplane* n); int ncvisual_rotate_cw(struct ncvisual* n); int ncvisual_rotate_ccw(struct ncvisual* n); void ncplane_translate(const struct ncplane* src, const struct ncplane* dst, int* y, int* x); diff --git a/src/lib/fill.c b/src/lib/fill.c index bee17ab77..41b045306 100644 --- a/src/lib/fill.c +++ b/src/lib/fill.c @@ -373,21 +373,6 @@ int ncplane_format(struct ncplane* n, int ystop, int xstop, uint32_t attrword){ return total; } -// generate a temporary plane that can hold the contents of n, rotated 90° -static ncplane* rotate_plane(const ncplane* n){ - int absy, absx; - ncplane_yx(n, &absy, &absx); - int dimy, dimx; - ncplane_dim_yx(n, &dimy, &dimx); - if(dimx % 2 != 0){ - return NULL; - } - const int newy = dimx / 2; - const int newx = dimy * 2; - ncplane* newp = ncplane_new(n->nc, newy, newx, absy, absx, n->userptr); - return newp; -} - // if we're a lower block, reverse the channels. if we're a space, set both to // the background. if we're a full block, set both to the foreground. static void @@ -491,7 +476,8 @@ rotate_2x1_cw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx return 0; } -int rotate_2x1_ccw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx){ +static int +rotate_2x1_ccw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx){ cell c1 = CELL_TRIVIAL_INITIALIZER; cell c2 = CELL_TRIVIAL_INITIALIZER; if(ncplane_at_yx_cell(src, srcy, srcx, &c1) < 0){ diff --git a/src/lib/internal.h b/src/lib/internal.h index 80a24ac9a..c40c7038a 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -12,12 +12,8 @@ extern "C" { #include #include #include -#include -#include -#include #include #include -#include #else #ifdef USE_OIIO const char* oiio_version(void); @@ -638,6 +634,20 @@ enforce_utf8(void){ struct ncvisual* ncvisual_create(float timescale); +static inline void* +memdup(const void* src, size_t len){ + void* ret = malloc(len); + if(ret){ + memcpy(ret, src, len); + } + return ret; +} + +// generate a temporary plane that can hold the contents of n, rotated 90° +ncplane* rotate_plane(const ncplane* n); + +void* bgra_to_rgba(const void* data, int rows, int rowstride, int cols); + #ifdef __cplusplus } #endif diff --git a/src/lib/libav.c b/src/lib/libav.c deleted file mode 100644 index 5add2bbfb..000000000 --- a/src/lib/libav.c +++ /dev/null @@ -1,627 +0,0 @@ -#include -#include "version.h" -#include "internal.h" - -struct AVFormatContext; -struct AVCodecContext; -struct AVFrame; -struct AVCodec; -struct AVCodecParameters; -struct AVPacket; - -typedef struct ncvisual { - int packet_outstanding; - int dstwidth, dstheight; - int stream_index; // match against this following av_read_frame() - int sub_stream_index; // subtitle stream index, can be < 0 if no subtitles - float timescale; // scale frame duration by this value - ncplane* ncp; - // if we're creating the plane based off the first frame's dimensions, these - // describe where the plane ought be placed, and how it ought be sized. this - // path sets ncobj. ncvisual_destroy() ought in that case kill the ncplane. - int placex, placey; - ncscale_e style; // none, scale, or stretch - struct notcurses* ncobj; // set iff this ncvisual "owns" its ncplane -#ifdef USE_FFMPEG - struct AVFormatContext* fmtctx; - struct AVCodecContext* codecctx; // video codec context - struct AVCodecContext* subtcodecctx; // subtitle codec context - struct AVFrame* frame; - struct AVFrame* oframe; - struct AVCodec* codec; - struct AVCodecParameters* cparams; - struct AVCodec* subtcodec; - struct AVPacket* packet; - struct SwsContext* swsctx; - AVSubtitle subtitle; -#endif -} ncvisual; - -#ifdef USE_FFMPEG -ncplane* ncvisual_plane(ncvisual* ncv){ - return ncv->ncp; -} - -ncvisual* ncvisual_create(float timescale){ - ncvisual* ret = malloc(sizeof(*ret)); - if(ret == NULL){ - return NULL; - } - memset(ret, 0, sizeof(*ret)); - ret->timescale = timescale; - return ret; -} - -void ncvisual_destroy(ncvisual* ncv){ - if(ncv){ - avcodec_close(ncv->codecctx); - avcodec_free_context(&ncv->codecctx); - av_frame_free(&ncv->frame); - av_freep(&ncv->oframe); - //avcodec_parameters_free(&ncv->cparams); - sws_freeContext(ncv->swsctx); - av_packet_free(&ncv->packet); - avformat_close_input(&ncv->fmtctx); - avsubtitle_free(&ncv->subtitle); - if(ncv->ncobj && ncv->ncp){ - ncplane_destroy(ncv->ncp); - } - free(ncv); - } -} - -bool notcurses_canopen(const notcurses* nc __attribute__ ((unused))){ - return true; -} - -/* static void -print_frame_summary(const AVCodecContext* cctx, const AVFrame* f){ - char pfmt[128]; - av_get_pix_fmt_string(pfmt, sizeof(pfmt), f->format); - fprintf(stderr, "Frame %05d (%d? %d?) %dx%d pfmt %d (%s)\n", - cctx->frame_number, - f->coded_picture_number, - f->display_picture_number, - f->width, f->height, - f->format, pfmt); - fprintf(stderr, " Data (%d):", AV_NUM_DATA_POINTERS); - int i; - for(i = 0 ; i < AV_NUM_DATA_POINTERS ; ++i){ - fprintf(stderr, " %p", f->data[i]); - } - fprintf(stderr, "\n Linesizes:"); - for(i = 0 ; i < AV_NUM_DATA_POINTERS ; ++i){ - fprintf(stderr, " %d", f->linesize[i]); - } - if(f->sample_aspect_ratio.num == 0 && f->sample_aspect_ratio.den == 1){ - fprintf(stderr, "\n Aspect ratio unknown"); - }else{ - fprintf(stderr, "\n Aspect ratio %d:%d", f->sample_aspect_ratio.num, f->sample_aspect_ratio.den); - } - if(f->interlaced_frame){ - fprintf(stderr, " [ILaced]"); - } - if(f->palette_has_changed){ - fprintf(stderr, " [NewPal]"); - } - fprintf(stderr, " PTS %ld Flags: 0x%04x\n", f->pts, f->flags); - fprintf(stderr, " %lums@%lums (%skeyframe) qual: %d\n", - f->pkt_duration, // FIXME in 'time_base' units - f->best_effort_timestamp, - f->key_frame ? "" : "non-", - f->quality); -}*/ - -static char* -deass(const char* ass){ - // SSA/ASS formats: - // Dialogue: Marked=0,0:02:40.65,0:02:41.79,Wolf main,Cher,0000,0000,0000,,Et les enregistrements de ses ondes delta ? - // FIXME more - if(strncmp(ass, "Dialogue:", strlen("Dialogue:"))){ - return NULL; - } - const char* delim = strchr(ass, ','); - int commas = 0; // we want 8 - while(delim && commas < 8){ - delim = strchr(delim + 1, ','); - ++commas; - } - if(!delim){ - return NULL; - } - // handle ASS syntax...\i0, \b0, etc. - char* dup = strdup(delim + 1); - char* c = dup; - while(*c){ - if(*c == '\\'){ - *c = ' '; - ++c; - if(*c){ - *c = ' ';; - } - } - ++c; - } - return dup; -} - -char* ncvisual_subtitle(const ncvisual* ncv){ - for(unsigned i = 0 ; i < ncv->subtitle.num_rects ; ++i){ - const AVSubtitleRect* rect = ncv->subtitle.rects[i]; - if(rect->type == SUBTITLE_ASS){ - return deass(rect->ass); - }else if(rect->type == SUBTITLE_TEXT) {; - return strdup(rect->text); - } - } - return NULL; -} - -static nc_err_e -averr2ncerr(int averr){ - // FIXME need to map averror codes to ncerrors -//fprintf(stderr, "AVERR: %d/%x %d/%x\n", averr, averr, -averr, -averr); - return -averr; -} - -nc_err_e ncvisual_decode(ncvisual* nc){ - bool have_frame = false; - bool unref = false; - av_freep(&nc->oframe->data[0]); - do{ - do{ - if(nc->packet_outstanding){ - break; - } - if(unref){ - av_packet_unref(nc->packet); - } - int averr; - if((averr = av_read_frame(nc->fmtctx, nc->packet)) < 0){ - /*if(averr != AVERROR_EOF){ - fprintf(stderr, "Error reading frame info (%s)\n", av_err2str(*averr)); - }*/ - return averr2ncerr(averr); - } - unref = true; - if(nc->packet->stream_index == nc->sub_stream_index){ - int result = 0, ret; - ret = avcodec_decode_subtitle2(nc->subtcodecctx, &nc->subtitle, &result, nc->packet); - if(ret >= 0 && result){ - // FIXME? - } - } - }while(nc->packet->stream_index != nc->stream_index); - ++nc->packet_outstanding; - if(avcodec_send_packet(nc->codecctx, nc->packet) < 0){ - //fprintf(stderr, "Error processing AVPacket (%s)\n", av_err2str(*ncerr)); - return ncvisual_decode(nc); - } - --nc->packet_outstanding; - av_packet_unref(nc->packet); - int averr = avcodec_receive_frame(nc->codecctx, nc->frame); - if(averr >= 0){ - have_frame = true; - }else if(averr == AVERROR(EAGAIN) || averr == AVERROR_EOF){ - have_frame = false; - }else if(averr < 0){ - //fprintf(stderr, "Error decoding AVPacket (%s)\n", av_err2str(averr)); - return averr2ncerr(averr); - } - }while(!have_frame); -//print_frame_summary(nc->codecctx, nc->frame); -#define IMGALLOCALIGN 32 - int rows, cols; - if(nc->ncp == NULL){ // create plane - if(nc->style == NCSCALE_NONE){ - rows = nc->frame->height / 2; - cols = nc->frame->width; - }else{ // FIXME differentiate between scale/stretch - notcurses_term_dim_yx(nc->ncobj, &rows, &cols); - if(nc->placey >= rows || nc->placex >= cols){ - return NCERR_DECODE; - } - rows -= nc->placey; - cols -= nc->placex; - } - nc->dstwidth = cols; - nc->dstheight = rows * 2; - nc->ncp = ncplane_new(nc->ncobj, rows, cols, nc->placey, nc->placex, NULL); - nc->placey = 0; - nc->placex = 0; - if(nc->ncp == NULL){ - return NCERR_NOMEM; - } - }else{ // check for resize - ncplane_dim_yx(nc->ncp, &rows, &cols); - if(rows != nc->dstheight / 2 || cols != nc->dstwidth){ - sws_freeContext(nc->swsctx); - nc->swsctx = NULL; - nc->dstheight = rows * 2; - nc->dstwidth = cols; - } - } - const int targformat = AV_PIX_FMT_RGBA; - nc->swsctx = sws_getCachedContext(nc->swsctx, - nc->frame->width, - nc->frame->height, - nc->frame->format, - nc->dstwidth, - nc->dstheight, - targformat, - SWS_LANCZOS, - NULL, NULL, NULL); - if(nc->swsctx == NULL){ - //fprintf(stderr, "Error retrieving swsctx\n"); - return NCERR_DECODE; - } - memcpy(nc->oframe, nc->frame, sizeof(*nc->oframe)); - nc->oframe->format = targformat; - nc->oframe->width = nc->dstwidth; - nc->oframe->height = nc->dstheight; - int size = av_image_alloc(nc->oframe->data, nc->oframe->linesize, - nc->oframe->width, nc->oframe->height, - nc->oframe->format, IMGALLOCALIGN); - if(size < 0){ - //fprintf(stderr, "Error allocating visual data (%s)\n", av_err2str(size)); - return NCERR_NOMEM; - } - int height = sws_scale(nc->swsctx, (const uint8_t* const*)nc->frame->data, - nc->frame->linesize, 0, - nc->frame->height, nc->oframe->data, nc->oframe->linesize); - if(height < 0){ - //fprintf(stderr, "Error applying scaling (%s)\n", av_err2str(height)); - return NCERR_NOMEM; - } -//print_frame_summary(nc->codecctx, nc->oframe); -#undef IMGALLOCALIGN - av_frame_unref(nc->frame); - return NCERR_SUCCESS; -} - -static ncvisual* -ncvisual_open(const char* filename, nc_err_e* ncerr){ - ncvisual* ncv = ncvisual_create(1); - if(ncv == NULL){ - // fprintf(stderr, "Couldn't create %s (%s)\n", filename, strerror(errno)); - *ncerr = NCERR_NOMEM; - return NULL; - } - memset(ncv, 0, sizeof(*ncv)); - int averr = avformat_open_input(&ncv->fmtctx, filename, NULL, NULL); - if(averr < 0){ - // fprintf(stderr, "Couldn't open %s (%s)\n", filename, av_err2str(*averr)); - *ncerr = averr2ncerr(averr); - goto err; - } - averr = avformat_find_stream_info(ncv->fmtctx, NULL); - if(averr < 0){ - /*fprintf(stderr, "Error extracting stream info from %s (%s)\n", filename, - av_err2str(*averr));*/ - *ncerr = averr2ncerr(averr); - goto err; - } -//av_dump_format(ncv->fmtctx, 0, filename, false); - if((averr = av_find_best_stream(ncv->fmtctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, &ncv->subtcodec, 0)) >= 0){ - ncv->sub_stream_index = averr; - if((ncv->subtcodecctx = avcodec_alloc_context3(ncv->subtcodec)) == NULL){ - //fprintf(stderr, "Couldn't allocate decoder for %s\n", filename); - *ncerr = NCERR_NOMEM; - goto err; - } - // FIXME do we need avcodec_parameters_to_context() here? - if((averr = avcodec_open2(ncv->subtcodecctx, ncv->subtcodec, NULL)) < 0){ - //fprintf(stderr, "Couldn't open codec for %s (%s)\n", filename, av_err2str(*averr)); - *ncerr = averr2ncerr(averr); - goto err; - } - }else{ - ncv->sub_stream_index = -1; - } - if((ncv->packet = av_packet_alloc()) == NULL){ - // fprintf(stderr, "Couldn't allocate packet for %s\n", filename); - *ncerr = NCERR_NOMEM; - goto err; - } - if((averr = av_find_best_stream(ncv->fmtctx, AVMEDIA_TYPE_VIDEO, -1, -1, &ncv->codec, 0)) < 0){ - // fprintf(stderr, "Couldn't find visuals in %s (%s)\n", filename, av_err2str(*averr)); - *ncerr = averr2ncerr(averr); - goto err; - } - ncv->stream_index = averr; - if(ncv->codec == NULL){ - //fprintf(stderr, "Couldn't find decoder for %s\n", filename); - goto err; - } - AVStream* st = ncv->fmtctx->streams[ncv->stream_index]; - if((ncv->codecctx = avcodec_alloc_context3(ncv->codec)) == NULL){ - //fprintf(stderr, "Couldn't allocate decoder for %s\n", filename); - *ncerr = NCERR_NOMEM; - goto err; - } - if(avcodec_parameters_to_context(ncv->codecctx, st->codecpar) < 0){ - goto err; - } - if((averr = avcodec_open2(ncv->codecctx, ncv->codec, NULL)) < 0){ - //fprintf(stderr, "Couldn't open codec for %s (%s)\n", filename, av_err2str(*averr)); - *ncerr = averr2ncerr(averr); - goto err; - } - /*if((ncv->cparams = avcodec_parameters_alloc()) == NULL){ - //fprintf(stderr, "Couldn't allocate codec params for %s\n", filename); - *averr = NCERR_NOMEM; - goto err; - } - if((*averr = avcodec_parameters_from_context(ncv->cparams, ncv->codecctx)) < 0){ - //fprintf(stderr, "Couldn't get codec params for %s (%s)\n", filename, av_err2str(*averr)); - goto err; - }*/ - if((ncv->frame = av_frame_alloc()) == NULL){ - // fprintf(stderr, "Couldn't allocate frame for %s\n", filename); - *ncerr = NCERR_NOMEM; - goto err; - } - if((ncv->oframe = av_frame_alloc()) == NULL){ - // fprintf(stderr, "Couldn't allocate output frame for %s\n", filename); - *ncerr = NCERR_NOMEM; - goto err; - } - return ncv; - -err: - ncvisual_destroy(ncv); - return NULL; -} - -ncvisual* ncplane_visual_open(ncplane* nc, const char* filename, nc_err_e* ncerr){ - ncvisual* ncv = ncvisual_open(filename, ncerr); - if(ncv == NULL){ - return NULL; - } - ncplane_dim_yx(nc, &ncv->dstheight, &ncv->dstwidth); - ncv->dstheight *= 2; - ncv->ncp = nc; - ncv->style = NCSCALE_STRETCH; - return ncv; -} - -ncvisual* ncvisual_from_file(notcurses* nc, const char* filename, - nc_err_e* ncerr, int y, int x, ncscale_e style){ - ncvisual* ncv = ncvisual_open(filename, ncerr); - if(ncv == NULL){ - return NULL; - } - ncv->placey = y; - ncv->placex = x; - ncv->style = style; - ncv->ncobj = nc; - ncv->ncp = NULL; - return ncv; -} - -int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx){ -//fprintf(stderr, "render %dx%d+%dx%d\n", begy, begx, leny, lenx); - if(begy < 0 || begx < 0 || lenx < -1 || leny < -1){ - return -1; - } - const AVFrame* f = ncv->oframe; - if(f == NULL){ - return -1; - } -//fprintf(stderr, "render %d/%d to %dx%d+%dx%d\n", f->height, f->width, begy, begx, leny, lenx); - if(begx >= f->width || begy >= f->height){ - return -1; - } - if(lenx == -1){ // -1 means "to the end"; use all space available - lenx = f->width - begx; - } - if(leny == -1){ - leny = f->height - begy; - } - if(lenx == 0 || leny == 0){ // no need to draw zero-size object, exit - return 0; - } - if(begx + lenx > f->width || begy + leny > f->height){ - return -1; - } - int dimy, dimx; - ncplane_dim_yx(ncv->ncp, &dimy, &dimx); - ncplane_cursor_move_yx(ncv->ncp, 0, 0); - const int linesize = f->linesize[0]; - void* data = f->data[0]; - // y and x are actual plane coordinates. each row corresponds to two rows of - // the input (scaled) frame (columns are 1:1). we track the row of the - // visual via visy. -//fprintf(stderr, "render: %dx%d:%d+%d of %d/%d -> %dx%d\n", begy, begx, leny, lenx, f->height, f->width, dimy, dimx); - int bpp = av_get_bits_per_pixel(av_pix_fmt_desc_get(f->format)); - if(bpp != 32){ - return -1; - } - int ret = ncblit_rgba(ncv->ncp, ncv->placey, ncv->placex, linesize, data, - begy, begx, leny, lenx); - //av_frame_unref(ncv->oframe); - return ret; -} - -// iterative over the decoded frames, calling streamer() with curry for each. -// frames carry a presentation time relative to the beginning, so we get an -// initial timestamp, and check each frame against the elapsed time to sync -// up playback. -int ncvisual_stream(notcurses* nc, ncvisual* ncv, nc_err_e* ncerr, - float timescale, streamcb streamer, void* curry){ - int frame = 1; - ncv->timescale = timescale; - struct timespec begin; // time we started - clock_gettime(CLOCK_MONOTONIC, &begin); - uint64_t nsbegin = timespec_to_ns(&begin); - bool usets = false; - // each frame has a pkt_duration in milliseconds. keep the aggregate, in case - // we don't have PTS available. - uint64_t sum_duration = 0; - while((*ncerr = ncvisual_decode(ncv)) == NCERR_SUCCESS){ - // codecctx seems to be off by a factor of 2 regularly. instead, go with - // the time_base from the avformatctx. - double tbase = av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base); - int64_t ts = ncv->oframe->best_effort_timestamp; - if(frame == 1 && ts){ - usets = true; - } - if(ncvisual_render(ncv, 0, 0, -1, -1) < 0){ - return -1; - } - if(streamer){ - int r = streamer(nc, ncv, curry); - if(r){ - return r; - } - } - ++frame; - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - uint64_t nsnow = timespec_to_ns(&now); - struct timespec interval; - uint64_t duration = ncv->oframe->pkt_duration * tbase * NANOSECS_IN_SEC; -//fprintf(stderr, "use: %u dur: %ju ts: %ju cctx: %f fctx: %f\n", usets, duration, ts, av_q2d(ncv->codecctx->time_base), av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base)); - sum_duration += (duration * ncv->timescale); - double schedns = nsbegin; - if(usets){ - if(tbase == 0){ - tbase = duration; - } - schedns += ts * (tbase * ncv->timescale) * NANOSECS_IN_SEC; - }else{ - schedns += sum_duration; - } - if(nsnow < schedns){ - ns_to_timespec(schedns - nsnow, &interval); - nanosleep(&interval, NULL); - } - } - if(*ncerr == NCERR_EOF){ - return 0; - } - return -1; -} - -int ncvisual_init(int loglevel){ - av_log_set_level(loglevel); - // FIXME could also use av_log_set_callback() and capture the message... - return 0; -} - -ncvisual* ncvisual_from_rgba(notcurses* nc, const void* rgba, int rows, int rowstride, int cols){ - ncvisual* ncv = ncvisual_create(1); - ncv->placey = 0; - ncv->placex = 0; - ncv->ncobj = nc; - const int targrows = rows / 2 + rows % 2; - ncv->ncp = ncplane_new(nc, targrows, cols, 0, 0, NULL); - if(ncv->ncp == NULL){ - ncvisual_destroy(ncv); - return NULL; - } - // FIXME create frame - return ncv; -} - -ncvisual* ncvisual_from_bgra(notcurses* nc, const void* bgra, int rows, int rowstride, int cols){ - ncvisual* ncv = ncvisual_create(1); - ncv->placey = 0; - ncv->placex = 0; - ncv->ncobj = nc; - const int targrows = rows / 2 + rows % 2; - ncv->ncp = ncplane_new(nc, targrows, cols, 0, 0, NULL); - if(ncv->ncp == NULL){ - ncvisual_destroy(ncv); - return NULL; - } - // FIXME create frame - return ncv; -} -#else // built without ffmpeg -#ifndef USE_OIIO // built without ffmpeg or oiio -bool notcurses_canopen(const notcurses* nc __attribute__ ((unused))){ - return false; -} - -ncplane* ncvisual_plane(ncvisual* ncv){ - return ncv->ncp; -} - -nc_err_e ncvisual_decode(ncvisual* nc){ - (void)nc; - return NCERR_UNIMPLEMENTED; -} - -int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx){ - (void)ncv; - (void)begy; - (void)begx; - (void)leny; - (void)lenx; - return -1; -} - -int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv, nc_err_e* ncerr, - float timespec, streamcb streamer, void* curry){ - (void)nc; - (void)ncv; - (void)ncerr; - (void)timespec; - (void)streamer; - (void)curry; - return -1; -} - -ncvisual* ncplane_visual_open(ncplane* nc, const char* filename, nc_err_e* ncerr){ - (void)nc; - (void)filename; - (void)ncerr; - return NULL; -} - -ncvisual* ncvisual_from_file(notcurses* nc, const char* filename, - nc_err_e* ncerr, int y, int x, ncscale_e style){ - (void)nc; - (void)filename; - (void)ncerr; - (void)y; - (void)x; - (void)style; - return NULL; -} - -char* ncvisual_subtitle(const ncvisual* ncv){ - (void)ncv; - return NULL; -} - -int ncvisual_init(int loglevel){ - (void)loglevel; - return 0; // allow success here -} - -void ncvisual_destroy(ncvisual* ncv){ - assert(!ncv); - (void)ncv; -} - -ncvisual* ncvisual_from_rgba(notcurses* nc, const void* rgba, int rows, int rowstride, int cols){ - (void)nc; - (void)rgba; - (void)rows; - (void)rowstride; - (void)cols; - return NULL; -} - -ncvisual* ncvisual_from_bgra(notcurses* nc, const void* bgra, int rows, int rowstride, int cols){ - (void)nc; - (void)bgra; - (void)rows; - (void)rowstride; - (void)cols; - return NULL; -} -#endif -#endif diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index ab8911aa3..2bc5993cc 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -1950,3 +1950,18 @@ int notcurses_lex_margins(const char* op, notcurses_options* opts){ int notcurses_inputready_fd(notcurses* n){ return fileno(n->ttyinfp); } + +// generate a temporary plane that can hold the contents of n, rotated 90° +ncplane* rotate_plane(const ncplane* n){ + int absy, absx; + ncplane_yx(n, &absy, &absx); + int dimy, dimx; + ncplane_dim_yx(n, &dimy, &dimx); + if(dimx % 2 != 0){ + return NULL; + } + const int newy = dimx / 2; + const int newx = dimy * 2; + ncplane* newp = ncplane_new(n->nc, newy, newx, absy, absx, n->userptr); + return newp; +} diff --git a/src/lib/oiio.cpp b/src/lib/oiio.cpp deleted file mode 100644 index 3735dad38..000000000 --- a/src/lib/oiio.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include "version.h" -#ifdef USE_OIIO -#include -#include -#include -#include -#include -#include "internal.h" - -typedef struct ncvisual { - int packet_outstanding; - int dstwidth, dstheight; - float timescale; // scale frame duration by this value - std::unique_ptr image; // must be close()d - std::unique_ptr raw; - char* filename; - bool did_scaling; - uint64_t framenum; - OIIO::ImageBuf scaled; // in use IFF did_scaling; - std::unique_ptr frame; - ncplane* ncp; - // if we're creating the plane based off the first frame's dimensions, these - // describe where the plane ought be placed, and how it ought be sized. this - // path sets ncobj. ncvisual_destroy() ought in that case kill the ncplane. - int placex, placey; - ncscale_e style; // none, scale, or stretch - struct notcurses* ncobj; // set iff this ncvisual "owns" its ncplane -} ncvisual; - -ncplane* ncvisual_plane(ncvisual* ncv){ - return ncv->ncp; -} - -bool notcurses_canopen(const notcurses* nc __attribute__ ((unused))){ - return true; -} - -ncvisual* ncvisual_create(float timescale){ - auto ret = new ncvisual; - if(ret == nullptr){ - return nullptr; - } - ret->packet_outstanding = 0; - ret->dstwidth = ret->dstheight = 0; - ret->framenum = 0; - ret->timescale = timescale; - ret->image = nullptr; - ret->ncp = nullptr; - ret->placex = ret->placey = 0; - ret->style = NCSCALE_NONE; - ret->ncobj = nullptr; - ret->frame = nullptr; - ret->raw = nullptr; - ret->filename = nullptr; - return ret; -} - -static ncvisual* -ncvisual_open(const char* filename, nc_err_e* err){ - ncvisual* ncv = ncvisual_create(filename, 1); - if(ncv == nullptr){ - *err = NCERR_NOMEM; - return nullptr; - } - if((ncv->filename = strdup(filename)) == nullptr){ - *err = NCERR_NOMEM; - delete ncv; - return nullptr; - } - ncv->image = OIIO::ImageInput::open(filename); - if(!ncv->image){ - // fprintf(stderr, "Couldn't create %s (%s)\n", filename, strerror(errno)); - *err = NCERR_DECODE; - ncvisual_destroy(ncv); - return nullptr; - } -/*const auto &spec = ncv->image->spec_dimensions(); -std::cout << "Opened " << filename << ": " << spec.height << "x" << -spec.width << "@" << spec.nchannels << " (" << spec.format << ")" << std::endl;*/ - return ncv; -} - -ncvisual* ncplane_visual_open(ncplane* nc, const char* filename, nc_err_e* ncerr){ - ncvisual* ncv = ncvisual_open(filename, ncerr); - if(ncv == nullptr){ - *ncerr = NCERR_NOMEM; - return nullptr; - } - ncplane_dim_yx(nc, &ncv->dstheight, &ncv->dstwidth); - ncv->dstheight *= 2; - ncv->ncp = nc; - ncv->style = NCSCALE_STRETCH; - ncv->ncobj = nullptr; - return ncv; -} - -ncvisual* ncvisual_from_file(notcurses* nc, const char* filename, nc_err_e* ncerr, - int y, int x, ncscale_e style){ - ncvisual* ncv = ncvisual_open(filename, ncerr); - if(ncv == nullptr){ - return nullptr; - } - ncv->placey = y; - ncv->placex = x; - ncv->style = style; - ncv->ncobj = nc; - ncv->ncp = nullptr; - ncv->ncobj = nc; - return ncv; -} - -nc_err_e ncvisual_decode(ncvisual* nc){ -//fprintf(stderr, "current subimage: %d frame: %p\n", nc->image->current_subimage(), nc->frame.get()); - const auto &spec = nc->image->spec_dimensions(nc->framenum); - if(nc->frame){ -//fprintf(stderr, "seeking subimage: %d\n", nc->image->current_subimage() + 1); - OIIO::ImageSpec newspec; - if(!nc->image->seek_subimage(nc->image->current_subimage() + 1, 0, newspec)){ - return NCERR_EOF; - } - // FIXME check newspec vis-a-vis image->spec()? - } -//fprintf(stderr, "SUBIMAGE: %d\n", nc->image->current_subimage()); - nc->did_scaling = false; - auto pixels = spec.width * spec.height;// * spec.nchannels; - if(spec.nchannels < 3 || spec.nchannels > 4){ - return NCERR_DECODE; // FIXME get some to test with - } - nc->frame = std::make_unique(pixels); - if(spec.nchannels == 3){ // FIXME replace with channel shuffle - std::fill(nc->frame.get(), nc->frame.get() + pixels, 0xfffffffful); - } -//fprintf(stderr, "READING: %d %ju\n", nc->image->current_subimage(), nc->framenum); - if(!nc->image->read_image(nc->framenum++, 0, 0, spec.nchannels, OIIO::TypeDesc(OIIO::TypeDesc::UINT8, 4), nc->frame.get(), 4)){ - return NCERR_DECODE; - } -//fprintf(stderr, "READ: %d %ju\n", nc->image->current_subimage(), nc->framenum); -/*for(int i = 0 ; i < pixels ; ++i){ - //fprintf(stderr, "%06d %02x %02x %02x %02x\n", i, - fprintf(stderr, "%06d %d %d %d %d\n", i, - (nc->frame[i]) & 0xff, - (nc->frame[i] >> 8) & 0xff, - (nc->frame[i] >> 16) & 0xff, - nc->frame[i] >> 24 - ); -}*/ - OIIO::ImageSpec rgbaspec = spec; - rgbaspec.nchannels = 4; - nc->raw = std::make_unique(rgbaspec, nc->frame.get()); -//fprintf(stderr, "SUBS: %d\n", nc->raw->nsubimages()); - int rows, cols; - if(nc->ncp == nullptr){ // create plane - if(nc->style == NCSCALE_NONE){ - rows = spec.height / 2; - cols = spec.width; - }else{ // FIXME differentiate between scale/stretch - notcurses_term_dim_yx(nc->ncobj, &rows, &cols); - if(nc->placey >= rows || nc->placex >= cols){ - return NCERR_DECODE; - } - rows -= nc->placey; - cols -= nc->placex; - } - nc->dstwidth = cols; - nc->dstheight = rows * 2; - nc->ncp = ncplane_new(nc->ncobj, rows, cols, nc->placey, nc->placex, nullptr); - nc->placey = 0; - nc->placex = 0; - if(nc->ncp == nullptr){ - return NCERR_NOMEM; - } - }else{ // check for resize - ncplane_dim_yx(nc->ncp, &rows, &cols); - if(rows != nc->dstheight / 2 || cols != nc->dstwidth){ - nc->dstheight = rows * 2; - nc->dstwidth = cols; - } - } - if(nc->dstwidth != spec.width || nc->dstheight != spec.height){ // scale it - OIIO::ROI roi(0, nc->dstwidth, 0, nc->dstheight, 0, 1, 0, 4); - if(!OIIO::ImageBufAlgo::resize(nc->scaled, *nc->raw, "", 0, roi)){ - // FIXME - } - nc->did_scaling = true; - } - return NCERR_SUCCESS; -} - -int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx){ -//fprintf(stderr, "render %dx%d+%dx%d\n", begy, begx, leny, lenx); - if(begy < 0 || begx < 0 || lenx < -1 || leny < -1){ - return -1; - } - if(ncv->frame == nullptr){ - return -1; - } - const auto &spec = ncv->did_scaling ? ncv->scaled.spec() : ncv->raw->spec(); - const void* pixels = ncv->did_scaling ? ncv->scaled.localpixels() : ncv->raw->localpixels(); -//fprintf(stderr, "render %d/%d to %dx%d+%dx%d\n", f->height, f->width, begy, begx, leny, lenx); - if(begx >= spec.width || begy >= spec.height){ - return -1; - } - if(lenx == -1){ // -1 means "to the end"; use all space available - lenx = spec.width - begx; - } - if(leny == -1){ - leny = spec.height - begy; - } - if(lenx == 0 || leny == 0){ // no need to draw zero-size object, exit - return 0; - } - if(begx + lenx > spec.width || begy + leny > spec.height){ - return -1; - } - int dimy, dimx; - ncplane_dim_yx(ncv->ncp, &dimy, &dimx); - ncplane_cursor_move_yx(ncv->ncp, 0, 0); - // y and x are actual plane coordinates. each row corresponds to two rows of - // the input (scaled) frame (columns are 1:1). we track the row of the - // visual via visy. -//fprintf(stderr, "render: %dx%d:%d+%d of %d/%d -> %dx%d\n", begy, begx, leny, lenx, f->height, f->width, dimy, dimx); - const int linesize = spec.width * 4; - int ret = ncblit_rgba(ncv->ncp, ncv->placey, ncv->placex, linesize, - pixels, begy, begx, leny, lenx); - //av_frame_unref(ncv->oframe); - return ret; -} - -int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv, nc_err_e* ncerr, - float timescale, streamcb streamer, void* curry){ - int frame = 1; - ncv->timescale = timescale; - struct timespec begin; // time we started - clock_gettime(CLOCK_MONOTONIC, &begin); - //uint64_t nsbegin = timespec_to_ns(&begin); - //bool usets = false; - // each frame has a pkt_duration in milliseconds. keep the aggregate, in case - // we don't have PTS available. - //uint64_t sum_duration = 0; - while((*ncerr = ncvisual_decode(ncv)) == NCERR_SUCCESS){ - /* codecctx seems to be off by a factor of 2 regularly. instead, go with - // the time_base from the avformatctx. - double tbase = av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base); - int64_t ts = ncv->oframe->best_effort_timestamp; - if(frame == 1 && ts){ - usets = true; - }*/ - if(ncvisual_render(ncv, 0, 0, -1, -1) < 0){ - return -1; - } - if(streamer){ - int r = streamer(nc, ncv, curry); - if(r){ - return r; - } - } - ++frame; - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - /*uint64_t nsnow = timespec_to_ns(&now); - struct timespec interval; - uint64_t duration = ncv->oframe->pkt_duration * tbase * NANOSECS_IN_SEC; - sum_duration += (duration * ncv->timescale); -//fprintf(stderr, "use: %u dur: %ju ts: %ju cctx: %f fctx: %f\n", usets, duration, ts, av_q2d(ncv->codecctx->time_base), av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base)); - double schedns = nsbegin; - if(usets){ - if(tbase == 0){ - tbase = duration; - } - schedns += ts * (tbase * ncv->timescale) * NANOSECS_IN_SEC; - }else{ - schedns += sum_duration; - } - if(nsnow < schedns){ - ns_to_timespec(schedns - nsnow, &interval); - nanosleep(&interval, nullptr); - }*/ - } - if(*ncerr == NCERR_EOF){ - return 0; - } - return -1; -} - -char* ncvisual_subtitle(const ncvisual* ncv){ // no support in OIIO - (void)ncv; - return nullptr; -} - -int ncvisual_init(int loglevel){ - // FIXME set OIIO global attribute "debug" based on loglevel - (void)loglevel; - // FIXME check OIIO_VERSION_STRING components against linked openimageio_version() - return 0; // allow success here -} - -void ncvisual_destroy(ncvisual* ncv){ - if(ncv){ - if(ncv->image){ - ncv->image->close(); - } - if(ncv->ncobj){ - ncplane_destroy(ncv->ncp); - } - free(ncv->filename); - delete ncv; - } -} - -// FIXME would be nice to have OIIO::attributes("libraries") in here -const char* oiio_version(void){ - return OIIO_VERSION_STRING; -} - -ncvisual* ncvisual_from_rgba(notcurses* nc, const void* rgba, int rows, int rowstride, int cols){ - ncvisual* ncv = ncvisual_create(1); - ncv->placey = 0; - ncv->placex = 0; - ncv->ncobj = nc; - const int targrows = rows / 2 + rows % 2; - ncv->ncp = ncplane_new(nc, targrows, cols, 0, 0, NULL); - if(ncv->ncp == NULL){ - ncvisual_destroy(ncv); - return NULL; - } - // FIXME create frame - return ncv; -} - -ncvisual* ncvisual_from_bgra(notcurses* nc, const void* bgra, int rows, int rowstride, int cols){ - ncvisual* ncv = ncvisual_create(1); - ncv->placey = 0; - ncv->placex = 0; - ncv->ncobj = nc; - const int targrows = rows / 2 + rows % 2; - ncv->ncp = ncplane_new(nc, targrows, cols, 0, 0, NULL); - if(ncv->ncp == NULL){ - ncvisual_destroy(ncv); - return NULL; - } - // FIXME create frame - return ncv; -} -#endif diff --git a/src/lib/visual.cpp b/src/lib/visual.cpp index df89d9eef..0ad2abde6 100644 --- a/src/lib/visual.cpp +++ b/src/lib/visual.cpp @@ -1,10 +1,1071 @@ +#include +#include "version.h" + +#ifdef USE_FFMPEG +extern "C" { +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#else +#ifdef USE_OIIO +#include +#include +#include +#include +#include +#endif +#endif #include "internal.h" -// ncvisual functionality independent of the underlying engine +#ifdef USE_FFMPEG +struct AVFormatContext; +struct AVCodecContext; +struct AVFrame; +struct AVCodec; +struct AVCodecParameters; +struct AVPacket; +#endif -ncvisual* ncvisual_from_plane(ncplane* n){ +typedef struct ncvisual { + int packet_outstanding; + int dstwidth, dstheight; + int stream_index; // match against this following av_read_frame() + int sub_stream_index; // subtitle stream index, can be < 0 if no subtitles + float timescale; // scale frame duration by this value + ncplane* ncp; + char* filename; + // if we're creating the plane based off the first frame's dimensions, these + // describe where the plane ought be placed, and how it ought be sized. this + // path sets ncobj. ncvisual_destroy() ought in that case kill the ncplane. + int placex, placey; + // ffmpeg sometimes pads lines. this many true bytes per row in data. + int rowstride; + const uint32_t* data; // (scaled) image data, linesize bytes per row + ncscale_e style; // none, scale, or stretch + uint64_t framenum; + struct notcurses* ncobj; // set iff this ncvisual "owns" its ncplane +#ifdef USE_FFMPEG + struct AVFormatContext* fmtctx; + struct AVCodecContext* codecctx; // video codec context + struct AVCodecContext* subtcodecctx; // subtitle codec context + struct AVFrame* frame; + struct AVFrame* oframe; + struct AVCodec* codec; + struct AVCodecParameters* cparams; + struct AVCodec* subtcodec; + struct AVPacket* packet; + struct SwsContext* swsctx; + AVSubtitle subtitle; +#else +#ifdef USE_OIIO + std::unique_ptr image; // must be close()d + std::unique_ptr raw; + std::unique_ptr frame; + OIIO::ImageBuf scaled; // in use IFF did_scaling; +#endif +#endif + bool did_scaling; +} ncvisual; + +ncvisual* ncvisual_create(float timescale){ + auto ret = new ncvisual{}; + if(ret == nullptr){ + return nullptr; + } + ret->timescale = timescale; + return ret; +} + +struct ncvisual* ncvisual_from_plane(ncplane* n){ int dimy, dimx; ncplane_dim_yx(n, &dimy, &dimx); - ncvisual* ncv = ncvisual_create(1); + struct ncvisual* ncv = ncvisual_create(1); + // FIXME populate via ncplane_at_yx() traversal return ncv; } + +void* bgra_to_rgba(const void* data, int rows, int rowstride, int cols){ + if(rowstride % 4){ // must be a multiple of 4 bytes + return NULL; + } +//fprintf(stderr, "ROWS: %d\n", rows); + assert(rows % 2 == 0); // FIXME handle odd number of rows + uint32_t* ret = static_cast(malloc(rowstride * rows)); + if(ret){ + for(int y = 0 ; y < rows ; ++y){ + for(int x = 0 ; x < cols ; ++x){ + const uint32_t* src = (const uint32_t*)data + (rowstride / 4) * y + x; + uint32_t* dst = ret + (rowstride / 4) * y + x; +//fprintf(stderr, "SRC: %x DST: %x\n", *src, *dst); + *dst = ((*src & 0xff00u) << 16u) | (*src & 0xff00ffu) | ((*src & 0xff000000) >> 16u); +//fprintf(stderr, "y: %d x: %d SRC: %x DST: %x\n", y, x, *src, *dst); + } + } + } + return ret; +} + +int ncvisual_setplane(ncvisual* ncv, ncplane* n){ + int ret = 0; + if(n != ncv->ncp){ + if(ncv->ncp){ + ret |= ncplane_destroy(ncv->ncp); + } + ncv->ncp = n; + } + return ret; +} + +int ncvisual_rotate_cw(struct ncvisual* ncv){ + if(ncv->data == NULL){ + return -1; + } + ncplane* n = ncvisual_plane(ncv); + ncplane* newp = rotate_plane(n); + if(newp == NULL){ + return -1; + } +//fprintf(stderr, "stride: %d height: %d width: %d\n", ncv->rowstride, ncv->dstheight, ncv->dstwidth); + assert(ncv->rowstride / 4 >= ncv->dstwidth); + uint32_t* data = static_cast(malloc(ncv->dstheight * ncv->dstwidth * 4)); + if(data == NULL){ + ncplane_destroy(newp); + return -1; + } + // targy <- x, targx <- ncv->dstheight - y - 1 + for(int targy = 0 ; targy < ncv->dstwidth ; ++targy){ + for(int targx = 0 ; targx < ncv->dstheight ; ++targx){ + const int x = targy; + const int y = ncv->dstheight - 1 - targx; +//fprintf(stderr, "CW: %d/%d (%08x) -> %d/%d (stride: %d)\n", y, x, ncv->data[y * (ncv->rowstride / 4) + x], targy, targx, ncv->rowstride); + data[targy * ncv->dstheight + targx] = ncv->data[y * (ncv->rowstride / 4) + x]; +//fprintf(stderr, "wrote %08x to %d (%d)\n", data[targy * ncv->dstheight + targx], targy * ncv->dstheight + targx, (targy * ncv->dstheight + targx) * 4); + } + } + int ret = ncplane_destroy(n); + // free(ncv->data); FIXME + int tmp = ncv->dstwidth; + ncv->dstwidth = ncv->dstheight; + ncv->dstheight = tmp; + ncv->rowstride = ncv->dstwidth * 4; + ncv->data = data; + ncv->ncp = newp; + return ret; +} + +int ncvisual_rotate_ccw(struct ncvisual* ncv){ + if(ncv->data == NULL){ + return -1; + } + ncplane* n = ncvisual_plane(ncv); + ncplane* newp = rotate_plane(n); + if(newp == NULL){ + return -1; + } + assert(ncv->rowstride / 4 >= ncv->dstwidth); + uint32_t* data = static_cast(malloc(ncv->dstheight * ncv->dstwidth * 4)); + if(data == NULL){ + ncplane_destroy(newp); + return -1; + } + // Each row of the target plane is taken from a column of the source plane. + // As the target row grows (down), the source column shrinks (moves left). + // work from the top to bottom and left to right of the dest. min y goes to + // min x. max y goes to max x. max x goes to min y. min x goes to max y. + for(int targy = 0 ; targy < ncv->dstwidth ; ++targy){ + for(int targx = 0 ; targx < ncv->dstheight ; ++targx){ + const int x = ncv->dstwidth - 1 - targy; + const int y = targx; +//fprintf(stderr, "CW: %d/%d (%08x) -> %d/%d (stride: %d)\n", y, x, ncv->data[y * (ncv->rowstride / 4) + x], targy, targx, ncv->rowstride); + data[targy * ncv->dstheight + targx] = ncv->data[y * (ncv->rowstride / 4) + x]; +//fprintf(stderr, "CCW: %d/%d -> %d/%d\n", y, x, targy, targx); + } + } + int ret = ncplane_destroy(n); + // free(ncv->data); FIXME + int tmp = ncv->dstwidth; + ncv->dstwidth = ncv->dstheight; + ncv->dstheight = tmp; + ncv->rowstride = ncv->dstwidth * 4; + ncv->data = data; + ncv->ncp = newp; + return ret; +} + +ncvisual* ncvisual_from_rgba(notcurses* nc, const void* rgba, int rows, + int rowstride, int cols){ + if(rowstride % 4){ + return NULL; + } + ncvisual* ncv = ncvisual_create(1); +//fprintf(stderr, "ROWS: %d STRIDE: %d (%d) COLS: %d\n", rows, rowstride, rowstride / 4, cols); + ncv->rowstride = rowstride; + ncv->ncobj = nc; + ncv->dstwidth = cols; + ncv->dstheight = rows; + if(ncv->dstheight % 2){ + ++ncv->dstheight; + } + int disprows = ncv->dstheight / 2; +//fprintf(stderr, "MADE INITIAL ONE %d/%d\n", disprows, ncv->dstwidth); + ncv->ncp = ncplane_new(nc, disprows, ncv->dstwidth, 0, 0, NULL); + if(ncv->ncp == NULL){ + ncvisual_destroy(ncv); + return NULL; + } + uint32_t* data = static_cast(malloc(rowstride * ncv->dstheight)); + if(data == NULL){ + ncvisual_destroy(ncv); + return NULL; + } + memcpy(data, rgba, rowstride * rows); + if(ncv->dstheight != rows){ + memset(data + rows * (rowstride / 4), 0, rowstride); + } + ncv->data = data; + return ncv; +} + +ncvisual* ncvisual_from_bgra(notcurses* nc, const void* bgra, int rows, + int rowstride, int cols){ + if(rowstride % 4){ + return NULL; + } + ncvisual* ncv = ncvisual_create(1); + ncv->rowstride = rowstride; + ncv->ncobj = nc; + ncv->dstwidth = cols; + ncv->dstheight = rows; + if(ncv->dstheight % 2){ + ++ncv->dstheight; + } + int disprows = ncv->dstheight / 2; + ncv->ncp = ncplane_new(nc, disprows, ncv->dstwidth, 0, 0, NULL); + if(ncv->ncp == NULL){ + ncvisual_destroy(ncv); + return NULL; + } + ncv->data = static_cast(bgra_to_rgba(bgra, rows, rowstride, cols)); + if(ncv->data == NULL){ + ncvisual_destroy(ncv); + return NULL; + } + return ncv; +} + +#ifdef USE_FFMPEG +ncplane* ncvisual_plane(ncvisual* ncv){ + return ncv->ncp; +} + +void ncvisual_destroy(ncvisual* ncv){ + if(ncv){ + avcodec_close(ncv->codecctx); + avcodec_free_context(&ncv->codecctx); + av_frame_free(&ncv->frame); + av_freep(&ncv->oframe); + //avcodec_parameters_free(&ncv->cparams); + sws_freeContext(ncv->swsctx); + av_packet_free(&ncv->packet); + avformat_close_input(&ncv->fmtctx); + avsubtitle_free(&ncv->subtitle); + if(ncv->ncobj && ncv->ncp){ + ncplane_destroy(ncv->ncp); + } + delete ncv; + } +} + +bool notcurses_canopen(const notcurses* nc __attribute__ ((unused))){ + return true; +} + +/* static void +print_frame_summary(const AVCodecContext* cctx, const AVFrame* f){ + char pfmt[128]; + av_get_pix_fmt_string(pfmt, sizeof(pfmt), f->format); + fprintf(stderr, "Frame %05d (%d? %d?) %dx%d pfmt %d (%s)\n", + cctx->frame_number, + f->coded_picture_number, + f->display_picture_number, + f->width, f->height, + f->format, pfmt); + fprintf(stderr, " Data (%d):", AV_NUM_DATA_POINTERS); + int i; + for(i = 0 ; i < AV_NUM_DATA_POINTERS ; ++i){ + fprintf(stderr, " %p", f->data[i]); + } + fprintf(stderr, "\n Linesizes:"); + for(i = 0 ; i < AV_NUM_DATA_POINTERS ; ++i){ + fprintf(stderr, " %d", f->linesize[i]); + } + if(f->sample_aspect_ratio.num == 0 && f->sample_aspect_ratio.den == 1){ + fprintf(stderr, "\n Aspect ratio unknown"); + }else{ + fprintf(stderr, "\n Aspect ratio %d:%d", f->sample_aspect_ratio.num, f->sample_aspect_ratio.den); + } + if(f->interlaced_frame){ + fprintf(stderr, " [ILaced]"); + } + if(f->palette_has_changed){ + fprintf(stderr, " [NewPal]"); + } + fprintf(stderr, " PTS %ld Flags: 0x%04x\n", f->pts, f->flags); + fprintf(stderr, " %lums@%lums (%skeyframe) qual: %d\n", + f->pkt_duration, // FIXME in 'time_base' units + f->best_effort_timestamp, + f->key_frame ? "" : "non-", + f->quality); +}*/ + +static char* +deass(const char* ass){ + // SSA/ASS formats: + // Dialogue: Marked=0,0:02:40.65,0:02:41.79,Wolf main,Cher,0000,0000,0000,,Et les enregistrements de ses ondes delta ? + // FIXME more + if(strncmp(ass, "Dialogue:", strlen("Dialogue:"))){ + return NULL; + } + const char* delim = strchr(ass, ','); + int commas = 0; // we want 8 + while(delim && commas < 8){ + delim = strchr(delim + 1, ','); + ++commas; + } + if(!delim){ + return NULL; + } + // handle ASS syntax...\i0, \b0, etc. + char* dup = strdup(delim + 1); + char* c = dup; + while(*c){ + if(*c == '\\'){ + *c = ' '; + ++c; + if(*c){ + *c = ' ';; + } + } + ++c; + } + return dup; +} + +char* ncvisual_subtitle(const ncvisual* ncv){ + for(unsigned i = 0 ; i < ncv->subtitle.num_rects ; ++i){ + const AVSubtitleRect* rect = ncv->subtitle.rects[i]; + if(rect->type == SUBTITLE_ASS){ + return deass(rect->ass); + }else if(rect->type == SUBTITLE_TEXT) {; + return strdup(rect->text); + } + } + return NULL; +} + +static nc_err_e +averr2ncerr(int averr){ + if(averr == AVERROR_EOF){ + return NCERR_EOF; + } + // FIXME need to map averror codes to ncerrors +//fprintf(stderr, "AVERR: %d/%x %d/%x\n", averr, averr, -averr, -averr); + return NCERR_DECODE; +} + +nc_err_e ncvisual_decode(ncvisual* nc){ + bool have_frame = false; + bool unref = false; + // FIXME what if this was set up with e.g. ncvisual_from_rgba()? + av_freep(&nc->oframe->data[0]); + do{ + do{ + if(nc->packet_outstanding){ + break; + } + if(unref){ + av_packet_unref(nc->packet); + } + int averr; + if((averr = av_read_frame(nc->fmtctx, nc->packet)) < 0){ + /*if(averr != AVERROR_EOF){ + fprintf(stderr, "Error reading frame info (%s)\n", av_err2str(*averr)); + }*/ + return averr2ncerr(averr); + } + unref = true; + if(nc->packet->stream_index == nc->sub_stream_index){ + int result = 0, ret; + ret = avcodec_decode_subtitle2(nc->subtcodecctx, &nc->subtitle, &result, nc->packet); + if(ret >= 0 && result){ + // FIXME? + } + } + }while(nc->packet->stream_index != nc->stream_index); + ++nc->packet_outstanding; + if(avcodec_send_packet(nc->codecctx, nc->packet) < 0){ + //fprintf(stderr, "Error processing AVPacket (%s)\n", av_err2str(*ncerr)); + return ncvisual_decode(nc); + } + --nc->packet_outstanding; + av_packet_unref(nc->packet); + int averr = avcodec_receive_frame(nc->codecctx, nc->frame); + if(averr >= 0){ + have_frame = true; + }else if(averr == AVERROR(EAGAIN) || averr == AVERROR_EOF){ + have_frame = false; + }else if(averr < 0){ + //fprintf(stderr, "Error decoding AVPacket (%s)\n", av_err2str(averr)); + return averr2ncerr(averr); + } + }while(!have_frame); +//print_frame_summary(nc->codecctx, nc->frame); +#define IMGALLOCALIGN 32 + int rows, cols; + if(nc->ncp == NULL){ // create plane + if(nc->style == NCSCALE_NONE){ + rows = nc->frame->height / 2; + cols = nc->frame->width; + }else{ // FIXME differentiate between scale/stretch + notcurses_term_dim_yx(nc->ncobj, &rows, &cols); + if(nc->placey >= rows || nc->placex >= cols){ + return NCERR_DECODE; + } + rows -= nc->placey; + cols -= nc->placex; + } + nc->dstwidth = cols; + nc->dstheight = rows * 2; + nc->ncp = ncplane_new(nc->ncobj, rows, cols, nc->placey, nc->placex, NULL); + nc->placey = 0; + nc->placex = 0; + if(nc->ncp == NULL){ + return NCERR_NOMEM; + } + }else{ // check for resize + ncplane_dim_yx(nc->ncp, &rows, &cols); + if(rows != nc->dstheight / 2 || cols != nc->dstwidth){ + sws_freeContext(nc->swsctx); + nc->swsctx = NULL; + nc->dstheight = rows * 2; + nc->dstwidth = cols; + } + } + const int targformat = AV_PIX_FMT_RGBA; + nc->swsctx = sws_getCachedContext(nc->swsctx, + nc->frame->width, + nc->frame->height, + static_cast(nc->frame->format), + nc->dstwidth, + nc->dstheight, + static_cast(targformat), + SWS_LANCZOS, + NULL, NULL, NULL); + if(nc->swsctx == NULL){ + //fprintf(stderr, "Error retrieving swsctx\n"); + return NCERR_DECODE; + } + memcpy(nc->oframe, nc->frame, sizeof(*nc->oframe)); + nc->oframe->format = targformat; + nc->oframe->width = nc->dstwidth; + nc->oframe->height = nc->dstheight; + int size = av_image_alloc(nc->oframe->data, nc->oframe->linesize, + nc->oframe->width, nc->oframe->height, + static_cast(nc->oframe->format), + IMGALLOCALIGN); + if(size < 0){ + //fprintf(stderr, "Error allocating visual data (%s)\n", av_err2str(size)); + return NCERR_NOMEM; + } + int height = sws_scale(nc->swsctx, (const uint8_t* const*)nc->frame->data, + nc->frame->linesize, 0, + nc->frame->height, nc->oframe->data, nc->oframe->linesize); + if(height < 0){ + //fprintf(stderr, "Error applying scaling (%s)\n", av_err2str(height)); + return NCERR_NOMEM; + } +//print_frame_summary(nc->codecctx, nc->oframe); +#undef IMGALLOCALIGN + av_frame_unref(nc->frame); + const AVFrame* f = nc->oframe; + int bpp = av_get_bits_per_pixel(av_pix_fmt_desc_get(static_cast(f->format))); + if(bpp != 32){ + return NCERR_DECODE; + } + nc->rowstride = f->linesize[0]; + nc->data = reinterpret_cast(f->data[0]); + return NCERR_SUCCESS; +} + +static ncvisual* +ncvisual_open(const char* filename, nc_err_e* ncerr){ + ncvisual* ncv = ncvisual_create(1); + if(ncv == NULL){ + // fprintf(stderr, "Couldn't create %s (%s)\n", filename, strerror(errno)); + *ncerr = NCERR_NOMEM; + return NULL; + } + memset(ncv, 0, sizeof(*ncv)); + int averr = avformat_open_input(&ncv->fmtctx, filename, NULL, NULL); + if(averr < 0){ + // fprintf(stderr, "Couldn't open %s (%s)\n", filename, av_err2str(*averr)); + *ncerr = averr2ncerr(averr); + ncvisual_destroy(ncv); + return NULL; + } + averr = avformat_find_stream_info(ncv->fmtctx, NULL); + if(averr < 0){ + /*fprintf(stderr, "Error extracting stream info from %s (%s)\n", filename, + av_err2str(*averr));*/ + *ncerr = averr2ncerr(averr); + ncvisual_destroy(ncv); + return NULL; + } +//av_dump_format(ncv->fmtctx, 0, filename, false); + if((averr = av_find_best_stream(ncv->fmtctx, AVMEDIA_TYPE_SUBTITLE, -1, -1, &ncv->subtcodec, 0)) >= 0){ + ncv->sub_stream_index = averr; + if((ncv->subtcodecctx = avcodec_alloc_context3(ncv->subtcodec)) == NULL){ + //fprintf(stderr, "Couldn't allocate decoder for %s\n", filename); + *ncerr = NCERR_NOMEM; + ncvisual_destroy(ncv); + return NULL; + } + // FIXME do we need avcodec_parameters_to_context() here? + if((averr = avcodec_open2(ncv->subtcodecctx, ncv->subtcodec, NULL)) < 0){ + //fprintf(stderr, "Couldn't open codec for %s (%s)\n", filename, av_err2str(*averr)); + *ncerr = averr2ncerr(averr); + ncvisual_destroy(ncv); + return NULL; + } + }else{ + ncv->sub_stream_index = -1; + } + if((ncv->packet = av_packet_alloc()) == NULL){ + // fprintf(stderr, "Couldn't allocate packet for %s\n", filename); + *ncerr = NCERR_NOMEM; + ncvisual_destroy(ncv); + return NULL; + } + if((averr = av_find_best_stream(ncv->fmtctx, AVMEDIA_TYPE_VIDEO, -1, -1, &ncv->codec, 0)) < 0){ + // fprintf(stderr, "Couldn't find visuals in %s (%s)\n", filename, av_err2str(*averr)); + *ncerr = averr2ncerr(averr); + ncvisual_destroy(ncv); + return NULL; + } + ncv->stream_index = averr; + if(ncv->codec == NULL){ + //fprintf(stderr, "Couldn't find decoder for %s\n", filename); + ncvisual_destroy(ncv); + return NULL; + } + AVStream* st = ncv->fmtctx->streams[ncv->stream_index]; + if((ncv->codecctx = avcodec_alloc_context3(ncv->codec)) == NULL){ + //fprintf(stderr, "Couldn't allocate decoder for %s\n", filename); + *ncerr = NCERR_NOMEM; + goto err; + } + if(avcodec_parameters_to_context(ncv->codecctx, st->codecpar) < 0){ + goto err; + } + if((averr = avcodec_open2(ncv->codecctx, ncv->codec, NULL)) < 0){ + //fprintf(stderr, "Couldn't open codec for %s (%s)\n", filename, av_err2str(*averr)); + *ncerr = averr2ncerr(averr); + goto err; + } + /*if((ncv->cparams = avcodec_parameters_alloc()) == NULL){ + //fprintf(stderr, "Couldn't allocate codec params for %s\n", filename); + *averr = NCERR_NOMEM; + goto err; + } + if((*averr = avcodec_parameters_from_context(ncv->cparams, ncv->codecctx)) < 0){ + //fprintf(stderr, "Couldn't get codec params for %s (%s)\n", filename, av_err2str(*averr)); + goto err; + }*/ + if((ncv->frame = av_frame_alloc()) == NULL){ + // fprintf(stderr, "Couldn't allocate frame for %s\n", filename); + *ncerr = NCERR_NOMEM; + goto err; + } + if((ncv->oframe = av_frame_alloc()) == NULL){ + // fprintf(stderr, "Couldn't allocate output frame for %s\n", filename); + *ncerr = NCERR_NOMEM; + goto err; + } + return ncv; + +err: + ncvisual_destroy(ncv); + return NULL; +} + +ncvisual* ncplane_visual_open(ncplane* nc, const char* filename, nc_err_e* ncerr){ + ncvisual* ncv = ncvisual_open(filename, ncerr); + if(ncv == NULL){ + return NULL; + } + ncplane_dim_yx(nc, &ncv->dstheight, &ncv->dstwidth); + ncv->dstheight *= 2; + ncv->ncp = nc; + ncv->style = NCSCALE_STRETCH; + return ncv; +} + +ncvisual* ncvisual_from_file(notcurses* nc, const char* filename, + nc_err_e* ncerr, int y, int x, ncscale_e style){ + ncvisual* ncv = ncvisual_open(filename, ncerr); + if(ncv == NULL){ + return NULL; + } + ncv->placey = y; + ncv->placex = x; + ncv->style = style; + ncv->ncobj = nc; + ncv->ncp = NULL; + return ncv; +} + +int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx){ +//fprintf(stderr, "render %dx%d+%dx%d\n", begy, begx, leny, lenx); + if(begy < 0 || begx < 0 || lenx < -1 || leny < -1){ + return -1; + } +//fprintf(stderr, "render %d/%d to %dx%d+%dx%d\n", ncv->height, ncv->width, begy, begx, leny, lenx); + if(begx >= ncv->dstwidth || begy >= ncv->dstheight){ + return -1; + } + if(lenx == -1){ // -1 means "to the end"; use all space available + lenx = ncv->dstwidth - begx; + } + if(leny == -1){ + leny = ncv->dstheight - begy; + } + if(lenx == 0 || leny == 0){ // no need to draw zero-size object, exit + return 0; + } + if(begx + lenx > ncv->dstwidth || begy + leny > ncv->dstheight){ + return -1; + } + int dimy, dimx; + ncplane_dim_yx(ncv->ncp, &dimy, &dimx); + ncplane_cursor_move_yx(ncv->ncp, 0, 0); + // y and x are actual plane coordinates. each row corresponds to two rows of + // the input (scaled) frame (columns are 1:1). we track the row of the + // visual via visy. +//fprintf(stderr, "render: %dx%d:%d+%d of %d/%d -> %dx%d\n", begy, begx, leny, lenx, f->height, f->width, dimy, dimx); + int ret = ncblit_rgba(ncv->ncp, ncv->placey, ncv->placex, ncv->rowstride, + ncv->data, begy, begx, leny, lenx); + //av_frame_unref(ncv->oframe); + return ret; +} + +// iterative over the decoded frames, calling streamer() with curry for each. +// frames carry a presentation time relative to the beginning, so we get an +// initial timestamp, and check each frame against the elapsed time to sync +// up playback. +int ncvisual_stream(notcurses* nc, ncvisual* ncv, nc_err_e* ncerr, + float timescale, streamcb streamer, void* curry){ + int frame = 1; + ncv->timescale = timescale; + struct timespec begin; // time we started + clock_gettime(CLOCK_MONOTONIC, &begin); + uint64_t nsbegin = timespec_to_ns(&begin); + bool usets = false; + // each frame has a pkt_duration in milliseconds. keep the aggregate, in case + // we don't have PTS available. + uint64_t sum_duration = 0; + while((*ncerr = ncvisual_decode(ncv)) == NCERR_SUCCESS){ + // codecctx seems to be off by a factor of 2 regularly. instead, go with + // the time_base from the avformatctx. + double tbase = av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base); + int64_t ts = ncv->oframe->best_effort_timestamp; + if(frame == 1 && ts){ + usets = true; + } + if(ncvisual_render(ncv, 0, 0, -1, -1) < 0){ + return -1; + } + if(streamer){ + int r = streamer(nc, ncv, curry); + if(r){ + return r; + } + } + ++frame; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + uint64_t nsnow = timespec_to_ns(&now); + struct timespec interval; + uint64_t duration = ncv->oframe->pkt_duration * tbase * NANOSECS_IN_SEC; +//fprintf(stderr, "use: %u dur: %ju ts: %ju cctx: %f fctx: %f\n", usets, duration, ts, av_q2d(ncv->codecctx->time_base), av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base)); + sum_duration += (duration * ncv->timescale); + double schedns = nsbegin; + if(usets){ + if(tbase == 0){ + tbase = duration; + } + schedns += ts * (tbase * ncv->timescale) * NANOSECS_IN_SEC; + }else{ + schedns += sum_duration; + } + if(nsnow < schedns){ + ns_to_timespec(schedns - nsnow, &interval); + nanosleep(&interval, NULL); + } + } + if(*ncerr == NCERR_EOF){ + return 0; + } + return -1; +} + +int ncvisual_init(int loglevel){ + av_log_set_level(loglevel); + // FIXME could also use av_log_set_callback() and capture the message... + return 0; +} +} // extern "C" +#else // built without ffmpeg +#ifndef USE_OIIO // built without ffmpeg or oiio +bool notcurses_canopen(const notcurses* nc __attribute__ ((unused))){ + return false; +} + +ncplane* ncvisual_plane(ncvisual* ncv){ + return ncv->ncp; +} + +nc_err_e ncvisual_decode(ncvisual* nc){ + (void)nc; + return NCERR_UNIMPLEMENTED; +} + +int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx){ + (void)ncv; + (void)begy; + (void)begx; + (void)leny; + (void)lenx; + return -1; +} + +int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv, nc_err_e* ncerr, + float timespec, streamcb streamer, void* curry){ + (void)nc; + (void)ncv; + (void)ncerr; + (void)timespec; + (void)streamer; + (void)curry; + return -1; +} + +ncvisual* ncplane_visual_open(ncplane* nc, const char* filename, nc_err_e* ncerr){ + (void)nc; + (void)filename; + (void)ncerr; + return NULL; +} + +ncvisual* ncvisual_from_file(notcurses* nc, const char* filename, + nc_err_e* ncerr, int y, int x, ncscale_e style){ + (void)nc; + (void)filename; + (void)ncerr; + (void)y; + (void)x; + (void)style; + return NULL; +} + +char* ncvisual_subtitle(const ncvisual* ncv){ + (void)ncv; + return NULL; +} + +int ncvisual_init(int loglevel){ + (void)loglevel; + return 0; // allow success here +} + +void ncvisual_destroy(ncvisual* ncv){ + assert(!ncv); + (void)ncv; +} +#else +#ifdef USE_OIIO +ncplane* ncvisual_plane(ncvisual* ncv){ + return ncv->ncp; +} + +bool notcurses_canopen(const notcurses* nc __attribute__ ((unused))){ + return true; +} + +static ncvisual* +ncvisual_open(const char* filename, nc_err_e* err){ + ncvisual* ncv = ncvisual_create(1); + if(ncv == nullptr){ + *err = NCERR_NOMEM; + return nullptr; + } + if((ncv->filename = strdup(filename)) == nullptr){ + *err = NCERR_NOMEM; + delete ncv; + return nullptr; + } + ncv->image = OIIO::ImageInput::open(filename); + if(!ncv->image){ + // fprintf(stderr, "Couldn't create %s (%s)\n", filename, strerror(errno)); + *err = NCERR_DECODE; + ncvisual_destroy(ncv); + return nullptr; + } +/*const auto &spec = ncv->image->spec_dimensions(); +std::cout << "Opened " << filename << ": " << spec.height << "x" << +spec.width << "@" << spec.nchannels << " (" << spec.format << ")" << std::endl;*/ + return ncv; +} + +ncvisual* ncplane_visual_open(ncplane* nc, const char* filename, nc_err_e* ncerr){ + ncvisual* ncv = ncvisual_open(filename, ncerr); + if(ncv == nullptr){ + *ncerr = NCERR_NOMEM; + return nullptr; + } + ncplane_dim_yx(nc, &ncv->dstheight, &ncv->dstwidth); + ncv->dstheight *= 2; + ncv->ncp = nc; + ncv->style = NCSCALE_STRETCH; + ncv->ncobj = nullptr; + return ncv; +} + +ncvisual* ncvisual_from_file(notcurses* nc, const char* filename, nc_err_e* ncerr, + int y, int x, ncscale_e style){ + ncvisual* ncv = ncvisual_open(filename, ncerr); + if(ncv == nullptr){ + return nullptr; + } + ncv->placey = y; + ncv->placex = x; + ncv->style = style; + ncv->ncobj = nc; + ncv->ncp = nullptr; + ncv->ncobj = nc; + return ncv; +} + +nc_err_e ncvisual_decode(ncvisual* nc){ +//fprintf(stderr, "current subimage: %d frame: %p\n", nc->image->current_subimage(), nc->frame.get()); + const auto &spec = nc->image->spec_dimensions(nc->framenum); + if(nc->frame){ +//fprintf(stderr, "seeking subimage: %d\n", nc->image->current_subimage() + 1); + OIIO::ImageSpec newspec; + if(!nc->image->seek_subimage(nc->image->current_subimage() + 1, 0, newspec)){ + return NCERR_EOF; + } + // FIXME check newspec vis-a-vis image->spec()? + } +//fprintf(stderr, "SUBIMAGE: %d\n", nc->image->current_subimage()); + nc->did_scaling = false; + auto pixels = spec.width * spec.height;// * spec.nchannels; + if(spec.nchannels < 3 || spec.nchannels > 4){ + return NCERR_DECODE; // FIXME get some to test with + } + nc->frame = std::make_unique(pixels); + if(spec.nchannels == 3){ // FIXME replace with channel shuffle + std::fill(nc->frame.get(), nc->frame.get() + pixels, 0xfffffffful); + } +//fprintf(stderr, "READING: %d %ju\n", nc->image->current_subimage(), nc->framenum); + if(!nc->image->read_image(nc->framenum++, 0, 0, spec.nchannels, OIIO::TypeDesc(OIIO::TypeDesc::UINT8, 4), nc->frame.get(), 4)){ + return NCERR_DECODE; + } +//fprintf(stderr, "READ: %d %ju\n", nc->image->current_subimage(), nc->framenum); +/*for(int i = 0 ; i < pixels ; ++i){ + //fprintf(stderr, "%06d %02x %02x %02x %02x\n", i, + fprintf(stderr, "%06d %d %d %d %d\n", i, + (nc->frame[i]) & 0xff, + (nc->frame[i] >> 8) & 0xff, + (nc->frame[i] >> 16) & 0xff, + nc->frame[i] >> 24 + ); +}*/ + OIIO::ImageSpec rgbaspec = spec; + rgbaspec.nchannels = 4; + nc->raw = std::make_unique(rgbaspec, nc->frame.get()); +//fprintf(stderr, "SUBS: %d\n", nc->raw->nsubimages()); + int rows, cols; + if(nc->ncp == nullptr){ // create plane + if(nc->style == NCSCALE_NONE){ + rows = spec.height / 2; + cols = spec.width; + }else{ // FIXME differentiate between scale/stretch + notcurses_term_dim_yx(nc->ncobj, &rows, &cols); + if(nc->placey >= rows || nc->placex >= cols){ + return NCERR_DECODE; + } + rows -= nc->placey; + cols -= nc->placex; + } + nc->dstwidth = cols; + nc->dstheight = rows * 2; + nc->ncp = ncplane_new(nc->ncobj, rows, cols, nc->placey, nc->placex, nullptr); + nc->placey = 0; + nc->placex = 0; + if(nc->ncp == nullptr){ + return NCERR_NOMEM; + } + }else{ // check for resize + ncplane_dim_yx(nc->ncp, &rows, &cols); + if(rows != nc->dstheight / 2 || cols != nc->dstwidth){ + nc->dstheight = rows * 2; + nc->dstwidth = cols; + } + } + if(nc->dstwidth != spec.width || nc->dstheight != spec.height){ // scale it + OIIO::ROI roi(0, nc->dstwidth, 0, nc->dstheight, 0, 1, 0, 4); + if(!OIIO::ImageBufAlgo::resize(nc->scaled, *nc->raw, "", 0, roi)){ + // FIXME + } + nc->did_scaling = true; + } + return NCERR_SUCCESS; +} + +int ncvisual_render(const ncvisual* ncv, int begy, int begx, int leny, int lenx){ +//fprintf(stderr, "render %dx%d+%dx%d\n", begy, begx, leny, lenx); + if(begy < 0 || begx < 0 || lenx < -1 || leny < -1){ + return -1; + } + if(ncv->frame == nullptr){ + return -1; + } + const auto &spec = ncv->did_scaling ? ncv->scaled.spec() : ncv->raw->spec(); + const void* pixels = ncv->did_scaling ? ncv->scaled.localpixels() : ncv->raw->localpixels(); +//fprintf(stderr, "render %d/%d to %dx%d+%dx%d\n", f->height, f->width, begy, begx, leny, lenx); + if(begx >= spec.width || begy >= spec.height){ + return -1; + } + if(lenx == -1){ // -1 means "to the end"; use all space available + lenx = spec.width - begx; + } + if(leny == -1){ + leny = spec.height - begy; + } + if(lenx == 0 || leny == 0){ // no need to draw zero-size object, exit + return 0; + } + if(begx + lenx > spec.width || begy + leny > spec.height){ + return -1; + } + int dimy, dimx; + ncplane_dim_yx(ncv->ncp, &dimy, &dimx); + ncplane_cursor_move_yx(ncv->ncp, 0, 0); + // y and x are actual plane coordinates. each row corresponds to two rows of + // the input (scaled) frame (columns are 1:1). we track the row of the + // visual via visy. +//fprintf(stderr, "render: %dx%d:%d+%d of %d/%d -> %dx%d\n", begy, begx, leny, lenx, f->height, f->width, dimy, dimx); + const int linesize = spec.width * 4; + int ret = ncblit_rgba(ncv->ncp, ncv->placey, ncv->placex, linesize, + pixels, begy, begx, leny, lenx); + //av_frame_unref(ncv->oframe); + return ret; +} + +int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv, nc_err_e* ncerr, + float timescale, streamcb streamer, void* curry){ + int frame = 1; + ncv->timescale = timescale; + struct timespec begin; // time we started + clock_gettime(CLOCK_MONOTONIC, &begin); + //uint64_t nsbegin = timespec_to_ns(&begin); + //bool usets = false; + // each frame has a pkt_duration in milliseconds. keep the aggregate, in case + // we don't have PTS available. + //uint64_t sum_duration = 0; + while((*ncerr = ncvisual_decode(ncv)) == NCERR_SUCCESS){ + /* codecctx seems to be off by a factor of 2 regularly. instead, go with + // the time_base from the avformatctx. + double tbase = av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base); + int64_t ts = ncv->oframe->best_effort_timestamp; + if(frame == 1 && ts){ + usets = true; + }*/ + if(ncvisual_render(ncv, 0, 0, -1, -1) < 0){ + return -1; + } + if(streamer){ + int r = streamer(nc, ncv, curry); + if(r){ + return r; + } + } + ++frame; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + /*uint64_t nsnow = timespec_to_ns(&now); + struct timespec interval; + uint64_t duration = ncv->oframe->pkt_duration * tbase * NANOSECS_IN_SEC; + sum_duration += (duration * ncv->timescale); +//fprintf(stderr, "use: %u dur: %ju ts: %ju cctx: %f fctx: %f\n", usets, duration, ts, av_q2d(ncv->codecctx->time_base), av_q2d(ncv->fmtctx->streams[ncv->stream_index]->time_base)); + double schedns = nsbegin; + if(usets){ + if(tbase == 0){ + tbase = duration; + } + schedns += ts * (tbase * ncv->timescale) * NANOSECS_IN_SEC; + }else{ + schedns += sum_duration; + } + if(nsnow < schedns){ + ns_to_timespec(schedns - nsnow, &interval); + nanosleep(&interval, nullptr); + }*/ + } + if(*ncerr == NCERR_EOF){ + return 0; + } + return -1; +} + +char* ncvisual_subtitle(const ncvisual* ncv){ // no support in OIIO + (void)ncv; + return nullptr; +} + +int ncvisual_init(int loglevel){ + // FIXME set OIIO global attribute "debug" based on loglevel + (void)loglevel; + // FIXME check OIIO_VERSION_STRING components against linked openimageio_version() + return 0; // allow success here +} + +void ncvisual_destroy(ncvisual* ncv){ + if(ncv){ + if(ncv->image){ + ncv->image->close(); + } + if(ncv->ncobj){ + ncplane_destroy(ncv->ncp); + } + free(ncv->filename); + delete ncv; + } +} + +extern "C" { +// FIXME would be nice to have OIIO::attributes("libraries") in here +const char* oiio_version(void){ + return OIIO_VERSION_STRING; +} +} +#endif +#endif +#endif diff --git a/src/tetris/lock.h b/src/tetris/lock.h index bc1c1041e..97fc3af64 100644 --- a/src/tetris/lock.h +++ b/src/tetris/lock.h @@ -1,5 +1,5 @@ bool LockPiece(){ // returns true if game has ended by reaching level 16 - curpiece_->get_plane()->mergedown(*board_); + curpiece_->mergedown(*board_); int bdimy, bdimx; board_->get_dim(&bdimy, &bdimx); int cleared; // how many contiguous lines were cleared diff --git a/src/tetris/main.cpp b/src/tetris/main.cpp index d834ba14e..83ef3a672 100644 --- a/src/tetris/main.cpp +++ b/src/tetris/main.cpp @@ -63,7 +63,7 @@ private: ncpp::NotCurses& nc_; uint64_t score_; std::mutex mtx_; // guards msdelay_ - std::unique_ptr curpiece_; + std::unique_ptr curpiece_; std::unique_ptr board_; std::unique_ptr backg_; ncpp::Plane* stdplane_; @@ -79,7 +79,7 @@ private: if(!curpiece_){ return false; } - curpiece_->get_plane()->get_yx(y, x); + curpiece_->get_yx(y, x); return true; } diff --git a/src/tetris/movedown.h b/src/tetris/movedown.h index 905fa9f24..8a4fcf060 100644 --- a/src/tetris/movedown.h +++ b/src/tetris/movedown.h @@ -1,11 +1,11 @@ bool MoveDown() { // returns true if the game has ended as a result of this move int y, x; if(PrepForMove(&y, &x)){ - if(!curpiece_->get_plane()->move(y + 1, x)){ + if(!curpiece_->move(y + 1, x)){ throw TetrisNotcursesErr("move()"); } if(InvalidMove()){ - if(!curpiece_->get_plane()->move(y, x)){ + if(!curpiece_->move(y, x)){ throw TetrisNotcursesErr("move()"); } if(y <= board_top_y_ - 1){ diff --git a/src/tetris/movelateral.h b/src/tetris/movelateral.h index d9a5ea203..b2fc7e83b 100644 --- a/src/tetris/movelateral.h +++ b/src/tetris/movelateral.h @@ -2,11 +2,11 @@ void MoveLateral(int direction) { // pass in -1 for left, 1 for right int shift = 2 * direction; int y, x; if(PrepForMove(&y, &x)){ - if(!curpiece_->get_plane()->move(y, x + shift)){ + if(!curpiece_->move(y, x + shift)){ throw TetrisNotcursesErr("move()"); } if(InvalidMove()){ - if(!curpiece_->get_plane()->move(y, x)){ + if(!curpiece_->move(y, x)){ throw TetrisNotcursesErr("move()"); } }else{ diff --git a/src/tetris/newpiece.h b/src/tetris/newpiece.h index e846072d1..ad1c7a97f 100644 --- a/src/tetris/newpiece.h +++ b/src/tetris/newpiece.h @@ -1,6 +1,6 @@ // tidx is an index into tetriminos. yoff and xoff are relative to the // terminal's origin. returns colored north-facing tetrimino on a plane. -std::unique_ptr NewPiece() { +std::unique_ptr NewPiece() { // "North-facing" tetrimino forms (form in which they are released from the top) are expressed in terms of // two rows having between 2 and 4 columns. We map each game column to four columns and each game row to two // rows. Each byte of the texture maps to one 4x4 component block (and wastes 7 bits). @@ -16,8 +16,7 @@ std::unique_ptr NewPiece() { int y, x; stdplane_->get_dim(&y, &x); const int xoff = x / 2 - BOARD_WIDTH + 2 * (random() % (BOARD_WIDTH / 2)); - /* FIXME - std::unique_ptr n = std::make_unique(2, cols, board_top_y_ - 1, xoff, nullptr); + std::unique_ptr n = std::make_unique(2, cols, board_top_y_ - 1, xoff, nullptr); if(n){ uint64_t channels = 0; channels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT); @@ -39,6 +38,4 @@ std::unique_ptr NewPiece() { throw TetrisNotcursesErr("render()"); } return n; - */ - return nullptr; } diff --git a/src/tetris/stuck.h b/src/tetris/stuck.h index 79ceda653..e11d92eac 100644 --- a/src/tetris/stuck.h +++ b/src/tetris/stuck.h @@ -1,19 +1,19 @@ bool InvalidMove() { // a bit wasteful, but piece are tiny int dy, dx; - curpiece_->get_plane()->get_dim(&dy, &dx); + curpiece_->get_dim(&dy, &dx); while(dy--){ int x = dx; while(x--){ ncpp::Cell c, b; - if(curpiece_->get_plane()->get_at(dy, x, &c) < 0){ + if(curpiece_->get_at(dy, x, &c) < 0){ throw TetrisNotcursesErr("get_at()"); } if(c.is_simple()){ continue; } - curpiece_->get_plane()->release(c); + curpiece_->release(c); int transy = dy, transx = x; // need game area coordinates via translation - curpiece_->get_plane()->translate(*board_, &transy, &transx); + curpiece_->translate(*board_, &transy, &transx); if(transy < 0 || transy >= board_->get_dim_y() || transx < 0 || transx >= board_->get_dim_x()){ return true; } diff --git a/tests/rotate.cpp b/tests/rotate.cpp index b21d492a7..222aacfe1 100644 --- a/tests/rotate.cpp +++ b/tests/rotate.cpp @@ -1,26 +1,27 @@ #include "main.h" +#include -void RotateCW(struct notcurses* nc, struct ncvisual* n) { +void RotateCW(struct notcurses* nc, struct ncplane* n) { CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_cw(n)); + CHECK(0 == ncplane_rotate_cw(n)); CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_cw(n)); + CHECK(0 == ncplane_rotate_cw(n)); CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_cw(n)); + CHECK(0 == ncplane_rotate_cw(n)); CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_cw(n)); + CHECK(0 == ncplane_rotate_cw(n)); CHECK(0 == notcurses_render(nc)); } -void RotateCCW(struct notcurses* nc, struct ncvisual* n) { +void RotateCCW(struct notcurses* nc, struct ncplane* n) { CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_ccw(n)); + CHECK(0 == ncplane_rotate_ccw(n)); CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_ccw(n)); + CHECK(0 == ncplane_rotate_ccw(n)); CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_ccw(n)); + CHECK(0 == ncplane_rotate_ccw(n)); CHECK(0 == notcurses_render(nc)); - CHECK(0 == ncvisual_rotate_ccw(n)); + CHECK(0 == ncplane_rotate_ccw(n)); CHECK(0 == notcurses_render(nc)); } @@ -105,6 +106,51 @@ TEST_CASE("Rotate") { CHECK(0 == ncplane_destroy(testn)); } + // use half of each dimension + SUBCASE("RotateRGBACW") { + std::vector rgba((dimx / 2) * (dimy / 2), 0xffbbccff); + auto ncv = ncvisual_from_rgba(nc_, rgba.data(), dimy / 2, dimx * 2, dimx / 2); + REQUIRE(ncv); + CHECK(dimx * dimy / 8 <= ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_cw(ncv)); + CHECK(dimx * dimy / 8 <= ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_cw(ncv)); + CHECK(dimx * dimy / 8 <= ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_cw(ncv)); + CHECK(dimx * dimy / 8 <= ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_cw(ncv)); + CHECK(dimx * dimy / 8 <= ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + ncvisual_destroy(ncv); + CHECK(0 == notcurses_render(nc_)); + } + + SUBCASE("RotateRGBACCW") { + std::vector rgba(dimx * dimy, 0xffbbccff); + auto ncv = ncvisual_from_rgba(nc_, rgba.data(), dimy, dimx * 2, dimx / 2); + REQUIRE(ncv); + CHECK(dimx * dimy / 4 == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_ccw(ncv)); + CHECK(dimx * dimy / 4 == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_ccw(ncv)); + CHECK(dimx * dimy / 4 == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_ccw(ncv)); + CHECK(dimx * dimy / 4 == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + CHECK(0 == ncvisual_rotate_ccw(ncv)); + CHECK(dimx * dimy / 4 == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + ncvisual_destroy(ncv); + CHECK(0 == notcurses_render(nc_)); + } + CHECK(0 == notcurses_stop(nc_)); CHECK(0 == fclose(outfp_)); diff --git a/tests/visual.cpp b/tests/visual.cpp index b29ed66ac..e32cc0372 100644 --- a/tests/visual.cpp +++ b/tests/visual.cpp @@ -1,4 +1,5 @@ #include "main.h" +#include TEST_CASE("Multimedia") { if(getenv("TERM") == nullptr){ @@ -123,6 +124,30 @@ TEST_CASE("Multimedia") { } #endif + SUBCASE("LoadRGBAFromMemory") { + int dimy, dimx; + ncplane_dim_yx(ncp_, &dimy, &dimx); + std::vector rgba(dimx * dimy * 2, 0x88bbccff); + auto ncv = ncvisual_from_rgba(nc_, rgba.data(), dimy * 2, dimx * 4, dimx); + REQUIRE(ncv); + CHECK(dimx * dimy == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + ncvisual_destroy(ncv); + CHECK(0 == notcurses_render(nc_)); + } + + SUBCASE("LoadBGRAFromMemory") { + int dimy, dimx; + ncplane_dim_yx(ncp_, &dimy, &dimx); + std::vector rgba(dimx * dimy * 2, 0x88bbccff); + auto ncv = ncvisual_from_bgra(nc_, rgba.data(), dimy * 2, dimx * 4, dimx); + REQUIRE(ncv); + CHECK(dimx * dimy == ncvisual_render(ncv, 0, 0, -1, -1)); + CHECK(0 == notcurses_render(nc_)); + ncvisual_destroy(ncv); + CHECK(0 == notcurses_render(nc_)); + } + CHECK(!notcurses_stop(nc_)); CHECK(!fclose(outfp_)); }