Repository: dankamongmen/notcurses (language: python)

Issue:
ncvisual_blitter_geom() needs return cell geometry
`ncvisual_blitset_geom()` or whatever we're calling it this week doesn't return the precisely-calculated cell geometry (see for example #1665's comments about a sixel that's not a multiple of 6 pixels high). as a result, no one can know precisely how many cells are going to be affected by a `ncvisual` render until it happens. add a new function with some stupid name, deprecating the existing `ncvisual_blitset_geom()` entry point. it should take an `ncvgeom` struct as introduced in ncdirect. fill in the `rcell{y, x}` fields. when this is done, update the direct mode `ncfdirect_geom()` to use this structure directly, rather than approximating `rcell{y,x}` for itself.
SIGILL in [yield] demo on freebsd
Since multithreading the `[yield]` demo, running it on FreeBSD generates an Illegal Instruction exception. i'll get a stacktrace.
ncvisual_blitter_geom() needs return cell geometry
`ncvisual_blitset_geom()` or whatever we're calling it this week doesn't return the precisely-calculated cell geometry (see for example #1665's comments about a sixel that's not a multiple of 6 pixels high). as a result, no one can know precisely how many cells are going to be affected by a `ncvisual` render until it happens. add a new function with some stupid name, deprecating the existing `ncvisual_blitset_geom()` entry point. it should take an `ncvgeom` struct as introduced in ncdirect. fill in the `rcell{y, x}` fields. when this is done, update the direct mode `ncfdirect_geom()` to use this structure directly, rather than approximating `rcell{y,x}` for itself.
SIGILL in [yield] demo on freebsd
Since multithreading the `[yield]` demo, running it on FreeBSD generates an Illegal Instruction exception. i'll get a stacktrace.
ncvisual_blitter_geom() needs return cell geometry
`ncvisual_blitset_geom()` or whatever we're calling it this week doesn't return the precisely-calculated cell geometry (see for example #1665's comments about a sixel that's not a multiple of 6 pixels high). as a result, no one can know precisely how many cells are going to be affected by a `ncvisual` render until it happens. add a new function with some stupid name, deprecating the existing `ncvisual_blitset_geom()` entry point. it should take an `ncvgeom` struct as introduced in ncdirect. fill in the `rcell{y, x}` fields. when this is done, update the direct mode `ncfdirect_geom()` to use this structure directly, rather than approximating `rcell{y,x}` for itself.
ncvisual_blitter_geom() needs return cell geometry
`ncvisual_blitset_geom()` or whatever we're calling it this week doesn't return the precisely-calculated cell geometry (see for example #1665's comments about a sixel that's not a multiple of 6 pixels high). as a result, no one can know precisely how many cells are going to be affected by a `ncvisual` render until it happens. add a new function with some stupid name, deprecating the existing `ncvisual_blitset_geom()` entry point. it should take an `ncvgeom` struct as introduced in ncdirect. fill in the `rcell{y, x}` fields. when this is done, update the direct mode `ncfdirect_geom()` to use this structure directly, rather than approximating `rcell{y,x}` for itself.
ncvisual_blitter_geom() needs return cell geometry
`ncvisual_blitset_geom()` or whatever we're calling it this week doesn't return the precisely-calculated cell geometry (see for example #1665's comments about a sixel that's not a multiple of 6 pixels high). as a result, no one can know precisely how many cells are going to be affected by a `ncvisual` render until it happens. add a new function with some stupid name, deprecating the existing `ncvisual_blitset_geom()` entry point. it should take an `ncvgeom` struct as introduced in ncdirect. fill in the `rcell{y, x}` fields. when this is done, update the direct mode `ncfdirect_geom()` to use this structure directly, rather than approximating `rcell{y,x}` for itself.


Discussion / hints:

not unexpectedly, this also happens on macOS

not unexpectedly, this also happens on macOS




Fix patch:
diff --git a/NEWS.md b/NEWS.md
index d9f9333692..20e9083a32 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -12,6 +12,14 @@ rearrangements of Notcurses.
     deprecated functionality, ABI3 ought require small changes, if any.
 
 * 2.4.9 (not yet released)
+  * `ncvisual_geom()` has been introduced, using the `ncvgeom` struct
+    introduced for direct mode. This allows complete statement of geometry
+    for an `ncvisual`. It replaces `ncvisual_blitter_geom()`, which has been
+    deprecated, and will be removed in ABI3. It furthermore exposes some of
+    the information previously available only from `ncplane_pixelgeom()`,
+    though that function continues to be supported.
+  * `ncvgeom`'s `rcelly` and `rcellx` fields are now (finally) filled in
+    by `ncvisual_geom()` (and thus `ncdirectf_geom()`), and suitable for use.
   * On transition between `ncplane`s (on terminals implementing complex wide
     glyphs), Notcurses now always issues an `hpa` sequence to force horizontal
     positioning. This fixes a number of longstanding bugs in e.g. the
diff --git a/README.md b/README.md
index f7847f006b..c0cb991f71 100644
--- a/README.md
+++ b/README.md
@@ -538,9 +538,11 @@ If things break or seem otherwise lackluster, **please** consult the
 
 <details>
   <summary>Why do the stats show more Linux framebuffer bitmap bytes written
-  than total bytes written to the terminal?</summary>
-  Linux framebuffer graphics aren't implemented via terminal writes, but instead
-  writes directly into a memory map.
+  than total bytes written to the terminal? And why don't Linux console
+  graphics work when I ssh?</summary>
+  Linux framebuffer graphics aren't implemented via terminal writes, but rather
+  writes directly into a memory map. This memory map isn't available on remote
+  machines, and these writes aren't tracked by the standard statistics.
 </details>
 
 ## Useful links
diff --git a/TERMINALS.md b/TERMINALS.md
index f453b3b48d..7d9271c7ff 100644
--- a/TERMINALS.md
+++ b/TERMINALS.md
@@ -78,9 +78,10 @@ relies on the font. Patches to correct/complete this table are very welcome!
 | [kmscon](https://github.com/dvdhrm/kmscon)        | | ❌    | ❌      |`TERM=xterm-256color`              | No RGB color AFAICT, nor any distinct terminfo entry. No actual `ccc` implementation. Sets `COLORTERM=kmscon`.|
 | [Konsole](https://invent.kde.org/utilities/konsole) | ❌       |  ❌   |?       |`TERM=konsole-direct`              | |
 | Linux console   | ❌                 |  ✅   |see [below](#the-linux-console) |`TERM=linux` `COLORTERM=24bit`   | 8 (512 glyph fonts) or 16 (256 glyph fonts) colors max, but RGB values are downsampled to a 256-index palette. See below. |
-| [mintty](https://github.com/mintty/mintty) | ✅ | ✅ | ? | `TERM=mintty-direct` | ? |
+| [mintty](https://github.com/mintty/mintty) | ✅ | ✅ | ? | `TERM=mintty-direct` | Windows, both old-skool and ConPTY |
 | [mlterm](https://github.com/arakiken/mlterm)          | ✅                 |  ❌   |?       |`TERM=mlterm-256color`           | Do not set `COLORTERM`. `mlterm-direct` gives strange results. |
 | [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html)           |                    |  ❌   |❌      |`TERM=putty-256color` `COLORTERM=24bit` | |
+| [refterm](https://github.com/cmuratori/refterm.git) | ? | ? | ? | ? | Windows, ConPTY only |
 | rxvt            | ✅                 |  ?    |?       |                                 | Seems unmaintained; many forks exist. |
 | [Sakura](https://github.com/dabisu/sakura)          | ✅                 |  ✅   |?       |`TERM=vte-256color` `COLORTERM=24bit` | VTE-derived, no terminfo entry. |
 | [GNU Screen](https://www.gnu.org/software/screen/)      | ✅                 |  ❌   |n/a     |`TERM=screen.OLDTERM`            | Must be compiled with `--enable-256color`. `TERM` should typically be `screen.` suffixed by the appropriate `TERM` value for the true connected terminal, e.g. `screen.vte-256color`. See below. |
diff --git a/USAGE.md b/USAGE.md
index 9c9d3eb372..6c4054ca23 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -3299,17 +3299,40 @@ Various transformations can be applied to an `ncvisual`, regardless of how
 it was built up:
 
 ```c
-// Get the size and ratio of ncvisual pixels to output cells along the y
-// and x axes. The input size (in pixels) will be written to 'y' and 'x'.
-// The scaling will be written to 'scaley' and 'scalex'. With these:
-//  rows = (y / scaley) + !!(y % scaley) or (y + scaley - 1) / scaley
-//  cols = (x / scalex) + !!(x % scalex) or (x + scalex - 1) / scalex
-// Returns non-zero for an invalid 'vopts'. The blitter that will be used
-// is returned in '*blitter'.
-// These results are invalidated upon a terminal resize.
-int ncvisual_blitter_geom(const struct notcurses* nc, const struct ncvisual* n,
-                          const struct ncvisual_options* vopts, int* y, int* x,
-                          int* scaley, int* scalex, ncblitter_e* blitter);
+// describes all geometries of an ncvisual: those which are inherent, and those
+// dependent upon a given rendering regime. pixy and pixx are the true internal
+// pixel geometry, taken directly from the load (and updated by
+// ncvisual_resize()). cdimy/cdimx are the cell-pixel geometry *at the time
+// of this call* (it can change with a font change, in which case all values
+// other than pixy/pixx are invalidated). rpixy/rpixx are the pixel geometry as
+// handed to the blitter, following any scaling. scaley/scalex are the number
+// of input pixels drawn to a single cell; when using NCBLIT_PIXEL, they are
+// equivalent to cdimy/cdimx. rcelly/rcellx are the cell geometry as written by
+// the blitter, following any padding (there is padding whenever rpix{y, x} is
+// not evenly divided by scale{y, x}, and also sometimes for Sixel).
+// maxpixely/maxpixelx are defined only when NCBLIT_PIXEL is used, and specify
+// the largest bitmap that the terminal is willing to accept. blitter is the
+// blitter which will be used, a function of the requested blitter and the
+// blitters actually supported by this environment. if no ncvisual was
+// supplied, only cdimy/cdimx are filled in.
+typedef struct ncvgeom {
+  int pixy, pixx;     // true pixel geometry of ncvisual data
+  int cdimy, cdimx;   // terminal cell geometry when this was calculated
+  int rpixy, rpixx;   // rendered pixel geometry (per visual_options)
+  int rcelly, rcellx; // rendered cell geometry (per visual_options)
+  int scaley, scalex; // pixels per filled cell (scale == c for bitmaps)
+  int maxpixely, maxpixelx; // only defined for NCBLIT_PIXEL
+  int begy, begx;     // upper-left corner of used section
+  int leny, lenx;     // geometry of used section
+  ncblitter_e blitter;// blitter that will be used
+} ncvgeom;
+
+// all-purpose ncvisual geometry solver. one or both of 'nc' and 'n' must be
+// non-NULL. if 'nc' is NULL, only pixy/pixx will be filled in, with the true
+// pixel geometry of 'n'. if 'n' is NULL, only cdimy/cdimx, blitter, and (if
+// applicable) maxpixely/maxpixelx are filled in.
+int ncvisual_geom(const struct notcurses* nc, const struct ncvisual* n,
+                  const struct ncvisual_options* vopts, ncvgeom* geom);
 
 // Scale the visual to 'rows' X 'columns' pixels, using the best scheme
 // available. This is a lossy transformation, unless the size is unchanged.
diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md
index ba579ddd54..424def9185 100644
--- a/doc/man/man3/notcurses_visual.3.md
+++ b/doc/man/man3/notcurses_visual.3.md
@@ -51,6 +51,18 @@ struct ncvisual_options {
 };
 
 typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*);
+
+typedef struct ncvgeom {
+  int pixy, pixx;     // true pixel geometry of ncvisual data
+  int cdimy, cdimx;   // terminal cell geometry when this was calculated
+  int rpixy, rpixx;   // rendered pixel geometry (per visual_options)
+  int rcelly, rcellx; // rendered cell geometry (per visual_options)
+  int scaley, scalex; // pixels per filled cell (scale == c for bitmaps)
+  int maxpixely, maxpixelx; // only defined for NCBLIT_PIXEL
+  int begy, begx;     // upper-left corner of used section
+  int leny, lenx;     // geometry of used section
+  ncblitter_e blitter;// blitter that will be used
+} ncvgeom;
 ```
 
 **struct ncvisual* ncvisual_from_file(const char* ***file***);**
@@ -67,7 +79,7 @@ typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*);
 
 **struct ncvisual* ncvisual_from_plane(struct ncplane* ***n***, ncblitter_e ***blit***, int ***begy***, int ***begx***, int ***leny***, int ***lenx***);**
 
-**int ncvisual_blitter_geom(const struct notcurses* ***nc***, const struct ncvisual* ***n***, const struct ncvisual_options* ***vopts***, int* ***y***, int* ***x***, int* ***scaley***, int* ***scalex***, ncblitter_e* ***blitter***);**
+**int ncvisual_geom(const struct notcurses* ***nc***, const struct ncvisual* ***n***, const struct ncvisual_options* ***vopts***, ncvgeom* ***geom***);**
 
 **void ncvisual_destroy(struct ncvisual* ***ncv***);**
 
@@ -177,9 +189,14 @@ geometry of same. ***flags*** is a bitfield over:
   as a transparent color.
 * **NCVISUAL_OPTION_CHILDPLANE**: Make a new plane, as a child of ***n***.
 
-**ncvisual_blitter_geom** allows the caller to determine any or all of the
-visual's pixel geometry, the blitter to be used, and that blitter's scaling
-in both dimensions. Any but the first argument may be **NULL**.
+**ncvisual_geom** allows the caller to determine any or all of the visual's
+pixel geometry, the blitter to be used, and that blitter's scaling in both
+dimensions. Any but the final argument may be **NULL**, though at least one
+of ***nc*** and ***n*** must be non-**NULL**. If ***nc*** is **NULL**,
+only properties intrinsic to the visual are returned (i.e. its original
+pixel geometry). If ***n*** is **NULL**, only properties independent of the
+visual are returned (i.e. cell-pixel geometry and maximum bitmap geometry).
+If both are supplied, all fields of the **ncvgeom** structure are filled in.
 
 **ncplane_qrcode** draws an ISO/IEC 18004:2015 QR Code for the **len** bytes of
 **data** using **NCBLIT_2x1** (this is the only blitter that will work with QR
@@ -317,7 +334,8 @@ which the visual was rendered. If **opts->n** is provided, this will be
 **opts->n**. Otherwise, a plane will be created, perfectly sized for the
 visual and the specified blitter.
 
-**ncvisual_blitter_geom** returns non-zero if the specified blitter is invalid.
+**ncvisual_geom** returns non-zero if the specified configuration is invalid,
+or if both ***nc*** and ***n*** are **NULL**.
 
 **ncvisual_media_defblitter** returns the blitter selected by **NCBLIT_DEFAULT**
 in the specified configuration. If UTF8 is not enabled, this will always be
@@ -358,7 +376,7 @@ When using non-interpolative blitting together with scaling, unless your goal
 includes minimizing the total area required, lower-resolution blitters will
 generally look just as good as higher resolution blitters, and be faster.
 
-The results of **ncvisual_blitter_geom** are invalidated by a terminal resize.
+The results of **ncvisual_geom** are invalidated by a terminal resize.
 
 # BUGS
 
diff --git a/include/ncpp/Visual.hh b/include/ncpp/Visual.hh
index e22c8e1390..2f72fd87f8 100644
--- a/include/ncpp/Visual.hh
+++ b/include/ncpp/Visual.hh
@@ -96,9 +96,9 @@ namespace ncpp
 			return error_guard<int> (ncvisual_polyfill_yx (visual, y, x, rgba), -1);
 		}
 
-		bool geom (const struct ncvisual_options *vopts, int *y, int *x, int *toy, int *tox, ncblitter_e* blitter) const NOEXCEPT_MAYBE
+		bool geom (const struct ncvisual_options *vopts, ncvgeom* geom) const NOEXCEPT_MAYBE
 		{
-			return error_guard (ncvisual_blitter_geom (get_notcurses (), visual, vopts, y, x, toy, tox, blitter), -1);
+			return error_guard (ncvisual_geom (get_notcurses (), visual, vopts, geom), -1);
 		}
 
 		bool at (int y, int x, uint32_t* pixel) const
diff --git a/include/notcurses/direct.h b/include/notcurses/direct.h
index 54b2bc7cc9..c3766d7c9c 100644
--- a/include/notcurses/direct.h
+++ b/include/notcurses/direct.h
@@ -350,31 +350,6 @@ API int ncdirect_stop(struct ncdirect* nc);
 typedef struct ncplane ncdirectv;
 typedef struct ncvisual ncdirectf;
 
-// FIXME this ought be used in the rendered mode API as well; it's currently
-// only used by direct mode. describes all geometries of an ncvisual--both those
-// which are inherent, and those in a given rendering regime. pixy and pixx are
-// the true internal pixel geometry, taken directly from the load (and updated
-// by ncvisual_resize()). cdimy/cdimx are the cell pixel geometry *at the time
-// of this call* (it can change with a font change, in which case all values
-// other than pixy/pixx are invalidated). rpixy/rpixx are the pixel geometry as
-// handed to the blitter, following any scaling. scaley/scalex are the number
-// of input pixels drawn to full cell; when using NCBLIT_PIXEL, they are
-// equivalent to cdimy/cdimx. rcelly/rcellx are the cell geometry as written by
-// the blitter, following any padding (there is padding whenever rpix{y, x} is
-// not evenly divided by scale{y, x}, and also sometimes for Sixel).
-// maxpixely/maxpixelx are defined only when NCBLIT_PIXEL is used, and specify
-// the largest bitmap that the terminal is willing to accept.
-typedef struct ncvgeom {
-  int pixy, pixx;     // true pixel geometry of ncvisual data
-  int cdimy, cdimx;   // terminal cell geometry when this was calculated
-  int rpixy, rpixx;   // rendered pixel geometry
-  int rcelly, rcellx; // rendered cell geometry
-  int scaley, scalex; // pixels per filled cell
-  // only defined for NCBLIT_PIXEL
-  int maxpixely, maxpixelx;
-  ncblitter_e blitter;// blitter that will be used
-} ncvgeom;
-
 // Display an image using the specified blitter and scaling. The image may
 // be arbitrarily many rows -- the output will scroll -- but will only occupy
 // the column of the cursor, and those to the right. The render/raster process
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index a34b4c0ca2..679998221a 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -776,7 +776,8 @@ nccell_wide_left_p(const nccell* c){
 
 // return a pointer to the NUL-terminated EGC referenced by 'c'. this pointer
 // can be invalidated by any further operation on the plane 'n', so...watch out!
-API __attribute__ ((returns_nonnull)) const char* nccell_extended_gcluster(const struct ncplane* n, const nccell* c);
+API __attribute__ ((returns_nonnull)) const char*
+nccell_extended_gcluster(const struct ncplane* n, const nccell* c);
 
 // return the number of columns occupied by 'c'. see ncstrwidth() for an
 // equivalent for multiple EGCs.
@@ -1779,6 +1780,17 @@ API void* ncplane_userptr(struct ncplane* n);
 API void ncplane_center_abs(const struct ncplane* n, int* RESTRICT y,
                             int* RESTRICT x);
 
+// Create an RGBA flat array from the selected region of the ncplane 'nc'.
+// Start at the plane's 'begy'x'begx' coordinate (which must lie on the
+// plane), continuing for 'leny'x'lenx' cells. Either or both of 'leny' and
+// 'lenx' can be specified as -1 to go through the boundary of the plane.
+// Only glyphs from the specified ncblitset may be present. If 'pxdimy' and/or
+// 'pxdimx' are non-NULL, they will be filled in with the pixel geometry.
+API ALLOC uint32_t* ncplane_as_rgba(const struct ncplane* n, ncblitter_e blit,
+                                    int begy, int begx, int leny, int lenx,
+                                    int* pxdimy, int* pxdimx)
+  __attribute__ ((nonnull (1)));
+
 // Return the offset into 'availu' at which 'u' ought be output given the
 // requirements of 'align'. Return -INT_MAX on invalid 'align'. Undefined
 // behavior on negative 'availu' or 'u'.
@@ -2869,28 +2881,43 @@ struct ncvisual_options {
   unsigned pxoffy, pxoffx;
 };
 
-// Create an RGBA flat array from the selected region of the ncplane 'nc'.
-// Start at the plane's 'begy'x'begx' coordinate (which must lie on the
-// plane), continuing for 'leny'x'lenx' cells. Either or both of 'leny' and
-// 'lenx' can be specified as -1 to go through the boundary of the plane.
-// Only glyphs from the specified ncblitset may be present. If 'pxdimy' and/or
-// 'pxdimx' are non-NULL, they will be filled in with the pixel geometry.
-API ALLOC uint32_t* ncplane_as_rgba(const struct ncplane* n, ncblitter_e blit,
-                                    int begy, int begx, int leny, int lenx,
-                                    int* pxdimy, int* pxdimx)
-  __attribute__ ((nonnull (1)));
-
-// Get the size and ratio of ncvisual pixels to output cells along the y
-// and x axes. The input size (in pixels) will be written to 'y' and 'x'.
-// The scaling will be written to 'scaley' and 'scalex'. With these:
-//  rows = (y / scaley) + !!(y % scaley) or (y + scaley - 1) / scaley
-//  cols = (x / scalex) + !!(x % scalex) or (x + scalex - 1) / scalex
-// Returns non-zero for an invalid 'vopts'. The blitter that will be used
-// is returned in '*blitter'.
-API int ncvisual_blitter_geom(const struct notcurses* nc, const struct ncvisual* n,
-                              const struct ncvisual_options* vopts, int* y, int* x,
-                              int* scaley, int* scalex, ncblitter_e* blitter)
-  __attribute__ ((nonnull (1)));
+// describes all geometries of an ncvisual: those which are inherent, and those
+// dependent upon a given rendering regime. pixy and pixx are the true internal
+// pixel geometry, taken directly from the load (and updated by
+// ncvisual_resize()). cdimy/cdimx are the cell-pixel geometry *at the time
+// of this call* (it can change with a font change, in which case all values
+// other than pixy/pixx are invalidated). rpixy/rpixx are the pixel geometry as
+// handed to the blitter, following any scaling. scaley/scalex are the number
+// of input pixels drawn to a single cell; when using NCBLIT_PIXEL, they are
+// equivalent to cdimy/cdimx. rcelly/rcellx are the cell geometry as written by
+// the blitter, following any padding (there is padding whenever rpix{y, x} is
+// not evenly divided by scale{y, x}, and also sometimes for Sixel).
+// maxpixely/maxpixelx are defined only when NCBLIT_PIXEL is used, and specify
+// the largest bitmap that the terminal is willing to accept. blitter is the
+// blitter which will be used, a function of the requested blitter and the
+// blitters actually supported by this environment. if no ncvisual was
+// supplied, only cdimy/cdimx are filled in.
+typedef struct ncvgeom {
+  int pixy, pixx;     // true pixel geometry of ncvisual data
+  int cdimy, cdimx;   // terminal cell geometry when this was calculated
+  int rpixy, rpixx;   // rendered pixel geometry (per visual_options)
+  int rcelly, rcellx; // rendered cell geometry (per visual_options)
+  int scaley, scalex; // pixels per filled cell (scale == c for bitmaps)
+  int maxpixely, maxpixelx; // only defined for NCBLIT_PIXEL
+  int begy, begx;     // upper-left corner of used section
+  int leny, lenx;     // geometry of used section
+  ncblitter_e blitter;// blitter that will be used
+} ncvgeom;
+
+// all-purpose ncvisual geometry solver. one or both of 'nc' and 'n' must be
+// non-NULL. if 'nc' is NULL, only pixy/pixx will be filled in, with the true
+// pixel geometry of 'n'. if 'n' is NULL, only cdimy/cdimx, blitter,
+// scaley/scalex, and maxpixely/maxpixelx are filled in; this is the same as
+// calling with a NULL 'n' and a default 'vopts'. cdimy/cdimx and
+// maxpixely/maxpixelx are only ever filled in if we know them.
+API int ncvisual_geom(const struct notcurses* nc, const struct ncvisual* n,
+                      const struct ncvisual_options* vopts, ncvgeom* geom)
+  __attribute__ ((nonnull (4)));
 
 // Destroy an ncvisual. Rendered elements will not be disrupted, but the visual
 // can be neither decoded nor rendered any further.
@@ -4186,6 +4213,18 @@ API uint32_t ncdirect_getc(struct ncdirect* nc, const struct timespec *ts,
                            const void* unused, ncinput* ni)
   __attribute__ ((nonnull (1))) __attribute__ ((deprecated));
 
+// Get the size and ratio of ncvisual pixels to output cells along the y
+// and x axes. The input size (in pixels) will be written to 'y' and 'x'.
+// The scaling will be written to 'scaley' and 'scalex'. With these:
+//  rows = (y / scaley) + !!(y % scaley) or (y + scaley - 1) / scaley
+//  cols = (x / scalex) + !!(x % scalex) or (x + scalex - 1) / scalex
+// Returns non-zero for an invalid 'vopts'. The blitter that will be used
+// is returned in '*blitter'.
+API int ncvisual_blitter_geom(const struct notcurses* nc, const struct ncvisual* n,
+                              const struct ncvisual_options* vopts, int* y, int* x,
+                              int* scaley, int* scalex, ncblitter_e* blitter)
+  __attribute__ ((nonnull (1))) __attribute__ ((deprecated));
+
 #undef API
 #undef ALLOC
 
diff --git a/src/demo/eagle.c b/src/demo/eagle.c
index c38ae1d277..181ed8e031 100644
--- a/src/demo/eagle.c
+++ b/src/demo/eagle.c
@@ -36,8 +36,6 @@ zoom_map(struct notcurses* nc, const char* map, int* ret){
   if(ncv == NULL){
     return NULL;
   }
-  // true height and width of visual, and blitter scaling parameters
-  int vheight, yscale, vwidth, xscale;
   // first we want to get the true size, so don't supply NCSSCALE_STRETCH yet,
   // but *do* explicitly supply NCBLIT_2x2 since we're not scaling.
   struct ncvisual_options vopts = {
@@ -45,11 +43,16 @@ zoom_map(struct notcurses* nc, const char* map, int* ret){
     .blitter = NCBLIT_2x2,
     .flags = NCVISUAL_OPTION_NOINTERPOLATE,
   };
-  if(ncvisual_blitter_geom(nc, ncv, &vopts, &vheight, &vwidth,
-                           &yscale, &xscale, NULL)){
+  ncvgeom geom;
+  if(ncvisual_geom(nc, ncv, &vopts, &geom)){
     ncvisual_destroy(ncv);
     return NULL;
   }
+  // true height and width of visual, and blitter scaling parameters
+  int vheight = geom.pixy;
+  int vwidth = geom.pixx;
+  int yscale = geom.scaley;
+  int xscale = geom.scalex;
 //fprintf(stderr, "VHEIGHT: %d VWIDTH: %d scale: %d/%d\n", vheight, vwidth, yscale, xscale);
   // we start at the lower left corner of the outzoomed map
   int placey, placex; // dimensions of true display
diff --git a/src/demo/intro.c b/src/demo/intro.c
index 3ebfca08ab..8877de5b62 100644
--- a/src/demo/intro.c
+++ b/src/demo/intro.c
@@ -78,28 +78,13 @@ orcashow(struct notcurses* nc, int dimy, int dimx){
     .blitter = NCBLIT_PIXEL,
     .scaling = NCSCALE_STRETCH,
   };
-  int cellpxy, cellpxx;
-  ncplane_pixelgeom(notcurses_stdplane_const(nc), NULL, NULL,
-                     &cellpxy, &cellpxx, NULL, NULL);
-  int odimy, odimx, scaley, scalex;
-  ncvisual_blitter_geom(nc, ncv, &vopts, &odimy, &odimx, &scaley, &scalex, NULL);
-  // even if we can't do pixel output, we want her the same size as if weu
-  // *could* do pixel output. if we have no idea as to the geom, use scale.
-  if(cellpxy == 0){
-    cellpxy = scaley;
-    cellpxx = scalex;
-  }
+  ncvgeom geom;
+  ncvisual_geom(nc, ncv, &vopts, &geom);
   struct ncplane_options nopts = {
-    .rows = (odimy / cellpxy) + !!(odimy % cellpxy),
-    .cols = (odimx / cellpxx) + !!(odimx % cellpxx),
+    .rows = geom.rcelly > dimy / 2 ? dimy / 2 : geom.rcelly,
+    .cols = geom.rcellx > dimx / 4 ? dimx / 4 : geom.rcellx,
     .name = "orca",
   };
-  if(nopts.cols > dimx - 1){
-    nopts.cols = dimx - 1;
-  }
-  if(nopts.rows > dimy - 1){
-    nopts.rows = dimy - 1;
-  }
   nopts.y = dimy - nopts.rows - 1;
   nopts.x = dimx - nopts.cols - 1;
   struct ncplane* n = ncplane_create(notcurses_stdplane(nc), &nopts);
@@ -111,6 +96,7 @@ orcashow(struct notcurses* nc, int dimy, int dimx){
   if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     ncplane_destroy(n);
     ncvisual_destroy(ncv);
+    return NULL;
   }
   ncvisual_destroy(ncv);
   return n;
diff --git a/src/demo/keller.c b/src/demo/keller.c
index 3ff1437784..a5e00441b3 100644
--- a/src/demo/keller.c
+++ b/src/demo/keller.c
@@ -36,18 +36,18 @@ visualize(struct notcurses* nc, struct ncvisual* ncv){
                 | NCVISUAL_OPTION_VERALIGNED
                 | NCVISUAL_OPTION_CHILDPLANE,
     };
-    int scalex, scaley, truey, truex;
     vopts.x = NCALIGN_CENTER;
     vopts.y = NCALIGN_CENTER;
     ncplane_erase(stdn); // to clear out old text
     struct ncplane* n = NULL;
-    if(ncvisual_blitter_geom(nc, ncv, &vopts, &truey, &truex, &scaley, &scalex, NULL) == 0){
+    ncvgeom geom;
+    if(ncvisual_geom(nc, ncv, &vopts, &geom) == 0){
       if( (n = ncvisual_blit(nc, ncv, &vopts)) ){
         ncplane_move_below(n, stdn);
         ncplane_printf_aligned(stdn, ncplane_dim_y(stdn) / 2 - 1, NCALIGN_CENTER,
-                              "%03dx%03d", truex, truey);
+                              "%03dx%03d", geom.pixx, geom.pixy);
         ncplane_printf_aligned(stdn, ncplane_dim_y(stdn) / 2 + 1, NCALIGN_CENTER,
-                              "%d:%d pixels -> cell", scalex, scaley);
+                              "%d:%d pixels -> cell", geom.scalex, geom.scaley);
       }
     }
 //fprintf(stderr, "X: %d truex: %d scalex: %d\n", vopts.x, truex, scalex);
diff --git a/src/demo/luigi.c b/src/demo/luigi.c
index e1f9d7fa0e..ec13d26398 100644
--- a/src/demo/luigi.c
+++ b/src/demo/luigi.c
@@ -219,10 +219,11 @@ int luigi_demo(struct notcurses* nc){
       ncplane_move_top(lastseen);
     }
     ncplane_move_yx(lastseen, yoff, i);
-    int dimy, scaley;
-    ncvisual_blitter_geom(nc, wmncv, NULL, &dimy, NULL, &scaley, NULL, NULL);
-    dimy /= scaley;
-    ncplane_move_yx(wmplane, rows * 4 / 5 - dimy + 1 + (i % 2), i - 60);
+    ncvgeom geom;
+    ncvisual_geom(nc, wmncv, NULL, &geom);
+    geom.pixy /= geom.scaley;
+    // FIXME what the fuck is this
+    ncplane_move_yx(wmplane, rows * 4 / 5 - geom.pixy + 1 + (i % 2), i - 60);
     DEMO_RENDER(nc);
     demo_nanosleep(nc, &stepdelay);
   }
diff --git a/src/demo/normal.c b/src/demo/normal.c
index 571c5c818c..e2e0450731 100644
--- a/src/demo/normal.c
+++ b/src/demo/normal.c
@@ -167,10 +167,10 @@ int normal_demo(struct notcurses* nc){
   struct ncvisual_options vopts = {
     .n = nstd,
   };
-  int yscale, xscale;
-  ncvisual_blitter_geom(nc, NULL, &vopts, NULL, NULL, &yscale, &xscale, NULL);
-  dy *= yscale;
-  dx *= xscale;
+  ncvgeom geom;
+  ncvisual_geom(nc, NULL, &vopts, &geom);
+  dy *= geom.scaley;
+  dx *= geom.scalex;
   uint32_t* rgba = malloc(sizeof(*rgba) * dy * dx);
   if(!rgba){
     goto err;
@@ -179,10 +179,10 @@ int normal_demo(struct notcurses* nc){
     rgba[off] = 0xff000000;
   }
   int y;
-  if(dy / yscale % 2){
-    y = dy / yscale + 1;
+  if(dy / geom.scaley % 2){
+    y = dy / geom.scaley + 1;
     for(int x = 0 ; x < dx ; ++x){
-      if(mcell(offset(rgba, y, x, dx), y, x, dy / yscale, dx)){
+      if(mcell(offset(rgba, y, x, dx), y, x, dy / geom.scaley, dx)){
         goto err;
       }
     }
diff --git a/src/demo/yield.c b/src/demo/yield.c
index c73ac6b2ac..14b398ffa4 100644
--- a/src/demo/yield.c
+++ b/src/demo/yield.c
@@ -4,16 +4,14 @@
 // the only dynamic information which needs be shared between the two threads
 // is the last polyfill origin, the total filled count, and whose turn it is.
 
-static int maxy;
-static int maxx;
-static int total;
-
 struct marsh {
   int id;                 // unique for each thread, mono-increasing
   int* turn;              // whose turn it is to prep the ncvisual, shared
   int* rturn;             // whose turn it is to render, shared
   int* polyy;             // last shared polyfill origin (y)
   int* polyx;             // last shared polyfill origin (x)
+  int maxy;               // pixels, y dimension
+  int maxx;               // pixels, x dimension
   uint32_t* polypixel;    // last shared polyfill pixel (rgba)
   int* filled;            // shared, how many have we filled?
   unsigned* done;         // shared, are we done?
@@ -38,13 +36,13 @@ yielder(struct marsh* m){
   int iters = 0;
   // less than this, and we exit almost immediately. more than this, and we
   // run closer to twenty seconds. 11/50 it is, then. pixels are different.
-  const long threshold_painted = total * 11 / 50;
+  const long threshold_painted = m->maxy * m->maxx * 11 / 50;
   pthread_mutex_lock(&lock);
   while(*m->turn != m->id && !*m->done){
     pthread_cond_wait(&cond, &lock);
   }
   while(*m->filled < threshold_painted && iters < MAXITER && !*m->done){
-//fprintf(stderr, "%d/%d tfilled: %ld thresh: %ld total: %ld\n", maxy, maxx, *m->filled, threshold_painted, total);
+//fprintf(stderr, "%d/%d tfilled: %d thresh: %ld total: %d\n", m->maxy, m->maxx, *m->filled, threshold_painted, m->maxy * m->maxx);
     int pfilled = 0;
     // the first time the first thread runs, it does not pick up the previous
     // polyfill origin (as there was none). all other runs, we do.
@@ -54,8 +52,8 @@ yielder(struct marsh* m){
     int filledcopy;
     do{
       ++iters;
-      int x = rand() % maxx;
-      int y = rand() % maxy;
+      int x = rand() % m->maxx;
+      int y = rand() % m->maxy;
       ncvisual_at_yx(m->ncv, y, x, m->polypixel);
       if(ncpixel_a(*m->polypixel) != 0xff){ // don't do areas we've already done
         continue;
@@ -237,7 +235,8 @@ int yield_demo(struct notcurses* nc){
   ncplane_set_fg_rgb8(label, 0xff, 0xff, 0xff);
   ncplane_set_styles(label, NCSTYLE_BOLD);
   ncplane_printf_aligned(label, 0, NCALIGN_CENTER, "Yield: %03.1f%%", 0.0);
-  if(ncvisual_blitter_geom(nc, v1, &m1.vopts, &maxy, &maxx, NULL, NULL, NULL)){
+  ncvgeom geom;
+  if(ncvisual_geom(nc, v1, &m1.vopts, &geom)){
     ncplane_destroy(label);
     ncvisual_destroy(v1);
     ncvisual_destroy(v2);
@@ -253,7 +252,8 @@ int yield_demo(struct notcurses* nc){
     ncplane_destroy(m1.vopts.n);
     return -1;
   }
-  total = maxx * maxy;
+  m1.maxy = m2.maxy = geom.pixy;
+  m1.maxx = m2.maxx = geom.pixx;
 
   DEMO_RENDER(nc);
 
diff --git a/src/lib/blitset.h b/src/lib/blitset.h
index 6980e3b897..eb564c9cc7 100644
--- a/src/lib/blitset.h
+++ b/src/lib/blitset.h
@@ -1,7 +1,9 @@
 #ifndef NOTCURSES_BLITSET
 #define NOTCURSES_BLITSET
 
-#include "notcurses/notcurses.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
 
 // number of pixels that map to a single cell, height-wise
 static inline int
@@ -50,4 +52,8 @@ ncplot_defblitter(const notcurses* nc){
 
 void set_pixel_blitter(ncblitter blitfxn);
 
+#ifdef __cplusplus
+}
+#endif
+
 #endif
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 82c24540d8..217772c311 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -1590,24 +1590,11 @@ ncdirectv* ncdirectf_render(ncdirect* n, ncdirectf* frame, const struct ncvisual
 
 int ncdirectf_geom(ncdirect* n, ncdirectf* frame,
                    const struct ncvisual_options* vopts, ncvgeom* geom){
-  geom->cdimy = n->tcache.cellpixy;
-  geom->cdimx = n->tcache.cellpixx;
-  geom->maxpixely = n->tcache.sixel_maxy;
-  geom->maxpixelx = n->tcache.sixel_maxx;
   const struct blitset* bset;
-  int r = ncvisual_blitset_geom(NULL, &n->tcache, frame, vopts,
-                                &geom->pixy, &geom->pixx,
-                                &geom->scaley, &geom->scalex,
-                                &geom->rpixy, &geom->rpixx, &bset);
-  if(r == 0){
-    // FIXME ncvisual_blitset_geom() ought calculate these two for us; until
-    // then, derive them ourselves. the row count might be short by one if
-    // we're using sixel, and we're not a multiple of 6
-    geom->rcelly = geom->pixy / geom->scaley;
-    geom->rcellx = geom->pixx / geom->scalex;
-    geom->blitter = bset->geom;
-  }
-  return r;
+  int disppxy, disppxx, outy, outx, placey, placex;
+  return ncvisual_geom_inner(&n->tcache, frame, vopts, geom, &bset,
+                             &disppxy, &disppxx, &outy, &outx,
+                             &placey, &placex);
 }
 
 unsigned ncdirect_supported_styles(const ncdirect* nc){
diff --git a/src/lib/fd.c b/src/lib/fd.c
index 7d45ccca1c..b70052b996 100644
--- a/src/lib/fd.c
+++ b/src/lib/fd.c
@@ -457,12 +457,12 @@ int get_tty_fd(FILE* ttyfp){
   int fd = -1;
   if(ttyfp){
     if((fd = fileno(ttyfp)) < 0){
-      logwarn("No file descriptor was available in outfp %p\n", ttyfp);
+      logwarn("no file descriptor was available in outfp %p\n", ttyfp);
     }else{
       if(tty_check(fd)){
         fd = dup(fd);
       }else{
-        loginfo("fd %d not a TTY\n", fd);
+        loginfo("fd %d is not a TTY\n", fd);
         fd = -1;
       }
     }
@@ -470,15 +470,17 @@ int get_tty_fd(FILE* ttyfp){
   if(fd < 0){
     fd = open("/dev/tty", O_RDWR | O_CLOEXEC | O_NOCTTY);
     if(fd < 0){
-      loginfo("Error opening /dev/tty (%s)\n", strerror(errno));
+      loginfo("couldn't open /dev/tty (%s)\n", strerror(errno));
     }else{
       if(!tty_check(fd)){
-        loginfo("File descriptor for /dev/tty (%d) is not actually a TTY\n", fd);
+        loginfo("file descriptor for /dev/tty (%d) is not actually a TTY\n", fd);
         close(fd);
         fd = -1;
       }
     }
   }
-  loginfo("Returning TTY fd %d\n", fd);
+  if(fd >= 0){
+    loginfo("returning TTY fd %d\n", fd);
+  }
   return fd;
 }
diff --git a/src/lib/fill.c b/src/lib/fill.c
index 4679fad17b..c18b99c301 100644
--- a/src/lib/fill.c
+++ b/src/lib/fill.c
@@ -644,8 +644,10 @@ int ncplane_qrcode(ncplane* n, int* ymax, int* xmax, const void* data, size_t le
         if(ncvisual_blit(ncplane_notcurses(n), ncv, &vopts) == n){
           ret = square;
         }
-        ncvisual_blitter_geom(ncplane_notcurses(n), ncv, &vopts, NULL, NULL,
-                              &yscale, &xscale, NULL);
+        ncvgeom geom;
+        ncvisual_geom(ncplane_notcurses(n), NULL, &vopts, &geom);
+        yscale = geom.scaley;
+        xscale = geom.scalex;
       }
       ncvisual_destroy(ncv);
     }
diff --git a/src/lib/in.c b/src/lib/in.c
index 401675f7a8..6e3d97a37e 100644
--- a/src/lib/in.c
+++ b/src/lib/in.c
@@ -2125,7 +2125,7 @@ internal_get(inputctx* ictx, const struct timespec* ts, ncinput* ni){
 }
 
 // FIXME kill off for API3, and expect an absolute deadline directly
-static void
+static inline void
 delaybound_to_deadline(const struct timespec* ts, struct timespec* absdl){
   if(ts){
     // incoming ts is a delay bound, but we want an absolute deadline for
diff --git a/src/lib/internal.h b/src/lib/internal.h
index ce089bb9fc..c0c377e2b9 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -388,12 +388,6 @@ struct blitset {
 
 #include "blitset.h"
 
-int ncvisual_blitset_geom(const notcurses* nc, const tinfo* tcache,
-                          const struct ncvisual* n,
-                          const struct ncvisual_options* vopts,
-                          int* y, int* x, int* scaley, int* scalex,
-                          int* leny, int* lenx, const struct blitset** blitter);
-
 void reset_stats(ncstats* stats);
 void summarize_stats(notcurses* nc);
 
@@ -1561,6 +1555,12 @@ rgba_blit_dispatch(ncplane* nc, const struct blitset* bset,
   return bset->blit(nc, linesize, data, leny, lenx, bargs);
 }
 
+int ncvisual_geom_inner(const tinfo* ti, const struct ncvisual* n,
+                        const struct ncvisual_options* vopts, ncvgeom* geom,
+                        const struct blitset** bset,
+                        int* disppxy, int* disppxx, int* outy, int* outx,
+                        int* placey, int* placex);
+
 static inline const struct blitset*
 rgba_blitter_low(const tinfo* tcache, ncscale_e scale, bool maydegrade,
                  ncblitter_e blitrec) {
diff --git a/src/lib/kitty.c b/src/lib/kitty.c
index c13071e2a9..6d6f62f86b 100644
--- a/src/lib/kitty.c
+++ b/src/lib/kitty.c
@@ -1129,6 +1129,7 @@ int kitty_scrub(const ncpile* p, sprixel* s){
   for(int yy = s->movedfromy ; yy < s->movedfromy + s->dimy && yy < p->dimy ; ++yy){
     for(int xx = s->movedfromx ; xx < s->movedfromx + s->dimx && xx < p->dimx ; ++xx){
       const int ridx = yy * p->dimx + xx;
+      assert(0 <= ridx);
       struct crender *r = &p->crender[ridx];
       if(!r->sprixel){
         if(s->n){
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 745a2d7869..f6ac1de122 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -421,22 +421,22 @@ make_ncpile(notcurses* nc, ncplane* n){
 ncplane* ncplane_new_internal(notcurses* nc, ncplane* n,
                               const ncplane_options* nopts){
   if(nopts->flags >= (NCPLANE_OPTION_FIXED << 1u)){
-    logwarn("Provided unsupported flags %016" PRIx64 "\n", nopts->flags);
+    logwarn("provided unsupported flags %016" PRIx64 "\n", nopts->flags);
   }
   if(nopts->flags & NCPLANE_OPTION_HORALIGNED || nopts->flags & NCPLANE_OPTION_VERALIGNED){
     if(n == NULL){
-      logerror("Alignment requires a parent plane\n");
+      logerror("alignment requires a parent plane\n");
       return NULL;
     }
   }
   if(nopts->flags & NCPLANE_OPTION_MARGINALIZED){
     if(nopts->rows != 0 || nopts->cols != 0){
-      logerror("Geometry specified with margins (r=%d, c=%d)\n",
+      logerror("geometry specified with margins (r=%d, c=%d)\n",
                nopts->rows, nopts->cols);
       return NULL;
     }
   }else if(nopts->rows <= 0 || nopts->cols <= 0){
-    logerror("Won't create denormalized plane (r=%d, c=%d)\n",
+    logerror("won't create denormalized plane (r=%d, c=%d)\n",
              nopts->rows, nopts->cols);
     return NULL;
   }
@@ -467,7 +467,7 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n,
   }
   size_t fbsize = sizeof(*p->fb) * (p->leny * p->lenx);
   if((p->fb = malloc(fbsize)) == NULL){
-    logerror("Error allocating cellmatrix (r=%d, c=%d)\n",
+    logerror("error allocating cellmatrix (r=%d, c=%d)\n",
              p->leny, p->lenx);
     free(p);
     return NULL;
@@ -540,7 +540,7 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n,
       pthread_mutex_unlock(&nc->stats.lock);
     pthread_mutex_unlock(&nc->pilelock);
   }
-  loginfo("Created new %dx%d plane \"%s\" @ %dx%d\n",
+  loginfo("created new %dx%d plane \"%s\" @ %dx%d\n",
           p->leny, p->lenx, p->name ? p->name : "", p->absy, p->absx);
   return p;
 }
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index d8a3952fb6..e041d95747 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -38,7 +38,7 @@ static void
 get_default_geometry(tinfo* ti){
   ti->default_rows = get_default_dimension("LINES", "lines", 24);
   ti->default_cols = get_default_dimension("COLUMNS", "cols", 80);
-  loginfo("Default geometry: %d row%s, %d column%s\n",
+  loginfo("default geometry: %d row%s, %d column%s\n",
           ti->default_rows, ti->default_rows != 1 ? "s" : "",
           ti->default_cols, ti->default_cols != 1 ? "s" : "");
   ti->dimy = ti->default_rows;
@@ -341,6 +341,11 @@ init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
 // supported. these ought follow keyboard push and precede kitty keyboard.
 #define XTMODKEYS "\x1b[>2;1m\x1b[>4;1m"
 
+// disable key modifier options; this corresponds to a resource value of
+// "-1", which cannot be set with the [>m sequence. supposedly, "[>m" by
+// itself ought reset all of them, but this doesn't seem to work FIXME.
+#define XTMODKEYSUNDO "\x1b[>2n\x1b[>4n"
+
 // these queries can hopefully uniquely and unquestionably identify the
 // terminal to which we are talking. if we already know what we're talking
 // to, there's no point in sending them.
@@ -393,7 +398,7 @@ init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
 // written whenever we switch between standard and alternate screen, or upon
 // startup (that's an entry into a screen! presumably the standard one).
 #define KBDENTER KKEYBOARD_PUSH XTMODKEYS KBDSUPPORT
-#define KBDLEAVE KKEYBOARD_POP // FIXME undo XTMODKEYS!
+#define KBDLEAVE KKEYBOARD_POP XTMODKEYSUNDO
 
 // enter the alternate screen (smcup). we could technically get this from
 // terminfo, but everyone who supports it supports it the same way, and we
diff --git a/src/lib/visual.c b/src/lib/visual.c
index 523b3af173..b559570986 100644
--- a/src/lib/visual.c
+++ b/src/lib/visual.c
@@ -126,165 +126,346 @@ ncvisual_origin(const struct ncvisual_options* vopts, int* restrict begy, int* r
   *begx = vopts ? vopts->begx : 0;
 }
 
-// 'leny' and 'lenx' get the number of pixels to actually be rendered, 'y' and
-// 'x' get the original size of the visual in pixels, and 'scaley' and 'scalex'
-// get the number of pixels per cell with the selected 'blitter'.
-// FIXME we ought also do the output calculations here (how many rows x cols,
-//   given the input plane vopts->n and scaling vopts->scaling)--but do not
-//   perform any actual scaling, nor create any planes!
-// takes tcache distinctly; nc is used only for logging, and can be NULL
-int ncvisual_blitset_geom(const notcurses* nc, const tinfo* tcache,
-                          const ncvisual* n, const struct ncvisual_options* vopts,
+
+int ncvisual_blitter_geom(const notcurses* nc, const ncvisual* n,
+                          const struct ncvisual_options* vopts,
                           int* y, int* x, int* scaley, int* scalex,
-                          int* leny, int* lenx, const struct blitset** blitter){
-  int fakeleny, fakelenx;
-  if(leny == NULL){
-    leny = &fakeleny;
+                          ncblitter_e* blitter){
+  ncvgeom geom;
+  const struct blitset* bset;
+  int disppxy, disppxx, outy, outx, placey, placex;
+  if(ncvisual_geom_inner(&nc->tcache, n, vopts, &geom, &bset,
+                         &disppxy, &disppxx, &outy, &outx,
+                         &placey, &placex)){
+    return -1;
   }
-  if(lenx == NULL){
-    lenx = &fakelenx;
+  if(y){
+    *y = geom.pixy;
   }
-  if(vopts && vopts->flags >= (NCVISUAL_OPTION_NOINTERPOLATE << 1u)){
-    logwarn("Warning: unknown ncvisual options %016" PRIx64 "\n", vopts->flags);
+  if(x){
+    *x = geom.pixx;
   }
-  if(vopts && (vopts->flags & NCVISUAL_OPTION_CHILDPLANE) && !vopts->n){
-    logerror("Requested child plane with NULL n\n");
-    return -1;
+  if(scaley){
+    *scaley = geom.scaley;
   }
-  int begy, begx;
-  ncvisual_origin(vopts, &begy, &begx);
-  *lenx = vopts ? vopts->lenx : 0;
-  *leny = vopts ? vopts->leny : 0;
-//fprintf(stderr, "blit %dx%d+%dx%d %p\n", begy, begx, *leny, *lenx, ncv->data);
-  if(begy < 0 || begx < 0 || *lenx <= -1 || *leny <= -1){
-    logerror("Invalid geometry for visual %d %d %d %d\n", begy, begx, *leny, *lenx);
-    return -1;
+  if(scalex){
+    *scalex = geom.scalex;
   }
-  if(n){
-//fprintf(stderr, "OUR DATA: %p rows/cols: %d/%d\n", n->data, n->pixy, n->pixx);
-    if(n->data == NULL){
-      logerror("No data in visual\n");
-      return -1;
+  if(blitter){
+    *blitter = bset->geom;
+  }
+  return 0;
+}
+
+// create a plane in which to blit the sprixel. |disppixx| and |disppixy| are
+// scaled pixel geometry on output, and unused on input. |placey| and |placex|
+// are used to position the new plane, and reset to 0 on output. |outy| and
+// |outx| are true output geometry on output, and unused on input (actual input
+// pixel geometry come from ncv->pixy and ncv->pixx).
+// |pxoffy| and |pxoffx| are pixel offset within the origin cell. they are not
+// included within |disppixx| nor |disppixy|, but count towards |outx| and
+// |outy|. these last two are furthermore clamped to sixel maxima, and |outy|
+// accounts for sixels being a multiple of six pixels tall.
+static void
+shape_sprixel_plane(const tinfo* ti, ncplane* parent, const ncvisual* ncv,
+                    ncscale_e scaling, int* disppixy, int* disppixx,
+                    uint64_t flags, int* outy, int* outx,
+                    int* placey, int* placex, int pxoffy, int pxoffx){
+  if(scaling != NCSCALE_NONE && scaling != NCSCALE_NONE_HIRES){
+    // disppixy/disppix are treated initially as cells
+    if(parent == NULL){
+      *disppixy = ti->dimy;
+      *disppixx = ti->dimx;
+    }else{
+      ncplane_dim_yx(parent, disppixy, disppixx);
     }
-//fprintf(stderr, "blit %d/%d to %dx%d+%dx%d scaling: %d\n", n->pixy, n->pixx, begy, begx, *leny, *lenx, vopts ? vopts->scaling : 0);
-    if(begx >= n->pixx || begy >= n->pixy){
-      logerror("Visual too large %d > %d or %d > %d\n", begy, n->pixy, begx, n->pixx);
-      return -1;
+    // FIXME why do we clamp only vertical, not horizontal, here?
+    if(*placey + *disppixy >= ti->dimy){
+      *disppixy = ti->dimy - *placey;
+    }
+    if(!(flags & NCVISUAL_OPTION_VERALIGNED)){
+      *disppixy -= *placey;
     }
-    if(*lenx == 0){ // 0 means "to the end"; use all available source material
-      *lenx = n->pixx - begx;
+    if(!(flags & NCVISUAL_OPTION_HORALIGNED)){
+      *disppixx -= *placex;
     }
-    if(*leny == 0){
-      *leny = n->pixy - begy;
+    *disppixx *= ti->cellpixx;
+    *disppixy *= ti->cellpixy;
+    *disppixx += pxoffx;
+    *disppixy += pxoffy;
+    *outx = *disppixx;
+    clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
+    if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
+      scale_visual(ncv, disppixy, disppixx); // can only shrink
+      *outx = *disppixx;
+      clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
     }
-//fprintf(stderr, "blit %d/%d to %dx%d+%dx%d scaling: %d flags: 0x%016lx\n", n->pixy, n->pixx, begy, begx, *leny, *lenx, vopts ? vopts->scaling : 0, vopts ? vopts->flags : 0);
-    if(*lenx <= 0 || *leny <= 0){ // no need to draw zero-size object, exit
-      logerror("Zero-size object %d %d\n", *leny, *lenx);
+  }else{
+    *disppixx = ncv->pixx + pxoffx;
+    *disppixy = ncv->pixy + pxoffy;
+    *outx = *disppixx;
+    clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
+  }
+  // pixel offsets ought be counted for clamping purposes, but not returned
+  // as part of the scaled geometry (they are included in outx/outy).
+  *disppixy -= pxoffy;
+  *disppixx -= pxoffx;
+}
+
+// in addition to the geom fields, we pass out:
+//  * 'disppixx'/'disppixy': scaled output size in pixels
+//  * 'outy'/'outx': true output size in pixels (ie post-sixel clamping)
+//  * 'placey'/'placex': offset at which to draw
+int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
+                        const struct ncvisual_options* vopts, ncvgeom* geom,
+                        const struct blitset** bset,
+                        int* disppixy, int* disppixx,
+                        int* outy, int* outx,
+                        int* placey, int* placex){
+  if(ti == NULL && n == NULL){
+    logerror("got NULL for both sources\n");
+    return -1;
+  }
+  struct ncvisual_options fakevopts;
+  if(vopts == NULL){
+    memset(&fakevopts, 0, sizeof(fakevopts));
+    vopts = &fakevopts;
+  }
+  // check basic vopts preconditions
+  if(vopts->flags >= (NCVISUAL_OPTION_NOINTERPOLATE << 1u)){
+    logwarn("warning: unknown ncvisual options %016" PRIx64 "\n", vopts->flags);
+  }
+  if((vopts->flags & NCVISUAL_OPTION_CHILDPLANE) && !vopts->n){
+    logerror("requested child plane with NULL n\n");
+    return -1;
+  }
+  if(vopts->flags & NCVISUAL_OPTION_HORALIGNED){
+    if(vopts->x < NCALIGN_UNALIGNED || vopts->x > NCALIGN_RIGHT){
+      logerror("bad x %d for horizontal alignment\n", vopts->x);
       return -1;
     }
-    if(begx + *lenx > n->pixx || begy + *leny > n->pixy){
-      logerror("Geometry too large %d > %d or %d > %d\n", begy + *leny, n->pixy, begx + *lenx, n->pixx);
+  }
+  if(vopts->flags & NCVISUAL_OPTION_VERALIGNED){
+    if(vopts->y < NCALIGN_UNALIGNED || vopts->y > NCALIGN_RIGHT){
+      logerror("bad y %d for vertical alignment\n", vopts->y);
       return -1;
     }
   }
-  const struct blitset* bset = rgba_blitter(tcache, vopts);
-  if(!bset){
-    logerror("Couldn't get a blitter for %d\n", vopts ? vopts->blitter : NCBLIT_DEFAULT);
+  if(n){
+    geom->pixy = n->pixy;
+    geom->pixx = n->pixx;
+  }
+  // when nc is NULL, we only report properties intrinsic to the ncvisual,
+  // i.e. only its original pixel geometry.
+  if(ti == NULL){
+    return 0;
+  }
+  // determine our blitter
+  *bset = rgba_blitter(ti, vopts);
+  if(!*bset){
+    logerror("couldn't get a blitter for %d\n", vopts ? vopts->blitter : NCBLIT_DEFAULT);
     return -1;
   }
-  if(blitter){
-    *blitter = bset;
-  }
-  if(vopts){
-    if(bset->geom == NCBLIT_PIXEL){
-      if(vopts->n){
-        if(vopts->n == notcurses_stdplane_const(nc)){
-          if(!(vopts->flags & NCVISUAL_OPTION_CHILDPLANE)){
-            logerror("Won't blit bitmaps to the standard plane\n");
-            return -1;
-          }
-        }
-        if(vopts->y && !(vopts->flags & (NCVISUAL_OPTION_VERALIGNED | NCVISUAL_OPTION_CHILDPLANE))){
-          logerror("Non-origin y placement %d for sprixel\n", vopts->y);
+  geom->cdimy = ti->cellpixy;
+  geom->cdimx = ti->cellpixx;
+  if((geom->blitter = (*bset)->geom) == NCBLIT_PIXEL){
+    geom->maxpixely = ti->sixel_maxy_pristine;
+    geom->maxpixelx = ti->sixel_maxx;
+  }
+  geom->scaley = encoding_y_scale(ti, *bset);
+  geom->scalex = encoding_x_scale(ti, *bset);
+  // when n is NULL, we only report properties unrelated to the ncvisual,
+  // i.e. the cell-pixel geometry, max bitmap geometry, blitter, and scaling.
+  if(n == NULL){
+    return 0;
+  }
+  ncscale_e scaling = vopts ? vopts->scaling : NCSCALE_NONE;
+  // determine how much of the original image we're using (leny/lenx)
+  ncvisual_origin(vopts, &geom->begy, &geom->begx);
+  geom->lenx = vopts->lenx;
+  geom->leny = vopts->leny;
+  *placey = vopts->y;
+  *placex = vopts->x;
+  logdebug("vis %dx%d+%dx%d %p\n", geom->begy, geom->begx, geom->leny, geom->lenx, n->data);
+  if(geom->begy < 0 || geom->begx < 0){
+    logerror("invalid geometry for visual %d %d %d %d\n", geom->begy, geom->begx, geom->leny, geom->lenx);
+    return -1;
+  }
+  if(n->data == NULL){
+    logerror("no data in visual\n");
+    return -1;
+  }
+  if(geom->begx >= n->pixx || geom->begy >= n->pixy){
+    logerror("visual too large %d > %d or %d > %d\n", geom->begy, n->pixy, geom->begx, n->pixx);
+    return -1;
+  }
+  if(geom->lenx == 0){ // 0 means "to the end"; use all available source material
+    geom->lenx = n->pixx - geom->begx;
+  }
+  if(geom->leny == 0){
+    geom->leny = n->pixy - geom->begy;
+  }
+  if(geom->lenx <= 0 || geom->leny <= 0){ // no need to draw zero-size object, exit
+    logerror("zero-size object %d %d\n", geom->leny, geom->lenx);
+    return -1;
+  }
+  if(geom->begx + geom->lenx > n->pixx || geom->begy + geom->leny > n->pixy){
+    logerror("geometry too large %d > %d or %d > %d\n", geom->begy + geom->leny, n->pixy, geom->begx + geom->lenx, n->pixx);
+    return -1;
+  }
+  if((*bset)->geom == NCBLIT_PIXEL){
+    if(vopts->n){
+      // FIXME does this work from direct mode?
+      if(vopts->n == notcurses_stdplane_const(ncplane_notcurses_const(vopts->n))){
+        if(!(vopts->flags & NCVISUAL_OPTION_CHILDPLANE)){
+          logerror("won't blit bitmaps to the standard plane\n");
           return -1;
         }
-        if(vopts->x && !(vopts->flags & (NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_CHILDPLANE))){
-          logerror("Non-origin x placement %d for sprixel\n", vopts->x);
+      }
+      if(vopts->y && !(vopts->flags & (NCVISUAL_OPTION_VERALIGNED | NCVISUAL_OPTION_CHILDPLANE))){
+        logerror("non-origin y placement %d for sprixel\n", vopts->y);
+        return -1;
+      }
+      if(vopts->x && !(vopts->flags & (NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_CHILDPLANE))){
+        logerror("non-origin x placement %d for sprixel\n", vopts->x);
+        return -1;
+      }
+      if(vopts->pxoffy >= ti->cellpixy){
+        logerror("pixel y-offset %d too tall for cell %d\n", vopts->pxoffy, ti->cellpixy);
+        return -1;
+      }
+      if(vopts->pxoffx >= ti->cellpixx){
+        logerror("pixel x-offset %d too wide for cell %d\n", vopts->pxoffx, ti->cellpixx);
+        return -1;
+      }
+      if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
+        // FIXME clamp to sprixel limits
+        int rows = ((geom->leny + ti->cellpixy - 1) / ti->cellpixy) + !!vopts->pxoffy;
+        if(rows > ncplane_dim_y(vopts->n)){
+          logerror("sprixel too tall %d for plane %d\n", geom->leny + vopts->pxoffy,
+                   ncplane_dim_y(vopts->n) * ti->cellpixy);
           return -1;
         }
-        // FIXME clamp to sprixel limits
-        if(vopts->scaling == NCSCALE_NONE || vopts->scaling == NCSCALE_NONE_HIRES){
-          if(vopts->pxoffy >= nc->tcache.cellpixy){
-            logerror("pixel y-offset %d too tall for cell %d\n", vopts->pxoffy, nc->tcache.cellpixy);
-            return -1;
-          }
-          int rows = ((*leny + nc->tcache.cellpixy - 1) / nc->tcache.cellpixy)
-                      + !!vopts->pxoffy;
-          if(rows > ncplane_dim_y(vopts->n)){
-            logerror("sprixel too tall %d for plane %d\n",
-                     *leny + vopts->pxoffy, ncplane_dim_y(vopts->n) * nc->tcache.cellpixy);
-            return -1;
-          }
-          if(vopts->pxoffx >= nc->tcache.cellpixx){
-            logerror("pixel x-offset %d too wide for cell %d\n", vopts->pxoffx, nc->tcache.cellpixx);
-            return -1;
-          }
-          int cols = ((*lenx + nc->tcache.cellpixx - 1) / nc->tcache.cellpixx)
-                      + !!vopts->pxoffx;
-          if(cols > ncplane_dim_x(vopts->n)){
-            logerror("sprixel too wide %d for plane %d\n",
-                     *lenx + vopts->pxoffx, ncplane_dim_x(vopts->n) * nc->tcache.cellpixx);
-            return -1;
-          }
+        int cols = ((geom->lenx + ti->cellpixx - 1) / ti->cellpixx) + !!vopts->pxoffx;
+        if(cols > ncplane_dim_x(vopts->n)){
+          logerror("sprixel too wide %d for plane %d\n", geom->lenx + vopts->pxoffx,
+                   ncplane_dim_x(vopts->n) * ti->cellpixx);
+          return -1;
         }
       }
+    }
+    if(vopts->n == NULL || (vopts->flags & NCVISUAL_OPTION_CHILDPLANE)){
+      // we'll need to create the plane
+      shape_sprixel_plane(ti, vopts->n, n, scaling, disppixy, disppixx,
+                          vopts->flags, outy, outx, placey, placex,
+                          vopts->pxoffy, vopts->pxoffx);
     }else{
-      if(vopts->pxoffx || vopts->pxoffy){
-        logerror("pixel offsets cannot be used with cell blitting\n");
-        return -1;
+      if(scaling != NCSCALE_NONE && scaling != NCSCALE_NONE_HIRES){
+        ncplane_dim_yx(vopts->n, disppixy, disppixx);
+        *disppixx *= ti->cellpixx;
+        *disppixx += vopts->pxoffx;
+        *disppixy *= ti->cellpixy;
+        *disppixy += vopts->pxoffy;
+        clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
+        int absplacex = 0, absplacey = 0;
+        if(!(vopts->flags & NCVISUAL_OPTION_HORALIGNED)){
+          absplacex = *placex;
+        }
+        if(!(vopts->flags & NCVISUAL_OPTION_VERALIGNED)){
+          absplacey = *placey;
+        }
+        *disppixx -= absplacex * ti->cellpixx;
+        *disppixy -= absplacey * ti->cellpixy;
+      }else{
+        *disppixx = geom->lenx + vopts->pxoffx;
+        *disppixy = geom->leny + vopts->pxoffy;
       }
+      logdebug("pixel prescale: %d %d %d %d\n", n->pixy, n->pixx, *disppixy, *disppixx);
+      if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
+        clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
+        scale_visual(n, disppixy, disppixx);
+      }
+      clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
+      // FIXME use a closed form
+      while((*outy + ti->cellpixy - 1) / ti->cellpixy > (unsigned)ncplane_dim_y(vopts->n)){
+        *outy -= ti->sprixel_scale_height;
+        *disppixy = *outy;
+      }
+      *outx = *disppixx;
+      *disppixx -= vopts->pxoffx;
+      *disppixy -= vopts->pxoffy;
     }
-  }
-  if(n){
-    if(y){
-      *y = n->pixy;
-    }
-    if(x){
-      *x = n->pixx;
-    }
-  }
-  if(scaley){
-    *scaley = encoding_y_scale(tcache, bset);
-  }
-  if(scalex){
-    *scalex = encoding_x_scale(tcache, bset);
-  }
-  if(vopts && vopts->flags & NCVISUAL_OPTION_HORALIGNED){
-    if(vopts->x < NCALIGN_UNALIGNED || vopts->x > NCALIGN_RIGHT){
-      logerror("Bad x %d for horizontal alignment\n", vopts->x);
+    logdebug("pblit: %dx%d ← %dx%d of %d/%d stride %u @%dx%d %p %u\n", *disppixy, *disppixx, geom->begy, geom->begx, n->pixy, n->pixx, n->rowstride, *placey, *placex, n->data, ti->cellpixx);
+    geom->rpixy = *disppixy;
+    geom->rpixx = *disppixx;
+    geom->rcellx = *outx / ti->cellpixx + !!(*outx % ti->cellpixx);
+    geom->rcelly = *outy / ti->cellpixy + !!(*outy % ti->cellpixy);
+  }else{ // cellblit
+    if(vopts->pxoffx || vopts->pxoffy){
+      logerror("pixel offsets cannot be used with cell blitting\n");
       return -1;
     }
-  }
-  if(vopts && vopts->flags & NCVISUAL_OPTION_VERALIGNED){
-    if(vopts->y < NCALIGN_UNALIGNED || vopts->y > NCALIGN_RIGHT){
-      logerror("Bad y %d for vertical alignment\n", vopts->y);
-      return -1;
+    int dispcols, disprows;
+    if(vopts->n == NULL || (vopts->flags & NCVISUAL_OPTION_CHILDPLANE)){ // create plane
+//fprintf(stderr, "CPATH1, create beg %dx%d len %dx%d\n", geom->begy, geom->begx, geom->leny, geom->lenx);
+      if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
+        dispcols = geom->lenx;
+        disprows = geom->leny;
+      }else{
+        if(vopts->n == NULL){
+          disprows = ti->dimy;
+          dispcols = ti->dimx;
+        }else{
+          ncplane_dim_yx(vopts->n, &disprows, &dispcols);
+        }
+        dispcols *= geom->scalex;
+        disprows *= geom->scaley;
+        if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
+          scale_visual(n, &disprows, &dispcols);
+        } // else stretch
+      }
+    }else{
+//fprintf(stderr, "CPATH2, reuse beg %dx%d len %dx%d\n", geom->begy, geom->begx, geom->leny, geom->lenx);
+      if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
+        dispcols = geom->lenx;
+        disprows = geom->leny;
+      }else{
+        ncplane_dim_yx(vopts->n, &disprows, &dispcols);
+        dispcols *= geom->scalex;
+        disprows *= geom->scaley;
+        if(!(vopts->flags & NCVISUAL_OPTION_HORALIGNED)){
+          dispcols -= *placex;
+        }
+        if(!(vopts->flags & NCVISUAL_OPTION_VERALIGNED)){
+          disprows -= *placey;
+        }
+        if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
+          scale_visual(n, &disprows, &dispcols);
+        } // else stretch
+      }
+      if(vopts->flags & NCVISUAL_OPTION_HORALIGNED){
+        *placex = ncplane_halign(vopts->n, *placex, dispcols / geom->scalex);
+      }
+      if(vopts->flags & NCVISUAL_OPTION_VERALIGNED){
+        *placey = ncplane_valign(vopts->n, *placey, disprows / geom->scaley);
+      }
     }
+    geom->rpixy = disprows;
+    geom->rpixx = dispcols;
+    geom->rcellx = dispcols / geom->scalex + !!(dispcols % geom->scalex);
+    geom->rcelly = disprows / geom->scaley + !!(disprows % geom->scaley);
   }
+  logdebug("rgeom: %d %d %d %d @ %d/%d (%d on %p)\n", geom->rcelly, geom->rcellx,
+           geom->rpixy, geom->rpixx, *placey, *placex, (*bset)->geom, vopts->n);
   return 0;
 }
 
-int ncvisual_blitter_geom(const notcurses* nc, const ncvisual* n,
-                          const struct ncvisual_options* vopts,
-                          int* y, int* x, int* scaley, int* scalex,
-                          ncblitter_e* blitter){
+int ncvisual_geom(const notcurses* nc, const ncvisual* n,
+                  const struct ncvisual_options* vopts, ncvgeom* geom){
   const struct blitset* bset;
-  int ret = ncvisual_blitset_geom(nc, &nc->tcache, n, vopts, y, x,
-                                  scaley, scalex, NULL, NULL, &bset);
-  if(ret == 0 && blitter){
-    *blitter = bset->geom;
-  }
-  return ret;
+  int disppxy, disppxx, outy, outx, placey, placex;
+  return ncvisual_geom_inner(nc ? &nc->tcache : NULL, n, vopts, geom, &bset,
+                             &disppxy, &disppxx, &outy, &outx, &placey, &placex);
 }
 
 void* rgb_loose_to_rgba(const void* data, int rows, int* rowstride, int cols, int alpha){
@@ -617,8 +798,8 @@ ncvisual* ncvisual_from_rgba(const void* rgba, int rows, int rowstride, int cols
       return NULL;
     }
     for(int y = 0 ; y < rows ; ++y){
-      memcpy(data + (ncv->rowstride * y) / 4, (const char*)rgba + rowstride * y, rowstride);
 //fprintf(stderr, "ROWS: %d STRIDE: %d (%d) COLS: %d %08x\n", ncv->pixy, ncv->rowstride, rowstride, cols, data[ncv->rowstride * y / 4]);
+      memcpy(data + (ncv->rowstride * y) / 4, (const char*)rgba + rowstride * y, rowstride);
     }
     ncvisual_set_data(ncv, data, true);
     ncvisual_details_seed(ncv);
@@ -801,180 +982,23 @@ int ncvisual_resize_noninterpolative(ncvisual* n, int rows, int cols){
 // the origin of the source region to draw (in pixels). leny/lenx define the
 // geometry of the source region to draw, again in pixels. ncv->pixy and
 // ncv->pixx define the source geometry in pixels.
-ncplane* ncvisual_render_cells(notcurses* nc, ncvisual* ncv, const struct blitset* bset,
-                               int placey, int placex, int begy, int begx,
-                               int leny, int lenx, ncplane* n, ncscale_e scaling,
+ncplane* ncvisual_render_cells(ncvisual* ncv, const struct blitset* bset,
+                               int placey, int placex,
+                               ncvgeom* geom, ncplane* n,
                                uint64_t flags, uint32_t transcolor){
-  int disprows, dispcols;
-  ncplane* createdn = NULL;
-//fprintf(stderr, "INPUT N: %p\n", n);
-  if(n == NULL || (flags & NCVISUAL_OPTION_CHILDPLANE)){ // create plane
-    if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
-      dispcols = lenx;
-      disprows = leny;
-    }else{
-      if(n == NULL){
-        ncplane_dim_yx(notcurses_stdplane(nc), &disprows, &dispcols);
-      }else{
-        ncplane_dim_yx(n, &disprows, &dispcols);
-      }
-      dispcols *= encoding_x_scale(&nc->tcache, bset);
-      disprows *= encoding_y_scale(&nc->tcache, bset);
-      if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
-        scale_visual(ncv, &disprows, &dispcols);
-      } // else stretch
-    }
-//fprintf(stderr, "PLACING NEW PLANE: %d/%d @ %d/%d %d/%d\n", disprows, dispcols, placey, placex, begy, begx);
-    struct ncplane_options nopts = {
-      .y = placey,
-      .x = placex,
-      .rows = disprows / encoding_y_scale(&nc->tcache, bset) +
-              !!(disprows % encoding_y_scale(&nc->tcache, bset)),
-      .cols = dispcols / encoding_x_scale(&nc->tcache, bset) +
-              !!(dispcols % encoding_x_scale(&nc->tcache, bset)),
-      .userptr = NULL,
-      .name = "cvis",
-      .resizecb = NULL,
-      .flags = 0,
-    };
-    if(flags & NCVISUAL_OPTION_HORALIGNED){
-      nopts.flags |= NCPLANE_OPTION_HORALIGNED;
-    }
-    if(flags & NCVISUAL_OPTION_VERALIGNED){
-      nopts.flags |= NCPLANE_OPTION_VERALIGNED;
-    }
-    if(n){
-      n = ncplane_create(n, &nopts);
-    }else{
-      n = ncpile_create(nc, &nopts);
-    }
-    if(n == NULL){
-      return NULL;
-    }
-    createdn = n;
-    placey = 0;
-    placex = 0;
-  }else{
-    if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
-      dispcols = lenx;
-      disprows = leny;
-    }else{
-      ncplane_dim_yx(n, &disprows, &dispcols);
-      dispcols *= encoding_x_scale(&nc->tcache, bset);
-      disprows *= encoding_y_scale(&nc->tcache, bset);
-      if(!(flags & NCVISUAL_OPTION_HORALIGNED)){
-        dispcols -= placex;
-      }
-      if(!(flags & NCVISUAL_OPTION_VERALIGNED)){
-        disprows -= placey;
-      }
-      if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
-        scale_visual(ncv, &disprows, &dispcols);
-      } // else stretch
-    }
-    if(flags & NCVISUAL_OPTION_HORALIGNED){
-      placex = ncplane_halign(n, placex, dispcols / encoding_x_scale(&nc->tcache, bset));
-    }
-    if(flags & NCVISUAL_OPTION_VERALIGNED){
-      placey = ncplane_valign(n, placey, disprows / encoding_y_scale(&nc->tcache, bset));
-    }
-  }
-//fprintf(stderr, "blit: %dx%d:%d+%d of %d/%d stride %u %p\n", begy, begx, leny, lenx, ncv->pixy, ncv->pixx, ncv->rowstride, ncv->data);
+  logdebug("cblit: rows/cols: %dx%d plane: %d/%d pix: %d/%d\n", geom->rcelly, geom->rcellx, ncplane_dim_y(n), ncplane_dim_x(n), geom->rpixy, geom->rpixx);
   blitterargs bargs;
-  if(flags & NCVISUAL_OPTION_ADDALPHA){
-    bargs.transcolor = transcolor | 0x1000000ul;
-  }else{
-    bargs.transcolor = 0;
-  }
-  bargs.begy = begy;
-  bargs.begx = begx;
-  bargs.leny = leny;
-  bargs.lenx = lenx;
+  bargs.transcolor = transcolor;
+  bargs.begy = geom->begy;
+  bargs.begx = geom->begx;
+  bargs.leny = geom->leny;
+  bargs.lenx = geom->lenx;
   bargs.flags = flags;
   bargs.u.cell.placey = placey;
   bargs.u.cell.placex = placex;
-  if(ncvisual_blit_internal(ncv, disprows, dispcols, n, bset, &bargs)){
-    ncplane_destroy(createdn);
-    return NULL;
-  }
-  return n;
-}
-
-// create a plane in which to blit the sprixel. |disppixx| and |disppixy| are
-// scaled pixel geometry on output, and unused on input. |placey| and |placex|
-// are used to position the new plane, and reset to 0 on output. |outy| and
-// |outx| are true output geometry on output, and unused on input (actual input
-// pixel geometry come from ncv->pixy and ncv->pixx).
-// |pxoffy| and |pxoffx| are pixel offset within the origin cell. they are not
-// included within |disppixx| nor |disppixy|, but count towards |outx| and
-// |outy|. these last two are furthermore clamped to sixel maxima, and |outy|
-// accounts for sixels being a multiple of six pixels tall.
-static ncplane*
-make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
-                   ncscale_e scaling, int* disppixy, int* disppixx,
-                   uint64_t flags, int* outy, int* outx,
-                   int* placey, int* placex, int pxoffy, int pxoffx){
-  if(scaling != NCSCALE_NONE && scaling != NCSCALE_NONE_HIRES){
-    if(parent == NULL){
-      ncplane_dim_yx(notcurses_stdplane(nc), disppixy, disppixx);
-    }else{
-      ncplane_dim_yx(parent, disppixy, disppixx);
-    }
-    // FIXME why do we clamp only vertical, not horizontal, here?
-    if(*placey + *disppixy >= ncplane_dim_y(notcurses_stdplane_const(nc))){
-      *disppixy = ncplane_dim_y(notcurses_stdplane_const(nc)) - *placey;
-    }
-    if(!(flags & NCVISUAL_OPTION_VERALIGNED)){
-      *disppixy -= *placey;
-    }
-    if(!(flags & NCVISUAL_OPTION_HORALIGNED)){
-      *disppixx -= *placex;
-    }
-    *disppixx *= nc->tcache.cellpixx;
-    *disppixy *= nc->tcache.cellpixy;
-    *disppixx += pxoffx;
-    *disppixy += pxoffy;
-    *outx = *disppixx;
-    clamp_to_sixelmax(&nc->tcache, disppixy, disppixx, outy, scaling);
-    if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
-      scale_visual(ncv, disppixy, disppixx); // can only shrink
-      *outx = *disppixx;
-      clamp_to_sixelmax(&nc->tcache, disppixy, disppixx, outy, scaling);
-    }
-  }else{
-    *disppixx = ncv->pixx + pxoffx;
-    *disppixy = ncv->pixy + pxoffy;
-    *outx = *disppixx;
-    clamp_to_sixelmax(&nc->tcache, disppixy, disppixx, outy, scaling);
-  }
-  struct ncplane_options nopts = {
-    .y = *placey,
-    .x = *placex,
-    .rows = (*outy + nc->tcache.cellpixy - 1) / nc->tcache.cellpixy,
-    .cols = (*disppixx + nc->tcache.cellpixx - 1) / nc->tcache.cellpixx,
-    .userptr = NULL,
-    .name = "bmap",
-    .resizecb = NULL,
-    .flags = ((flags & NCVISUAL_OPTION_HORALIGNED) ? NCPLANE_OPTION_HORALIGNED : 0)
-           | ((flags & NCVISUAL_OPTION_VERALIGNED) ? NCPLANE_OPTION_VERALIGNED : 0),
-  };
-//fprintf(stderr, "PLACING NEW PLANE: %d/%d @ %d/%d 0x%016lx\n", nopts.rows, nopts.cols, nopts.y, nopts.x, nopts.flags);
-  ncplane* n;
-  if(parent == NULL){
-    n = ncpile_create(nc, &nopts);
-  }else{
-    n = ncplane_create(parent, &nopts);
-  }
-  if(n == NULL){
+  if(ncvisual_blit_internal(ncv, geom->rpixy, geom->rpixx, n, bset, &bargs)){
     return NULL;
   }
-  // pixel offsets ought be counted for clamping purposes, but not returned
-  // as part of the scaled geometry (they are included in outx/outy).
-  *disppixy -= pxoffy;
-  *disppixx -= pxoffx;
-  // we always actually blit to the origin of the plane
-  *placey = 0;
-  *placex = 0;
   return n;
 }
 
@@ -996,114 +1020,51 @@ make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
 // must not exceed ncv->cols. these sums define the selected geometry. the
 // output width is always equal to the scaled width; it has no distinct name.
 ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blitset* bset,
-                                int placey, int placex, int begy, int begx,
-                                int leny, int lenx, ncplane* n, ncscale_e scaling,
-                                uint64_t flags, uint32_t transcolor,
+                                int placey, int placex, const ncvgeom* geom,
+                                ncplane* n, uint64_t flags, uint32_t transcolor,
                                 int pxoffy, int pxoffx){
-  ncplane* stdn = notcurses_stdplane(nc);
-  if(n == stdn && !(flags & NCVISUAL_OPTION_CHILDPLANE)){
-    logerror("won't blit bitmaps to the standard plane\n");
-    return NULL;
-  }
-  int disppixy = 0, disppixx = 0, outy = 0, outx = 0;
-  ncplane* createdn = NULL;
-  if(n == NULL || (flags & NCVISUAL_OPTION_CHILDPLANE)){ // create plane
-    if((createdn = make_sprixel_plane(nc, n, ncv, scaling, &disppixy, &disppixx,
-                                      flags, &outy, &outx, &placey, &placex,
-                                      pxoffy, pxoffx)) == NULL){
-      return NULL;
-    }
-    n = createdn;
-  }else{
-    if(scaling != NCSCALE_NONE && scaling != NCSCALE_NONE_HIRES){
-      ncplane_dim_yx(n, &disppixy, &disppixx);
-      disppixx *= nc->tcache.cellpixx;
-      disppixx += pxoffx;
-      disppixy *= nc->tcache.cellpixy;
-      disppixy += pxoffy;
-      clamp_to_sixelmax(&nc->tcache, &disppixy, &disppixx, &outy, scaling);
-      int absplacex = 0, absplacey = 0;
-      if(!(flags & NCVISUAL_OPTION_HORALIGNED)){
-        absplacex = placex;
-      }
-      if(!(flags & NCVISUAL_OPTION_VERALIGNED)){
-        absplacey = placey;
-      }
-      disppixx -= absplacex * nc->tcache.cellpixx;
-      disppixy -= absplacey * nc->tcache.cellpixy;
-    }else{
-      disppixx = lenx + pxoffx;
-      disppixy = leny + pxoffy;
-    }
-    if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
-      clamp_to_sixelmax(&nc->tcache, &disppixy, &disppixx, &outy, scaling);
-      scale_visual(ncv, &disppixy, &disppixx);
-    }
-    clamp_to_sixelmax(&nc->tcache, &disppixy, &disppixx, &outy, scaling);
-    // FIXME use a closed form
-    while((outy + nc->tcache.cellpixy - 1) / nc->tcache.cellpixy > (unsigned)ncplane_dim_y(n)){
-      outy -= nc->tcache.sprixel_scale_height;
-      disppixy = outy;
-    }
-    outx = disppixx;
-    disppixx -= pxoffx;
-    disppixy -= pxoffy;
-  }
-  logdebug("pblit: %dx%d <- %dx%d of %d/%d stride %u @%dx%d %p %u\n", disppixy, disppixx, begy, begx, ncv->pixy, ncv->pixx, ncv->rowstride, placey, placex, ncv->data, nc->tcache.cellpixx);
+  logdebug("pblit: rows/cols: %dx%d plane: %d/%d\n", geom->rcelly, geom->rcellx, ncplane_dim_y(n), ncplane_dim_x(n));
+  const tinfo* ti = &nc->tcache;
   blitterargs bargs;
-  if(flags & NCVISUAL_OPTION_ADDALPHA){
-    bargs.transcolor = transcolor | 0x1000000ul;
-  }else{
-    bargs.transcolor = 0;
-  }
-  bargs.begy = begy;
-  bargs.begx = begx;
-  bargs.leny = leny;
-  bargs.lenx = lenx;
+  bargs.transcolor = transcolor;
+  bargs.begy = geom->begy;
+  bargs.begx = geom->begx;
+  bargs.leny = geom->leny;
+  bargs.lenx = geom->lenx;
   bargs.flags = flags;
-  bargs.u.pixel.colorregs = nc->tcache.color_registers;
+  bargs.u.pixel.colorregs = ti->color_registers;
   bargs.u.pixel.pxoffy = pxoffy;
   bargs.u.pixel.pxoffx = pxoffx;
-  int cols = outx / nc->tcache.cellpixx + !!(outx % nc->tcache.cellpixx);
-  int rows = outy / nc->tcache.cellpixy + !!(outy % nc->tcache.cellpixy);
-  logdebug("cblit: rows/cols: %dx%d plane: %d/%d out: %d/%d\n", rows, cols, ncplane_dim_y(n), ncplane_dim_x(n), outy, outx);
   if(n->sprite == NULL){
-    if((n->sprite = sprixel_alloc(&nc->tcache, n, rows, cols)) == NULL){
-      ncplane_destroy(createdn);
+    if((n->sprite = sprixel_alloc(&nc->tcache, n, geom->rcelly, geom->rcellx)) == NULL){
       return NULL;
     }
-    if((n->tam = create_tam(rows, cols)) == NULL){
-      ncplane_destroy(createdn);
+    if((n->tam = create_tam(geom->rcelly, geom->rcellx)) == NULL){
       return NULL;;
     }
   }else{
     n->sprite = sprixel_recycle(n);
-    if(n->sprite->dimy != rows || n->sprite->dimx != cols){
+    if(n->sprite->dimy != geom->rcelly || n->sprite->dimx != geom->rcellx){
       destroy_tam(n);
-      if((n->tam = create_tam(rows, cols)) == NULL){
-        ncplane_destroy(createdn);
+      if((n->tam = create_tam(geom->rcelly, geom->rcellx)) == NULL){
         return NULL;
       }
     }
-    n->sprite->dimx = cols;
-    n->sprite->dimy = rows;
+    n->sprite->dimx = geom->rcellx;
+    n->sprite->dimy = geom->rcelly;
   }
   bargs.u.pixel.spx = n->sprite;
   // FIXME need to pull off the ncpile's sprixellist if anything below fails!
-  // at this point, disppixy/disppixx are the output geometry (might be larger
-  // than scaledy/scaledx for sixel), while scaledy/scaledx are the scaled
-  // geometry. cells occupied are determined based off disppixy/disppixx.
-  if(ncvisual_blit_internal(ncv, disppixy, disppixx, n, bset, &bargs)){
-    ncplane_destroy(createdn);
+  if(ncvisual_blit_internal(ncv, geom->rpixy, geom->rpixx, n, bset, &bargs)){
     return NULL;
   }
   // if we created the plane earlier, placex/placey were taken into account, and
   // zeroed out, thus neither of these will have any effect.
   if(flags & NCVISUAL_OPTION_HORALIGNED){
     if(placex == NCALIGN_CENTER){
-      placex = (ncplane_dim_x(ncplane_parent_const(n)) * nc->tcache.cellpixx - disppixx) / 2 / nc->tcache.cellpixx;
+      placex = (ncplane_dim_x(ncplane_parent_const(n)) * ti->cellpixx - geom->rpixx) / 2 / ti->cellpixx;
     }else if(placex == NCALIGN_RIGHT){
-      placex = (ncplane_dim_x(ncplane_parent_const(n)) * nc->tcache.cellpixy - disppixx) / nc->tcache.cellpixx;
+      placex = (ncplane_dim_x(ncplane_parent_const(n)) * ti->cellpixx - geom->rpixx) / ti->cellpixx;
     }
     if(placex < 0){
       return NULL;
@@ -1111,9 +1072,9 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   }
   if(flags & NCVISUAL_OPTION_VERALIGNED){
     if(placey == NCALIGN_CENTER){
-      placey = (ncplane_dim_y(ncplane_parent_const(n)) * nc->tcache.cellpixy - disppixy) / 2 / nc->tcache.cellpixy;
+      placey = (ncplane_dim_y(ncplane_parent_const(n)) * ti->cellpixy - geom->rpixy) / 2 / ti->cellpixy;
     }else if(placey == NCALIGN_BOTTOM){
-      placey = (ncplane_dim_y(ncplane_parent_const(n)) * nc->tcache.cellpixy - disppixy) / nc->tcache.cellpixy;
+      placey = (ncplane_dim_y(ncplane_parent_const(n)) * ti->cellpixy - geom->rpixy) / ti->cellpixy;
     }
     if(placey < 0){
       return NULL;
@@ -1130,11 +1091,10 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
     // if we blow up here, then we've got a TAM sized to the sprixel, rather
     // than the plane. running it through destroy_tam() via ncplane_destroy()
     // will use incorrect bounds for scrubbing said TAM. do it manually here.
-    cleanup_tam(n->tam, rows, cols);
+    cleanup_tam(n->tam, geom->rcelly, geom->rcellx);
     free(n->tam);
     n->tam = NULL;
     sprixel_hide(bargs.u.pixel.spx);
-    ncplane_destroy(createdn);
     return NULL;
   }
   n->sprite = bargs.u.pixel.spx;
@@ -1143,35 +1103,72 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
 }
 
 ncplane* ncvisual_blit(notcurses* nc, ncvisual* ncv, const struct ncvisual_options* vopts){
+//fprintf(stderr, "%p tacache: %p\n", n, n->tacache);
+  struct ncvisual_options fakevopts;
+  if(vopts == NULL){
+    memset(&fakevopts, 0, sizeof(fakevopts));
+    vopts = &fakevopts;
+  }
+  loginfo("inblit %dx%d %d@%d %dx%d @ %dx%d %p\n", ncv->pixy, ncv->pixx, vopts->y, vopts->x,
+          vopts->leny, vopts->lenx, vopts->begy, vopts->begx, vopts->n);
+  ncvgeom geom;
   const struct blitset* bset;
-  int leny, lenx;
-  if(ncvisual_blitset_geom(nc, &nc->tcache, ncv, vopts, NULL, NULL, NULL, NULL,
-                           &leny, &lenx, &bset) < 0){
+  int disppxy, disppxx, outy, outx, placey, placex;
+  if(ncvisual_geom_inner(&nc->tcache, ncv, vopts, &geom, &bset,
+                         &disppxy, &disppxx, &outy, &outx,
+                         &placey, &placex)){
     // ncvisual_blitset_geom() emits its own diagnostics, no need for an error here
     return NULL;
   }
-  int begy, begx;
-  ncvisual_origin(vopts, &begy, &begx);
-  int placey = vopts ? vopts->y : 0;
-  int placex = vopts ? vopts->x : 0;
-//fprintf(stderr, "beg/len: %d %d %d %d place: %d/%d scale: %d/%d\n", begy, leny, begx, lenx, placey, placex, encoding_y_scale(&nc->tcache, bset), encoding_x_scale(&nc->tcache, bset));
-  ncplane* n = (vopts ? vopts->n : NULL);
-//fprintf(stderr, "%p tacache: %p\n", n, n->tacache);
-  ncscale_e scaling = vopts ? vopts->scaling : NCSCALE_NONE;
+  ncplane* n = vopts->n;
   uint32_t transcolor = 0;
-  if(vopts && vopts->flags & NCVISUAL_OPTION_ADDALPHA){
+  if(vopts->flags & NCVISUAL_OPTION_ADDALPHA){
     transcolor = 0x1000000ull | vopts->transcolor;
   }
-  if(bset->geom != NCBLIT_PIXEL){
-    n = ncvisual_render_cells(nc, ncv, bset, placey, placex, begy, begx,
-                              leny, lenx, n, scaling,
-                              vopts ? vopts->flags : 0, transcolor);
+  ncplane* createdn = NULL; // to destroy on error
+  if(n == NULL || (vopts->flags & NCVISUAL_OPTION_CHILDPLANE)){ // create plane
+    struct ncplane_options nopts = {
+      .y = placey,
+      .x = placex,
+      .rows = geom.rcelly,
+      .cols = geom.rcellx,
+      .userptr = NULL,
+      .name = geom.blitter == NCBLIT_PIXEL ? "bmap" : "cvis",
+      .resizecb = NULL,
+      .flags = 0,
+    };
+    if(vopts->flags & NCVISUAL_OPTION_HORALIGNED){
+      nopts.flags |= NCPLANE_OPTION_HORALIGNED;
+      nopts.x = vopts->x;
+    }
+    if(vopts->flags & NCVISUAL_OPTION_VERALIGNED){
+      nopts.flags |= NCPLANE_OPTION_VERALIGNED;
+      nopts.y = vopts->y;
+    }
+    loginfo("placing new plane: %d/%d @ %d/%d 0x%016lx\n", nopts.rows, nopts.cols, nopts.y, nopts.x, nopts.flags);
+    if(n == NULL){
+      n = ncpile_create(nc, &nopts);
+    }else{
+      n = ncplane_create(n, &nopts);
+    }
+    if((createdn = n) == NULL){
+      return NULL;
+    }
+    placey = 0;
+    placex = 0;
+  }
+  logdebug("blit to plane %p at %d/%d geom %dx%d\n", n, ncplane_y(n), ncplane_x(n), ncplane_dim_y(n), ncplane_dim_x(n));
+  if(geom.blitter != NCBLIT_PIXEL){
+    n = ncvisual_render_cells(ncv, bset, placey, placex,
+                              &geom, n, vopts->flags, transcolor);
   }else{
-    n = ncvisual_render_pixels(nc, ncv, bset, placey, placex, begy, begx,
-                               leny, lenx, n, scaling,
-                               vopts ? vopts->flags : 0, transcolor,
-                               vopts ? vopts->pxoffy : 0,
-                               vopts ? vopts->pxoffx : 0);
+    n = ncvisual_render_pixels(nc, ncv, bset, placey, placex,
+                               &geom, n,
+                               vopts->flags, transcolor,
+                               vopts->pxoffy, vopts->pxoffx);
+  }
+  if(n == NULL){
+    ncplane_destroy(createdn);
   }
   return n;
 }
diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c
index dba857949b..b9b42d7f77 100644
--- a/src/media/ffmpeg.c
+++ b/src/media/ffmpeg.c
@@ -498,9 +498,11 @@ int ffmpeg_stream(notcurses* nc, ncvisual* ncv, float timescale,
       ncplane_erase(activevopts.n); // new frame could be partially transparent
     }
     // decay the blitter explicitly, so that the callback knows the blitter it
-    // was actually rendered with
-    ncvisual_blitter_geom(nc, ncv, &activevopts, NULL, NULL, NULL, NULL,
-                          &activevopts.blitter);
+    // was actually rendered with. basically just need rgba_blitter(), but
+    // that's not exported.
+    ncvgeom geom;
+    ncvisual_geom(nc, ncv, &activevopts, &geom);
+    activevopts.blitter = geom.blitter;
     if((newn = ncvisual_blit(nc, ncv, &activevopts)) == NULL){
       if(activevopts.n != vopts->n){
         ncplane_destroy(activevopts.n);
@@ -637,10 +639,9 @@ int ffmpeg_blit(ncvisual* ncv, int rows, int cols, ncplane* n,
   if(data == NULL){
     return -1;
   }
-//fprintf(stderr, "WHN NCV: %d/%d bargslen: %d/%d targ: %d/%d\n", inframe->width, inframe->height, bargs->leny, bargs->lenx, rows, cols);
+//fprintf(stderr, "WHN NCV: bargslen: %d/%d targ: %d/%d\n", bargs->leny, bargs->lenx, rows, cols);
   int ret = 0;
   if(rgba_blit_dispatch(n, bset, stride, data, rows, cols, bargs) < 0){
-//fprintf(stderr, "rgba dispatch failed!\n");
     ret = -1;
   }
   if(data != ncv->data){
diff --git a/src/media/oiio-indep.c b/src/media/oiio-indep.c
index 61a2569ea9..cf0917eb21 100644
--- a/src/media/oiio-indep.c
+++ b/src/media/oiio-indep.c
@@ -27,7 +27,14 @@ int oiio_stream(struct notcurses* nc, ncvisual* ncv, float timescale,
   do{
     // decay the blitter explicitly, so that the callback knows the blitter it
     // was actually rendered with
-    ncvisual_blitter_geom(nc, ncv, &activevopts, NULL, NULL, NULL, NULL, &activevopts.blitter);
+    ncvgeom geom;
+    if(ncvisual_geom(nc, ncv, &activevopts, &geom)){
+      if(activevopts.n != vopts->n){
+        ncplane_destroy(activevopts.n);
+      }
+      return -1;
+    }
+    activevopts.blitter = geom.blitter;
     if((newn = ncvisual_blit(nc, ncv, &activevopts)) == NULL){
       if(activevopts.n != vopts->n){
         ncplane_destroy(activevopts.n);
diff --git a/src/poc/rotate.c b/src/poc/rotate.c
index 3cb89c020a..1ca5d65378 100644
--- a/src/poc/rotate.c
+++ b/src/poc/rotate.c
@@ -41,7 +41,6 @@ int main(int argc, char** argv){
   if(!ncv){
     goto err;
   }
-  int scaley, scalex;
   vopts.n = n;
   if((ncvisual_blit(nc, ncv, &vopts)) == NULL){
     goto err;
@@ -52,8 +51,9 @@ int main(int argc, char** argv){
   clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL);
 
   ncplane_erase(n);
-  ncvisual_blitter_geom(nc, ncv, &vopts, NULL, NULL, &scaley, &scalex, NULL);
-  if(ncvisual_resize(ncv, dimy * scaley, dimx * scalex)){
+  ncvgeom geom;
+  ncvisual_geom(nc, ncv, &vopts, &geom);
+  if(ncvisual_resize(ncv, dimy * geom.scaley, dimx * geom.scalex)){
     goto err;
   }
   if(ncvisual_blit(nc, ncv, &vopts) == NULL){


Test patch:
diff --git a/src/tests/bitmap.cpp b/src/tests/bitmap.cpp
index ce794c98ad..497ac3d0d1 100644
--- a/src/tests/bitmap.cpp
+++ b/src/tests/bitmap.cpp
@@ -307,7 +307,6 @@ TEST_CASE("Bitmaps") {
     vopts.scaling = NCSCALE_NONE;
     auto ninf = ncvisual_blit(nc_, ncv, &vopts);
     ncplane_set_name(ninf, "ninf");
-notcurses_debug(nc_, stderr);
     REQUIRE(nullptr != ninf);
     // y of scaled might not be exactly equal to y of inflated since one
     // is based around what can be fit into a specific space, whereas the
@@ -363,6 +362,7 @@ notcurses_debug(nc_, stderr);
     REQUIRE(nullptr != ncv);
     auto child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
+    CHECK(0 == notcurses_render(nc_));
     CHECK(18 == ncplane_dim_y(child));
     CHECK(5 == ncplane_dim_x(child));
     CHECK(0 == ncplane_y(child));
diff --git a/src/tests/direct.cpp b/src/tests/direct.cpp
index 6eaaa2a1aa..47fadda526 100644
--- a/src/tests/direct.cpp
+++ b/src/tests/direct.cpp
@@ -126,7 +126,7 @@ TEST_CASE("Direct") {
     }
   }
 
-  SUBCASE("CursorPostGlyphRender") {
+  SUBCASE("CursorPostCellBlit") {
     if(ncdirect_canget_cursor(nc_)){
       auto dirf = ncdirectf_from_file(nc_, find_data("worldmap.png").get());
       REQUIRE(nullptr != dirf);
diff --git a/src/tests/media.cpp b/src/tests/media.cpp
index fce2f4c1ef..1253f15d8f 100644
--- a/src/tests/media.cpp
+++ b/src/tests/media.cpp
@@ -92,14 +92,18 @@ TEST_CASE("Media") {
     ncplane_dim_yx(ncp_, &dimy, &dimx);
     auto ncv = ncvisual_from_file(find_data("changes.jpg").get());
     REQUIRE(ncv);
-    int odimy, odimx, ndimy, ndimx;
     struct ncvisual_options opts{};
     opts.n = ncp_;
-    CHECK(0 == ncvisual_blitter_geom(nc_, ncv, &opts, &odimy, &odimx, nullptr, nullptr, nullptr));
+    ncvgeom geom{};
+    CHECK(0 == ncvisual_geom(nc_, ncv, &opts, &geom));
+    int odimy = geom.pixy;
+    int odimx = geom.pixx;
     CHECK(ncvisual_blit(nc_, ncv, &opts));
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncvisual_resize_noninterpolative(ncv, ncv->pixy * 2, ncv->pixx * 2));
-    CHECK(0 == ncvisual_blitter_geom(nc_, ncv, &opts, &ndimy, &ndimx, nullptr, nullptr, nullptr));
+    CHECK(0 == ncvisual_geom(nc_, ncv, &opts, &geom));
+    int ndimy = geom.pixy;
+    int ndimx = geom.pixx;
     CHECK(ndimy == odimy * 2);
     CHECK(ndimx == odimx * 2);
     CHECK(ncvisual_blit(nc_, ncv, &opts));
diff --git a/src/tests/visual.cpp b/src/tests/visual.cpp
index 88087c0743..8698853d48 100644
--- a/src/tests/visual.cpp
+++ b/src/tests/visual.cpp
@@ -3,6 +3,28 @@
 #include <vector>
 #include <cmath>
 
+// verify results for extrinsic geometries with NULL or default vopts
+void default_visual_extrinsics(const notcurses* nc, const ncvgeom& g) {
+  CHECK(0 == g.pixy);
+  CHECK(0 == g.pixx);
+  if(notcurses_canpixel(nc)){
+    CHECK(1 <= g.cdimy);
+    CHECK(1 <= g.cdimx);
+  }else{
+    CHECK(0 == g.cdimy);
+    CHECK(0 == g.cdimx);
+  }
+  CHECK(1 <= g.scaley);
+  CHECK(1 <= g.scalex);
+  CHECK(0 == g.rpixy);
+  CHECK(0 == g.rpixx);
+  CHECK(0 <= g.maxpixely);
+  CHECK(0 <= g.maxpixelx);
+  // we never use pixel by default, and must not revolve to default
+  CHECK(NCBLIT_PIXEL != g.blitter);
+  CHECK(NCBLIT_DEFAULT != g.blitter);
+}
+
 TEST_CASE("Visual") {
   auto nc_ = testing_notcurses();
   REQUIRE(nullptr != nc_);
@@ -29,6 +51,141 @@ TEST_CASE("Visual") {
     }
   }
 
+  // ncvisual_geom() with a NULL nc
+  SUBCASE("VisualIntrinsicGeometry") {
+    std::vector<uint32_t> v(20, 0xfffffffflu);
+    auto ncv = ncvisual_from_rgba(v.data(), 2, 10 * sizeof(decltype(v)::value_type), 10);
+    REQUIRE(nullptr != ncv);
+    ncvgeom g{};
+    CHECK(0 == ncvisual_geom(nullptr, ncv, nullptr, &g));
+    ncvisual_destroy(ncv);
+    CHECK(2 == g.pixy);
+    CHECK(10 == g.pixx);
+    CHECK(0 == g.cdimy);
+    CHECK(0 == g.cdimx);
+    CHECK(0 == g.rpixy);
+    CHECK(0 == g.rpixx);
+    CHECK(0 == g.scaley);
+    CHECK(0 == g.scalex);
+    CHECK(0 == g.maxpixely);
+    CHECK(0 == g.maxpixelx);
+    CHECK(NCBLIT_DEFAULT == g.blitter);
+  }
+
+  // ncvisual_geom() with a NULL ncvisual and NULL visual_options
+  SUBCASE("VisualExtrinsicGeometryNULL") {
+    ncvgeom g{};
+    CHECK(0 == ncvisual_geom(nc_, nullptr, nullptr, &g));
+    default_visual_extrinsics(nc_, g);
+  }
+
+  // ncvisual_geom() with a NULL ncvisual and default visual_options
+  SUBCASE("VisualExtrinsicGeometryDefault") {
+    ncvgeom g{};
+    struct ncvisual_options vopts{};
+    CHECK(0 == ncvisual_geom(nc_, nullptr, &vopts, &g));
+    default_visual_extrinsics(nc_, g);
+  }
+
+  // ncvisual_geom() with a NULL ncvisual and NCBLIT_PIXEL requested
+  SUBCASE("VisualExtrinsicGeometryPixel") {
+    ncvgeom g{};
+    struct ncvisual_options vopts{};
+    vopts.blitter = NCBLIT_PIXEL;
+    CHECK(0 == ncvisual_geom(nc_, nullptr, &vopts, &g));
+    CHECK(0 == g.pixy);
+    CHECK(0 == g.pixx);
+    if(notcurses_canpixel(nc_)){
+      CHECK(1 <= g.cdimy);
+      CHECK(1 <= g.cdimx);
+      CHECK(g.cdimy == g.scaley);
+      CHECK(g.cdimx == g.scalex);
+    }else{
+      CHECK(0 == g.cdimy);
+      CHECK(0 == g.cdimx);
+      CHECK(1 <= g.scaley);
+      CHECK(1 <= g.scalex);
+    }
+    CHECK(0 == g.rpixy);
+    CHECK(0 == g.rpixx);
+    CHECK(0 <= g.maxpixely);
+    CHECK(0 <= g.maxpixelx);
+    CHECK(NCBLIT_DEFAULT != g.blitter); // we must not revolve to default
+  }
+
+  // build a simple ncvisual and check the calculated geometries for 1x1
+  // cell blitting in the absence of scaling
+  SUBCASE("VisualCellGeometryNoScaling") {
+    std::vector<uint32_t> v(80, 0xfffffffflu);
+    auto ncv = ncvisual_from_rgba(v.data(), 8, 10 * sizeof(decltype(v)::value_type), 10);
+    REQUIRE(nullptr != ncv);
+    struct ncvisual_options vopts{};
+    ncvgeom g{};
+    vopts.blitter = NCBLIT_1x1;
+    CHECK(0 == ncvisual_geom(nc_, ncv, &vopts, &g));
+    ncvisual_destroy(ncv);
+    CHECK(8 == g.pixy);
+    CHECK(10 == g.pixx);
+    CHECK(1 == g.scaley);
+    CHECK(1 == g.scalex);
+    CHECK(8 == g.rpixy);
+    CHECK(10 == g.rpixx);
+    CHECK(8 == g.rcelly);
+    CHECK(10 == g.rcellx);
+    CHECK(NCBLIT_1x1 == g.blitter);
+  }
+
+  // build a square ncvisual and check the calculated geometries for 1x1
+  // cell blitting with scaling
+  SUBCASE("VisualCellGeometryScaling") {
+    std::vector<uint32_t> v(100, 0xfffffffflu);
+    auto ncv = ncvisual_from_rgba(v.data(), 10, 10 * sizeof(decltype(v)::value_type), 10);
+    REQUIRE(nullptr != ncv);
+    struct ncvisual_options vopts{};
+    ncvgeom g{};
+    vopts.blitter = NCBLIT_1x1;
+    vopts.scaling = NCSCALE_SCALE;
+    CHECK(0 == ncvisual_geom(nc_, ncv, &vopts, &g));
+    ncvisual_destroy(ncv);
+    int dimy, dimx;
+    ncplane_dim_yx(n_, &dimy, &dimx);
+    int mindim = dimy < dimx ? dimy : dimx;
+    CHECK(10 == g.pixy);
+    CHECK(10 == g.pixx);
+    CHECK(1 == g.scaley);
+    CHECK(1 == g.scalex);
+    CHECK(mindim == g.rpixy);
+    CHECK(mindim == g.rpixx);
+    CHECK(mindim == g.rcelly);
+    CHECK(mindim == g.rcellx);
+    CHECK(NCBLIT_1x1 == g.blitter);
+  }
+
+  // build a square ncvisual and check the calculated geometries for 1x1
+  // cell blitting with stretching
+  SUBCASE("VisualCellGeometryStretching") {
+    std::vector<uint32_t> v(100, 0xfffffffflu);
+    auto ncv = ncvisual_from_rgba(v.data(), 10, 10 * sizeof(decltype(v)::value_type), 10);
+    REQUIRE(nullptr != ncv);
+    struct ncvisual_options vopts{};
+    ncvgeom g{};
+    vopts.blitter = NCBLIT_1x1;
+    vopts.scaling = NCSCALE_STRETCH;
+    CHECK(0 == ncvisual_geom(nc_, ncv, &vopts, &g));
+    ncvisual_destroy(ncv);
+    int dimy, dimx;
+    ncplane_dim_yx(n_, &dimy, &dimx);
+    CHECK(10 == g.pixy);
+    CHECK(10 == g.pixx);
+    CHECK(1 == g.scaley);
+    CHECK(1 == g.scalex);
+    CHECK(dimy == g.rpixy);
+    CHECK(dimx == g.rpixx);
+    CHECK(dimy == g.rcelly);
+    CHECK(dimx == g.rcellx);
+    CHECK(NCBLIT_1x1 == g.blitter);
+  }
+
   // check that we properly populate RGB + A -> RGBA from 35x4 (see #1806)
   SUBCASE("VisualFromRGBPacked35x4") {
     unsigned char rgb[4 * 35 * 3] = "";
@@ -149,22 +306,16 @@ TEST_CASE("Visual") {
   // ensure that NCSCALE_STRETCH gives us a full plane, and that we write
   // everywhere within that plane
   SUBCASE("Stretch") {
-    std::vector<uint32_t> v(1, htole(0xe61c28ff));
+    std::vector<uint32_t> v(1, htole(0xff1c28ff));
     int dimy, dimx;
     ncplane_dim_yx(ncp_, &dimy, &dimx);
     auto ncv = ncvisual_from_rgba(v.data(), 1, sizeof(decltype(v)::value_type), 1);
     REQUIRE(nullptr != ncv);
-    struct ncvisual_options vopts = {
-      .n = n_,
-      .scaling = NCSCALE_STRETCH,
-      .y = 0, .x = 0,
-      .begy = 0, .begx = 0,
-      .leny = 0, .lenx = 0,
-      .blitter = NCBLIT_1x1,
-      .flags = NCVISUAL_OPTION_CHILDPLANE,
-      .transcolor = 0,
-      .pxoffy = 0, .pxoffx = 0,
-    };
+    struct ncvisual_options vopts{};
+    vopts.n = n_;
+    vopts.scaling = NCSCALE_STRETCH;
+    vopts.blitter = NCBLIT_1x1;
+    vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
     auto n = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(0 == notcurses_render(nc_));
     REQUIRE(nullptr != n);