Repository: dankamongmen/notcurses (language: python)

Issue:
deal with cell pixel geometry changes
In the event of a cell pixel geometry change (probably due to a font change or a font size change), we need to adjust the TAMs of all visible sprixels. I believe that some terminals scale sprixels along with a cell pixel geometry changes, in which case the TAM ought remain accurate, but our definition of the sprixel's pixel geometry needs to be updated. We'll probably need to encode this as expert knowledge.

i presume we'll get a `SIGWINCH` whenever this happens, and can pick up the new cell pixel geometry at that time. if it has changed, walk the active sprixel list. we probably actually need to walk all sprixels, including in secondary piles...hrmm.

XTerm doesn't update the cell pixel geometry it returns via `ioctl()` unless the cell geometry itself changes (or at least didn't last time I looked; I reported the bug about a month ago). not sure what we can do about that case.
deal with cell pixel geometry changes
In the event of a cell pixel geometry change (probably due to a font change or a font size change), we need to adjust the TAMs of all visible sprixels. I believe that some terminals scale sprixels along with a cell pixel geometry changes, in which case the TAM ought remain accurate, but our definition of the sprixel's pixel geometry needs to be updated. We'll probably need to encode this as expert knowledge.

i presume we'll get a `SIGWINCH` whenever this happens, and can pick up the new cell pixel geometry at that time. if it has changed, walk the active sprixel list. we probably actually need to walk all sprixels, including in secondary piles...hrmm.

XTerm doesn't update the cell pixel geometry it returns via `ioctl()` unless the cell geometry itself changes (or at least didn't last time I looked; I reported the bug about a month ago). not sure what we can do about that case.


Discussion / hints:
so we can't just go modifying other threads' sprixels from any location. and the age-old question remains: do we want to maintain cell geometry (requiring scaling of the image), or maintain its pixel geometry (meaning the containing plane will change its cell geometry, perhaps in an unexpected way)?

if the window geometry changed along with the cell-pixel geometry (quite plausible in the face of a font size change), rescaling the image seems to make more sense, but scaling according to the cell-pixel geometry delta doesn't necessarily mean we'll still fit within the visual area. so i think that's kind of hokum.

so let's first say that we're going to change up the TAM at `ncpile_render()` time (have we yet checked for a screen update at this time? yes, that happens as the first act of `ncpile_render()`.) we need change them before we begin actual painting. so we'd run through the pile's sprixel list and inspect for each one the stored cell-pixel geometry:

`int cellpxy, cellpxx; // cell-pixel geometry at time of creation`

if the cell-pixel geometry has changed, we'll need update it and change out the TAM. that's easy enough. the questions at this point are:

 * need we explicitly do anything to remove the old graphic? no, definitely not if we're keeping the old pixel geometry.
 * how do we map old TAM entries to new ones?
 * what changes need we make to the encoded image?

the last two seem like major issues, but given that all wiping and restoration occurs during paint and raster, maybe not? let's think...hrmm so let's say we have a given cell in the TAM marked ANNIHILATED, and suddenly that cell would be partially annihilated and partially visible...well, that can only happen if the glyphs weren't changing, but they are now. so each cell's worth of the image will now be either entirely annihilated or entirely visible. but do we need restore or not? the easiest thing to do would be to rebuild the original image (virtually), clean the TAM up to all-visible, and then rewipe anything needing rewiping. it would be expensive, but this is a rare event, and there wouldn't be any flicker since we can do all those changes internally. i think that's the best way to start things. this of course requires the ability to rebuild the image entirely, which is easiest if we have the old image around, which is a memory hit but something it's seemed might be necessary for other reasons. hrmm. a 4096x4096x4 image is going to be 64MB of storage, not cheap at all...
alright, i think the thing to do is to:

* detect a cell-pixel geometry change from the last time the pile came through (means we must store cell-pixel geometry at time of last render in the pile, no problem there, and also *use it* when blitting (as opposed to the current cell-pixel geometry)). all sprixels in a pile are guaranteed to have the same cell-pixel geometry (maybe we can then eliminate it within the sprixel?)
* whenever reparenting a sprixel to a pile with a different cell-pixel geometry, or displaying it under a different cell-pixel geometry, call `rescale_sprixel()`. this ought be done prior to calling `paint()`, and should be performed on all sprixels within a pile upon detecting a cell-pixel geometry change.

`rescale_sprixel()`:
* realloc the TAM based on new cell geometry, holding pixel geometry constant
* reencode the sprixel, updating all sprixcells (they might switch among OPAQUE, MIXED, and TRANSPARENT) and auxvectors as if there were no annihilations. shouldn't need a copy -- auxvectors ought hold all necessary reconstruction information (except in the Kitty reflective case). reset all sprixcell entries in the TAM to MIXED, OPAQUE, or TRANSPARENT.

at this point, you can freely `paint()` the planes of the pile. any cells needing wiping will be wiped afresh. it's expensive, but again, this ought be a very rare event.
alright, i've changed everything to use the `cellpxy`/`cellpxx` from the target `ncpile` when appropriate. i need to actually set these values up, though.
> alright, i've changed everything to use the `cellpxy`/`cellpxx` from the target `ncpile` when appropriate. i need to actually set these values up, though.

one annoyance (coming, as it does so often, from Direct Mode) -- we don't have `s->n` in the blit functions, and will need to pass something down, probably in `bargs`.
so we can't just go modifying other threads' sprixels from any location. and the age-old question remains: do we want to maintain cell geometry (requiring scaling of the image), or maintain its pixel geometry (meaning the containing plane will change its cell geometry, perhaps in an unexpected way)?

if the window geometry changed along with the cell-pixel geometry (quite plausible in the face of a font size change), rescaling the image seems to make more sense, but scaling according to the cell-pixel geometry delta doesn't necessarily mean we'll still fit within the visual area. so i think that's kind of hokum.

so let's first say that we're going to change up the TAM at `ncpile_render()` time (have we yet checked for a screen update at this time? yes, that happens as the first act of `ncpile_render()`.) we need change them before we begin actual painting. so we'd run through the pile's sprixel list and inspect for each one the stored cell-pixel geometry:

`int cellpxy, cellpxx; // cell-pixel geometry at time of creation`

if the cell-pixel geometry has changed, we'll need update it and change out the TAM. that's easy enough. the questions at this point are:

 * need we explicitly do anything to remove the old graphic? no, definitely not if we're keeping the old pixel geometry.
 * how do we map old TAM entries to new ones?
 * what changes need we make to the encoded image?

the last two seem like major issues, but given that all wiping and restoration occurs during paint and raster, maybe not? let's think...hrmm so let's say we have a given cell in the TAM marked ANNIHILATED, and suddenly that cell would be partially annihilated and partially visible...well, that can only happen if the glyphs weren't changing, but they are now. so each cell's worth of the image will now be either entirely annihilated or entirely visible. but do we need restore or not? the easiest thing to do would be to rebuild the original image (virtually), clean the TAM up to all-visible, and then rewipe anything needing rewiping. it would be expensive, but this is a rare event, and there wouldn't be any flicker since we can do all those changes internally. i think that's the best way to start things. this of course requires the ability to rebuild the image entirely, which is easiest if we have the old image around, which is a memory hit but something it's seemed might be necessary for other reasons. hrmm. a 4096x4096x4 image is going to be 64MB of storage, not cheap at all...
alright, i think the thing to do is to:

* detect a cell-pixel geometry change from the last time the pile came through (means we must store cell-pixel geometry at time of last render in the pile, no problem there, and also *use it* when blitting (as opposed to the current cell-pixel geometry)). all sprixels in a pile are guaranteed to have the same cell-pixel geometry (maybe we can then eliminate it within the sprixel?)
* whenever reparenting a sprixel to a pile with a different cell-pixel geometry, or displaying it under a different cell-pixel geometry, call `rescale_sprixel()`. this ought be done prior to calling `paint()`, and should be performed on all sprixels within a pile upon detecting a cell-pixel geometry change.

`rescale_sprixel()`:
* realloc the TAM based on new cell geometry, holding pixel geometry constant
* reencode the sprixel, updating all sprixcells (they might switch among OPAQUE, MIXED, and TRANSPARENT) and auxvectors as if there were no annihilations. shouldn't need a copy -- auxvectors ought hold all necessary reconstruction information (except in the Kitty reflective case). reset all sprixcell entries in the TAM to MIXED, OPAQUE, or TRANSPARENT.

at this point, you can freely `paint()` the planes of the pile. any cells needing wiping will be wiped afresh. it's expensive, but again, this ought be a very rare event.
alright, i've changed everything to use the `cellpxy`/`cellpxx` from the target `ncpile` when appropriate. i need to actually set these values up, though.
> alright, i've changed everything to use the `cellpxy`/`cellpxx` from the target `ncpile` when appropriate. i need to actually set these values up, though.

one annoyance (coming, as it does so often, from Direct Mode) -- we don't have `s->n` in the blit functions, and will need to pass something down, probably in `bargs`.

Fix patch:
diff --git a/README.md b/README.md
index 22243ac5a6..9919703713 100644
--- a/README.md
+++ b/README.md
@@ -565,6 +565,17 @@ If things break or seem otherwise lackluster, **please** consult the
   machines, and these writes aren't tracked by the standard statistics.
 </details>
 
+<details>
+  <summary>I just want to display a bitmap on my terminal. Your library is
+  complex and stupid. You are simple and stupid.</summary>
+  If you're willing to call a binary, use <tt>ncplayer</tt> to put an image,
+  with desired scaling, anywhere on the screen and call it a day. Otherwise,
+  call <tt>notcurses_init()</tt>, <tt>ncvisual_from_file()</tt>,
+  <tt>ncvisual_blit()</tt>, <tt>notcurses_render()</tt>, and
+  <tt>notcurses_stop()</tt>. It's not too tough. And thanks—your thoughtful
+  comments and appreciative tone are why I work on Free Software.
+</details>
+
 ## Useful links
 
 * [BiDi in Terminal Emulators](https://terminal-wg.pages.freedesktop.org/bidi/)
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index c764f36912..01591e3f4e 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -1778,6 +1778,7 @@ API int ncplane_scrollup(struct ncplane* n, int r)
 // Scroll |n| up until |child| is no longer hidden beneath it. Returns an
 // error if |child| is not a child of |n|, or |n| is not scrolling, or |child|
 // is fixed. Returns the number of scrolling events otherwise (might be 0).
+// If the child plane is not fixed, it will likely scroll as well.
 API int ncplane_scrollup_child(struct ncplane* n, const struct ncplane* child)
   __attribute__ ((nonnull (1, 2)));
 
diff --git a/src/lib/banner.c b/src/lib/banner.c
index 915a9e5663..45a6ac3d55 100644
--- a/src/lib/banner.c
+++ b/src/lib/banner.c
@@ -38,13 +38,13 @@ int init_banner(const notcurses* nc, fbuf* f){
     free(osver);
     term_fg_palindex(nc, f, nc->tcache.caps.colors <= 256 ?
                      14 % nc->tcache.caps.colors : 0x2080e0);
-    if(nc->tcache.cellpixy && nc->tcache.cellpixx){
+    if(nc->tcache.cellpxy && nc->tcache.cellpxx){
       fbuf_printf(f, "%s%d rows (%dpx) %d cols (%dpx) %dx%d ",
                   clreol,
-                  nc->stdplane->leny, nc->tcache.cellpixy,
-                  nc->stdplane->lenx, nc->tcache.cellpixx,
-                  nc->stdplane->leny * nc->tcache.cellpixy,
-                  nc->stdplane->lenx * nc->tcache.cellpixx);
+                  nc->stdplane->leny, nc->tcache.cellpxy,
+                  nc->stdplane->lenx, nc->tcache.cellpxx,
+                  nc->stdplane->leny * nc->tcache.cellpxy,
+                  nc->stdplane->lenx * nc->tcache.cellpxx);
     }else{
       fbuf_printf(f, "%d rows %d cols ",
                   nc->stdplane->leny, nc->stdplane->lenx);
diff --git a/src/lib/blitset.h b/src/lib/blitset.h
index 037faba9ce..b82bb1c3e9 100644
--- a/src/lib/blitset.h
+++ b/src/lib/blitset.h
@@ -9,7 +9,7 @@ extern "C" {
 static inline int
 encoding_y_scale(const tinfo* tcache, const struct blitset* bset) {
   if(bset->geom == NCBLIT_PIXEL){
-    return tcache->cellpixy;
+    return tcache->cellpxy;
   }
   return bset->height;
 }
@@ -18,7 +18,7 @@ encoding_y_scale(const tinfo* tcache, const struct blitset* bset) {
 static inline int
 encoding_x_scale(const tinfo* tcache, const struct blitset* bset) {
   if(bset->geom == NCBLIT_PIXEL){
-    return tcache->cellpixx;
+    return tcache->cellpxx;
   }
   return bset->width;
 }
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 425b3a84f8..d80882533e 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -654,8 +654,8 @@ ncdirect_render_visual(ncdirect* n, ncvisual* ncv,
       disprows = dimy * encoding_y_scale(&n->tcache, bset) - 1;
       outy = disprows;
     }else{
-      dispcols = dimx * n->tcache.cellpixx;
-      disprows = dimy * n->tcache.cellpixy;
+      dispcols = dimx * n->tcache.cellpxx;
+      disprows = dimy * n->tcache.cellpxy;
       clamp_to_sixelmax(&n->tcache, &disprows, &dispcols, &outy, vopts->scaling);
     }
     if(vopts->scaling == NCSCALE_SCALE || vopts->scaling == NCSCALE_SCALE_HIRES){
@@ -675,7 +675,7 @@ ncdirect_render_visual(ncdirect* n, ncvisual* ncv,
     }
   }
   if(bset->geom == NCBLIT_PIXEL){
-    while((outy + n->tcache.cellpixy - 1) / n->tcache.cellpixy > dimy){
+    while((outy + n->tcache.cellpxy - 1) / n->tcache.cellpxy > dimy){
       outy -= n->tcache.sprixel_scale_height;
       disprows = outy;
     }
@@ -693,8 +693,8 @@ ncdirect_render_visual(ncdirect* n, ncvisual* ncv,
     .flags = 0,
   };
   if(bset->geom == NCBLIT_PIXEL){
-    nopts.rows = outy / n->tcache.cellpixy + !!(outy % n->tcache.cellpixy);
-    nopts.cols = dispcols / n->tcache.cellpixx + !!(dispcols % n->tcache.cellpixx);
+    nopts.rows = outy / n->tcache.cellpxy + !!(outy % n->tcache.cellpxy);
+    nopts.cols = dispcols / n->tcache.cellpxx + !!(dispcols % n->tcache.cellpxx);
   }
   if(ymax && nopts.rows > ymax){
     nopts.rows = ymax;
@@ -717,7 +717,9 @@ ncdirect_render_visual(ncdirect* n, ncvisual* ncv,
   }
   if(bset->geom == NCBLIT_PIXEL){
     bargs.u.pixel.colorregs = n->tcache.color_registers;
-    if((bargs.u.pixel.spx = sprixel_alloc(&n->tcache, ncdv, nopts.rows, nopts.cols)) == NULL){
+    bargs.u.pixel.cellpxy = n->tcache.cellpxy;
+    bargs.u.pixel.cellpxx = n->tcache.cellpxx;
+    if((bargs.u.pixel.spx = sprixel_alloc(ncdv, nopts.rows, nopts.cols)) == NULL){
       free_plane(ncdv);
       return NULL;
     }
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 00a30a6172..480b9e46bc 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -288,17 +288,37 @@ struct crender {
   } s;
 };
 
+// a pile is a collection of planes which will be rendered together. piles are
+// completely distinct with regards to thread-safety; one can always operate
+// concurrently on distinct piles (save rasterizing, of course). material from
+// other piles will be blown away whenever a pile is rasterized. no ordering
+// is meaningful between planes. when a new one is added, or one is destroyed,
+// the pilelock (in struct notcurses) is taken. a pile is destroyed whenever
+// its last plane is destroyed or reparented away. a pile contains a totally-
+// ordered list of piles (ordered on the z axis) and a directed acyclic forest
+// of bound planes, with each root plane rooting a DAG.
+//
+// the geometries are updated each time the pile is rendered. until a pile is
+// rendered, its geometries might be out-of-date with regards to the terminal
+// description in tcache. when it is rendered again, the geometries will be
+// updated, sprixels will have their TAMs rebuilt (if necessary), and each
+// root plane will have its resize callback invoked (possibly invoking its
+// bound planes' resize callbacks in turn).
+//
+// at context start, there is one pile (the standard pile), containing one
+// plane (the standard plane). each ncplane holds a pointer to its pile.
 typedef struct ncpile {
   ncplane* top;               // topmost plane, never NULL
   ncplane* bottom;            // bottommost plane, never NULL
   ncplane* roots;             // head of root plane list
   struct crender* crender;    // array (rows * cols crender objects)
   struct notcurses* nc;       // notcurses context
-  struct ncpile *prev, *next; // circular list
+  struct ncpile *prev, *next; // circular list pointers
   size_t crenderlen;          // size of crender vector
-  unsigned dimy, dimx;        // rows and cols at time of last render
+  unsigned dimy, dimx;        // rows and cols at last render/creation
+  unsigned cellpxx, cellpxy;  // cell-pixel geometry at last render/creation
   int scrolls;                // how many real lines need be scrolled at raster
-  sprixel* sprixelcache;      // list of sprixels
+  sprixel* sprixelcache;      // sorted list of sprixels, assembled during paint
 } ncpile;
 
 // the standard pile can be reached through ->stdplane.
@@ -359,6 +379,8 @@ typedef struct blitterargs {
       sprixel* spx;    // sprixel object
       int pxoffy;      // pixel y-offset within origin cell
       int pxoffx;      // pixel x-offset within origin cell
+      int cellpxy;     // targeted cell-pixel geometry. this ought come from the
+      int cellpxx;     // target ncpile, or tcache in Direct Mode
     } pixel;           // for pixels
   } u;
 } blitterargs;
@@ -664,7 +686,7 @@ void sprixel_free(sprixel* s);
 void sprixel_hide(sprixel* s);
 
 // dimy and dimx are cell geometry, not pixel.
-sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx);
+sprixel* sprixel_alloc(ncplane* n, int dimy, int dimx);
 sprixel* sprixel_recycle(ncplane* n);
 int sprite_init(const tinfo* t, int fd);
 int sprite_clear_all(const tinfo* t, fbuf* f);
@@ -1748,7 +1770,7 @@ typedef struct ncvisual_implementation {
   int (*visual_decode_loop)(struct ncvisual* nc);
   int (*visual_stream)(notcurses* nc, struct ncvisual* ncv, float timescale,
                        ncstreamcb streamer, const struct ncvisual_options* vopts, void* curry);
-  struct ncplane* (*visual_subtitle)(struct ncplane* parent, const struct ncvisual* ncv);
+  ncplane* (*visual_subtitle)(ncplane* parent, const struct ncvisual* ncv);
   int rowalign; // rowstride base, can be 0 for no padding
   // do a persistent resize, changing the ncv itself
   int (*visual_resize)(struct ncvisual* ncv, unsigned rows, unsigned cols);
diff --git a/src/lib/kitty.c b/src/lib/kitty.c
index 6650b66227..df9a99233e 100644
--- a/src/lib/kitty.c
+++ b/src/lib/kitty.c
@@ -233,8 +233,8 @@ init_sprixel_animation(sprixel* s){
 // auxiliary vector back into the actual data. we then free the auxvector.
 int kitty_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   const int totalpixels = s->pixy * s->pixx;
-  const int xpixels = s->cellpxx;
-  const int ypixels = s->cellpxy;
+  const int xpixels = ncplane_pile(s->n)->cellpxx;
+  const int ypixels = ncplane_pile(s->n)->cellpxy;
   int targx = xpixels;
   if((xcell + 1) * xpixels > s->pixx){
     targx = s->pixx - xcell * xpixels;
@@ -308,8 +308,8 @@ int kitty_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 // wiping)?
 static inline unsigned
 kitty_anim_auxvec_blitsource_p(const sprixel* s, const uint8_t* auxvec){
-  const size_t offset = s->cellpxy * s->cellpxx * 4;
-  if(auxvec[offset]){
+  size_t off = ncplane_pile(s->n)->cellpxy * ncplane_pile(s->n)->cellpxx * 4;
+  if(auxvec[off]){
     return 1;
   }
   return 0;
@@ -354,8 +354,8 @@ kitty_anim_auxvec(int dimy, int dimx, int posy, int posx,
   return a;
 }
 
-uint8_t* kitty_trans_auxvec(const tinfo* ti){
-  const size_t slen = ti->cellpixy * ti->cellpixx;
+uint8_t* kitty_trans_auxvec(const ncpile* p){
+  const size_t slen = p->cellpxy * p->cellpxx;
   uint8_t* a = malloc(slen);
   if(a){
     memset(a, 0, slen);
@@ -367,13 +367,14 @@ uint8_t* kitty_trans_auxvec(const tinfo* ti){
 // by the wipe proper, and when blitting a new frame with annihilations.
 static int
 kitty_blit_wipe_selfref(sprixel* s, fbuf* f, int ycell, int xcell){
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
   if(fbuf_printf(f, "\x1b_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=2,c=1,q=2;",
-                 xcell * s->cellpxx, ycell * s->cellpxy,
-                 s->cellpxx, s->cellpxy, s->id) < 0){
+                 xcell * cellpxx, ycell * cellpxy, cellpxx, cellpxy, s->id) < 0){
     return -1;
   }
   // FIXME ought be smaller around the fringes!
-  int totalp = s->cellpxy * s->cellpxx;
+  int totalp = cellpxy * cellpxx;
   // FIXME preserve so long as cellpixel geom stays constant?
   #define TRINULLALPHA "AAAAAAAAAAAAAAAA"
   for(int p = 0 ; p + 3 <= totalp ; p += 3){
@@ -416,7 +417,7 @@ int kitty_wipe_animation(sprixel* s, int ycell, int xcell){
   }
   int tamidx = ycell * s->dimx + xcell;
   uint8_t* auxvec = s->n->tam[tamidx].auxvector;
-  auxvec[s->cellpxx * s->cellpxy * 4] = 0;
+  auxvec[ncplane_pile(s->n)->cellpxx * ncplane_pile(s->n)->cellpxy * 4] = 0;
   s->invalidated = SPRIXEL_INVALIDATED;
   return 1;
 }
@@ -444,14 +445,14 @@ sprixel* kitty_recycle(ncplane* n){
   int dimy = hides->dimy;
   int dimx = hides->dimx;
   sprixel_hide(hides);
-  return sprixel_alloc(&ncplane_notcurses_const(n)->tcache, n, dimy, dimx);
+  return sprixel_alloc(n, dimy, dimx);
 }
 
 // for pre-animation kitty (NCPIXEL_KITTY_STATIC), we need a byte per pixel,
 // in which we stash the alpha.
 static inline uint8_t*
 kitty_auxiliary_vector(const sprixel* s){
-  int pixels = s->cellpxy * s->cellpxx;
+  int pixels = ncplane_pile(s->n)->cellpxy * ncplane_pile(s->n)->cellpxx;
   uint8_t* ret = malloc(sizeof(*ret) * pixels);
   if(ret){
     memset(ret, 0, sizeof(*ret) * pixels);
@@ -466,8 +467,8 @@ int kitty_wipe(sprixel* s, int ycell, int xcell){
     return -1;
   }
   const int totalpixels = s->pixy * s->pixx;
-  const int xpixels = s->cellpxx;
-  const int ypixels = s->cellpxy;
+  const int xpixels = ncplane_pile(s->n)->cellpxx;
+  const int ypixels = ncplane_pile(s->n)->cellpxy;
   // if the cell is on the right or bottom borders, it might only be partially
   // filled by actual graphic data, and we need to cap our target area.
   int targx = xpixels;
@@ -515,7 +516,7 @@ int kitty_wipe(sprixel* s, int ycell, int xcell){
 //fprintf(stderr, "POSTCHOMP: [%.16s]\n", c + tripbytes);
       assert(chomped >= 0);
       auxvecidx += chomped;
-      assert(auxvecidx <= s->cellpxy * s->cellpxx);
+      assert(auxvecidx <= ypixels * xpixels);
       thisrow -= chomped;
 //fprintf(stderr, "POSTCHIMP CHOMP: %d pixoffset: %d next: %d tripbytes: %d tripskip: %d thisrow: %d\n", chomped, pixoffset, nextpixel, tripbytes, tripskip, thisrow);
       if(thisrow == 0){
@@ -746,8 +747,10 @@ write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
   unsigned bufidx = 0; // an index; the actual offset is bufidx * 4
   bool translucent = bargs->flags & NCVISUAL_OPTION_BLEND;
   sprixel* s = bargs->u.pixel.spx;
-  const int cdimy = s->cellpxy;
-  const int cdimx = s->cellpxx;
+  const int cdimy = bargs->u.pixel.cellpxy;
+  const int cdimx = bargs->u.pixel.cellpxx;
+  assert(0 != cdimy);
+  assert(0 != cdimx);
   const uint32_t transcolor = bargs->transcolor;
   int total = leny * lenx; // total number of pixels (4 * total == bytecount)
   // number of 4KiB chunks we'll need
@@ -847,7 +850,7 @@ write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
           }else if(level == NCPIXEL_KITTY_SELFREF){
             selfref_annihilated = true;
           }else{
-            ((uint8_t*)tam[tyx].auxvector)[s->cellpxx * s->cellpxy * 4] = 1;
+            ((uint8_t*)tam[tyx].auxvector)[cdimx * cdimy * 4] = 1;
             wipe[e] = 1;
           }
           if(rgba_trans_p(source[e], transcolor)){
@@ -938,14 +941,16 @@ int kitty_rebuild_selfref(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
     return -1;
   }
   fbuf* f = &s->glyph;
-  const int ystart = ycell * s->cellpxy;
-  const int xstart = xcell * s->cellpxx;
-  const int xlen = xstart + s->cellpxx > s->pixx ? s->pixx - xstart : s->cellpxx;
-  const int ylen = ystart + s->cellpxy > s->pixy ? s->pixy - ystart : s->cellpxy;
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
+  const int ystart = ycell * cellpxy;
+  const int xstart = xcell * cellpxx;
+  const int xlen = xstart + cellpxx > s->pixx ? s->pixx - xstart : cellpxx;
+  const int ylen = ystart + cellpxy > s->pixy ? s->pixy - ystart : cellpxy;
   logdebug("rematerializing %u at %d/%d (%dx%d)\n", s->id, ycell, xcell, ylen, xlen);
   fbuf_printf(f, "\e_Ga=c,x=%d,y=%d,X=%d,Y=%d,w=%d,h=%d,i=%d,r=1,c=2,q=2;\x1b\\",
-              xcell * s->cellpxx, ycell * s->cellpxy,
-              xcell * s->cellpxx, ycell * s->cellpxy,
+              xcell * cellpxx, ycell * cellpxy,
+              xcell * cellpxx, ycell * cellpxy,
               xlen, ylen, s->id);
   const int tyx = xcell + ycell * s->dimx;
   memcpy(&s->n->tam[tyx].state, auxvec, sizeof(s->n->tam[tyx].state));
@@ -959,10 +964,12 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
     return -1;
   }
   fbuf* f = &s->glyph;
-  const int ystart = ycell * s->cellpxy;
-  const int xstart = xcell * s->cellpxx;
-  const int xlen = xstart + s->cellpxx > s->pixx ? s->pixx - xstart : s->cellpxx;
-  const int ylen = ystart + s->cellpxy > s->pixy ? s->pixy - ystart : s->cellpxy;
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
+  const int ystart = ycell * cellpxy;
+  const int xstart = xcell * cellpxx;
+  const int xlen = xstart + cellpxx > s->pixx ? s->pixx - xstart : cellpxx;
+  const int ylen = ystart + cellpxy > s->pixy ? s->pixy - ystart : cellpxy;
   const int linesize = xlen * 4;
   const int total = xlen * ylen;
   const int tyx = xcell + ycell * s->dimx;
@@ -973,13 +980,13 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   int targetout = 0; // number of pixels expected out after this chunk
 //fprintf(stderr, "total: %d chunks = %d, s=%d,v=%d\n", total, chunks, lenx, leny);
   // FIXME this ought be factored out and shared with write_kitty_data()
-  logdebug("placing %d/%d at %d/%d\n", ylen, xlen, ycell * s->cellpxy, xcell * s->cellpxx);
+  logdebug("placing %d/%d at %d/%d\n", ylen, xlen, ycell * cellpxy, xcell * cellpxx);
   while(chunks--){
     if(totalout == 0){
       const int c = kitty_anim_auxvec_blitsource_p(s, auxvec) ? 2 : 1;
       const int r = kitty_anim_auxvec_blitsource_p(s, auxvec) ? 1 : 2;
       if(fbuf_printf(f, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,c=%d,r=%d,%s;",
-                     xcell * s->cellpxx, ycell * s->cellpxy, xlen, ylen,
+                     xcell * cellpxx, ycell * cellpxy, xlen, ylen,
                      s->id, c, r, chunks ? "m=1" : "q=2") < 0){
         return -1;
       }
@@ -1023,13 +1030,13 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 //fprintf(stderr, "Tyx: %d y: %d (%d) * %d x: %d (%d) state %d %p\n", tyx, y, y / cdimy, cols, x, x / cdimx, tam[tyx].state, tam[tyx].auxvector);
         wipe[e] = 0;
         if(rgba_trans_p(source[e], 0)){
-          if(x % s->cellpxx == 0 && y % s->cellpxy == 0){
+          if(x % cellpxx == 0 && y % cellpxy == 0){
             s->n->tam[tyx].state = SPRIXCELL_TRANSPARENT;
           }else if(s->n->tam[tyx].state == SPRIXCELL_OPAQUE_KITTY){
             s->n->tam[tyx].state = SPRIXCELL_MIXED_KITTY;
           }
         }else{
-          if(x % s->cellpxx == 0 && y % s->cellpxy == 0){
+          if(x % cellpxx == 0 && y % cellpxy == 0){
             s->n->tam[tyx].state = SPRIXCELL_OPAQUE_KITTY;
           }else if(s->n->tam[tyx].state == SPRIXCELL_TRANSPARENT){
             s->n->tam[tyx].state = SPRIXCELL_MIXED_KITTY;
diff --git a/src/lib/linux.c b/src/lib/linux.c
index b73efc80b6..c0c3040649 100644
--- a/src/lib/linux.c
+++ b/src/lib/linux.c
@@ -1,11 +1,11 @@
 #include "linux.h"
 #include "internal.h"
 
-// auxvecs for framebuffer are 1B each for s->cellpxx * s->cellpxy elements,
+// auxvecs for framebuffer are 1B each for cellpxx * cellpxy elements,
 // and store the original alpha value.
 static inline uint8_t*
 fbcon_auxiliary_vector(const sprixel* s){
-  int pixels = s->cellpxy * s->cellpxx;
+  int pixels = ncplane_pile(s->n)->cellpxy * ncplane_pile(s->n)->cellpxx;
   uint8_t* ret = malloc(sizeof(*ret) * pixels);
   if(ret){
     memset(ret, 0, sizeof(*ret) * pixels);
@@ -18,19 +18,21 @@ int fbcon_wipe(sprixel* s, int ycell, int xcell){
   if(auxvec == NULL){
     return -1;
   }
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
   char* glyph = s->glyph.buf;
-  for(int y = 0 ; y < s->cellpxy ; ++y){
-    if(ycell * s->cellpxy + y >= s->pixy){
+  for(int y = 0 ; y < cellpxy ; ++y){
+    if(ycell * cellpxy + y >= s->pixy){
       break;
     }
     // number of pixels total above our pixel row
-    const size_t yoff = s->pixx * (ycell * s->cellpxy + y);
-    for(int x = 0 ; x < s->cellpxx ; ++x){
-      if(xcell * s->cellpxx + x >= s->pixx){
+    const size_t yoff = s->pixx * (ycell * cellpxy + y);
+    for(int x = 0 ; x < cellpxx ; ++x){
+      if(xcell * cellpxx + x >= s->pixx){
         break;
       }
-      size_t offset = (yoff + xcell * s->cellpxx + x) * 4;
-      const int vyx = (y % s->cellpxy) * s->cellpxx + x;
+      size_t offset = (yoff + xcell * cellpxx + x) * 4;
+      const int vyx = (y % cellpxy) * cellpxx + x;
       auxvec[vyx] = glyph[offset + 3];
       glyph[offset + 3] = 0;
     }
@@ -43,8 +45,8 @@ int fbcon_blit(struct ncplane* n, int linesize, const void* data,
                int leny, int lenx, const struct blitterargs* bargs){
   uint32_t transcolor = bargs->transcolor;
   sprixel* s = bargs->u.pixel.spx;
-  int cdimx = s->cellpxx;
-  int cdimy = s->cellpxy;
+  int cdimx = bargs->u.pixel.cellpxx;
+  int cdimy = bargs->u.pixel.cellpxy;
   // FIXME this will need be a copy of the tinfo's fbuf map
   size_t flen = leny * lenx * 4;
   if(fbuf_reserve(&s->glyph, flen)){
@@ -124,18 +126,20 @@ int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   if(auxvec == NULL){
     return -1;
   }
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
   sprixcell_e state = SPRIXCELL_TRANSPARENT;
-  for(int y = 0 ; y < s->cellpxy ; ++y){
-    if(ycell * s->cellpxy + y >= s->pixy){
+  for(int y = 0 ; y < cellpxy ; ++y){
+    if(ycell * cellpxy + y >= s->pixy){
       break;
     }
-    const size_t yoff = s->pixx * (ycell * s->cellpxy + y);
-    for(int x = 0 ; x < s->cellpxx ; ++x){
-      if(xcell * s->cellpxx + x >= s->pixx){
+    const size_t yoff = s->pixx * (ycell * cellpxy + y);
+    for(int x = 0 ; x < cellpxx ; ++x){
+      if(xcell * cellpxx + x >= s->pixx){
         break;
       }
-      size_t offset = (yoff + xcell * s->cellpxx + x) * 4;
-      const int vyx = (y % s->cellpxy) * s->cellpxx + x;
+      size_t offset = (yoff + xcell * cellpxx + x) * 4;
+      const int vyx = (y % cellpxy) * cellpxx + x;
       if(x == 0 && y == 0){
         if(auxvec[vyx] == 0){
           state = SPRIXCELL_TRANSPARENT;
@@ -160,9 +164,11 @@ int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 int fbcon_draw(const tinfo* ti, sprixel* s, int y, int x){
   logdebug("id %" PRIu32 " dest %d/%d\n", s->id, y, x);
   int wrote = 0;
-  for(unsigned l = 0 ; l < (unsigned)s->pixy && l + y * ti->cellpixy < ti->pixy ; ++l){
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
+  for(unsigned l = 0 ; l < (unsigned)s->pixy && l + y * cellpxy < ti->pixy ; ++l){
     // FIXME pixel size isn't necessarily 4B, line isn't necessarily psize*pixx
-    size_t offset = ((l + y * ti->cellpixy) * ti->pixx + x * ti->cellpixx) * 4;
+    size_t offset = ((l + y * cellpxy) * ti->pixx + x * cellpxx) * 4;
     uint8_t* tl = ti->linux_fbuffer + offset;
     const char* src = (char*)s->glyph.buf + (l * s->pixx * 4);
     for(unsigned c = 0 ; c < (unsigned)s->pixx && c < ti->pixx ; ++c){
@@ -186,13 +192,15 @@ int fbcon_draw(const tinfo* ti, sprixel* s, int y, int x){
 // they're written in order. if we're scrolling all rows, we're clearing the
 // entire space; we always clear something (we might not always move anything).
 void fbcon_scroll(const struct ncpile* p, tinfo* ti, int rows){
-  if(ti->cellpixy < 1){
+  const int cellpxy = p->cellpxy;
+  const int cellpxx = p->cellpxx;
+  if(cellpxy < 1){
     return;
   }
   logdebug("scrolling %d\n", rows);
-  const int rowbytes = ti->cellpixx * p->dimx * 4;
-  const int totalrows = ti->cellpixy * p->dimy;
-  int srows = rows * ti->cellpixy; // number of pixel rows being scrolled
+  const int rowbytes = cellpxx * p->dimx * 4;
+  const int totalrows = cellpxy * p->dimy;
+  int srows = rows * cellpxy; // number of pixel rows being scrolled
   if(srows > totalrows){
     srows = totalrows;
   }
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 193fbf91f7..11bf3ceadd 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -261,8 +261,8 @@ int update_term_dimensions(unsigned* rows, unsigned* cols, tinfo* tcache,
     if(cols){
       *cols = tcache->default_cols;
     }
-    tcache->cellpixy = 0;
-    tcache->cellpixx = 0;
+    tcache->cellpxy = 0;
+    tcache->cellpxx = 0;
     return 0;
   }
   unsigned rowsafe, colsafe;
@@ -296,18 +296,18 @@ int update_term_dimensions(unsigned* rows, unsigned* cols, tinfo* tcache,
       tcache->pixx = ws.ws_xpixel;
     }
     // update even if we didn't get values just now, because we need set
-    // cellpix{y,x} up from an initial CSI14n, which set only pix{y,x}.
+    // cellpx{y,x} up from an initial CSI14n, which set only pix{y,x}.
     unsigned cpixy = ws.ws_row ? tcache->pixy / ws.ws_row : 0;
     unsigned cpixx = ws.ws_col ? tcache->pixx / ws.ws_col : 0;
-    if(tcache->cellpixy != cpixy){
-      tcache->cellpixy = cpixy;
+    if(tcache->cellpxy != cpixy){
+      tcache->cellpxy = cpixy;
       *pgeo_changed = 1;
     }
-    if(tcache->cellpixx != cpixx){
-      tcache->cellpixx = cpixx;
+    if(tcache->cellpxx != cpixx){
+      tcache->cellpxx = cpixx;
       *pgeo_changed = 1;
     }
-    if(tcache->cellpixy == 0 || tcache->cellpixx == 0){
+    if(tcache->cellpxy == 0 || tcache->cellpxx == 0){
       tcache->pixel_draw = NULL; // disable support
     }
   }
@@ -363,7 +363,7 @@ int update_term_dimensions(unsigned* rows, unsigned* cols, tinfo* tcache,
     if(margin_b){
       ++sixelrows;
     }
-    tcache->sixel_maxy = sixelrows * tcache->cellpixy;
+    tcache->sixel_maxy = sixelrows * tcache->cellpxy;
     if(tcache->sixel_maxy > tcache->sixel_maxy_pristine){
       tcache->sixel_maxy = tcache->sixel_maxy_pristine;
     }
@@ -451,8 +451,10 @@ make_ncpile(notcurses* nc, ncplane* n){
     }
     n->above = NULL;
     n->below = NULL;
-    ret->dimy = 0;
-    ret->dimx = 0;
+    ret->dimy = nc->tcache.dimy;
+    ret->dimx = nc->tcache.dimx;
+    ret->cellpxy = nc->tcache.cellpxy;
+    ret->cellpxx = nc->tcache.cellpxx;
     ret->crender = NULL;
     ret->crenderlen = 0;
     ret->sprixelcache = NULL;
@@ -3102,28 +3104,29 @@ void ncplane_pixel_geom(const ncplane* n,
                         unsigned* RESTRICT pxy, unsigned* RESTRICT pxx,
                         unsigned* RESTRICT celldimy, unsigned* RESTRICT celldimx,
                         unsigned* RESTRICT maxbmapy, unsigned* RESTRICT maxbmapx){
-  notcurses* nc = ncplane_notcurses(n);
+  const notcurses* nc = ncplane_notcurses_const(n);
+  const ncpile* p = ncplane_pile_const(n);
   if(celldimy){
-    *celldimy = nc->tcache.cellpixy;
+    *celldimy = p->cellpxy;
   }
   if(celldimx){
-    *celldimx = nc->tcache.cellpixx;
+    *celldimx = p->cellpxx;
   }
   if(pxy){
-    *pxy = nc->tcache.cellpixy * ncplane_dim_y(n);
+    *pxy = p->cellpxy * ncplane_dim_y(n);
   }
   if(pxx){
-    *pxx = nc->tcache.cellpixx * ncplane_dim_x(n);
+    *pxx = p->cellpxx * ncplane_dim_x(n);
   }
   if(notcurses_check_pixel_support(nc) > 0){
     if(maxbmapy){
-      *maxbmapy = nc->tcache.cellpixy * ncplane_dim_y(n);
+      *maxbmapy = p->cellpxy * ncplane_dim_y(n);
       if(*maxbmapy > nc->tcache.sixel_maxy && nc->tcache.sixel_maxy){
         *maxbmapy = nc->tcache.sixel_maxy;
       }
     }
     if(maxbmapx){
-      *maxbmapx = nc->tcache.cellpixx * ncplane_dim_x(n);
+      *maxbmapx = p->cellpxx * ncplane_dim_x(n);
       if(*maxbmapx > nc->tcache.sixel_maxx && nc->tcache.sixel_maxx){
         *maxbmapx = nc->tcache.sixel_maxx;
       }
diff --git a/src/lib/plot.c b/src/lib/plot.c
index e610d7234f..9fce90974f 100644
--- a/src/lib/plot.c
+++ b/src/lib/plot.c
@@ -59,11 +59,11 @@ create_pixelp(ncplot *p, ncplane* n){
 // we have some color gradient across the life of the plot (almost; it gets
 // recalculated if the cell-pixel geometry changes and we're using
 // NCBLIT_PIXEL). if we're using cell blitting, we only get one channel pair
-// per row, no matter what height we have. with pixels, we get cellpixy * rows.
+// per row, no matter what height we have. with pixels, we get cellpxy * rows.
 static int
 calculate_gradient_vector(ncplot* p, unsigned pixelp){
   const int dimy = ncplane_dim_y(p->ncp);
-  const unsigned states = dimy * (pixelp ? ncplane_notcurses(p->ncp)->tcache.cellpixy : 1);
+  const unsigned states = dimy * (pixelp ? ncplane_pile(p->ncp)->cellpxy : 1);
   if(states == p->chancount){ // no need to recalculate
     return 0;
   }
@@ -93,13 +93,13 @@ int redraw_pixelplot_##T(nc##X##plot* ncp){ \
   if(calculate_gradient_vector(&ncp->plot, 1)){ \
     return -1; \
   } \
-  const int scale = ncplane_notcurses_const(ncp->plot.ncp)->tcache.cellpixx; \
+  const int scale = ncplane_pile_const(ncp->plot.ncp)->cellpxx; \
   ncplane_erase(ncp->plot.ncp); \
   unsigned dimy, dimx; \
   ncplane_dim_yx(ncp->plot.ncp, &dimy, &dimx); \
   const unsigned scaleddim = dimx * scale; \
   /* each transition is worth this much change in value */ \
-  const size_t states = ncplane_notcurses_const(ncp->plot.ncp)->tcache.cellpixy; \
+  const size_t states = ncplane_pile_const(ncp->plot.ncp)->cellpxy; \
   /* FIXME can we not rid ourselves of this meddlesome double? either way, the \
      interval is one row's range (for linear plots), or the base \
      (base^slots == maxy-miny) of the range (for exponential plots). */ \
@@ -480,8 +480,8 @@ create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T mi
   ncpp->plot.rangex = opts->rangex; \
   /* if we're sizing the plot based off the plane dimensions, scale it by the \
      plot geometry's width for all calculations */ \
-  const unsigned scaleddim = dimx * (bset->geom == NCBLIT_PIXEL ? ncplane_notcurses(n)->tcache.cellpixx : bset->width); \
-  const unsigned scaledprefixlen = NCPREFIXCOLUMNS * (bset->geom == NCBLIT_PIXEL ? ncplane_notcurses(n)->tcache.cellpixx : bset->width); \
+  const unsigned scaleddim = dimx * (bset->geom == NCBLIT_PIXEL ? ncplane_pile_const(n)->cellpxx : bset->width); \
+  const unsigned scaledprefixlen = NCPREFIXCOLUMNS * (bset->geom == NCBLIT_PIXEL ? ncplane_pile_const(n)->cellpxx : bset->width); \
   if((ncpp->plot.slotcount = ncpp->plot.rangex) == 0){ \
     ncpp->plot.slotcount = scaleddim; \
   } \
diff --git a/src/lib/sixel.c b/src/lib/sixel.c
index f7de3c308b..a2ae2c213b 100644
--- a/src/lib/sixel.c
+++ b/src/lib/sixel.c
@@ -27,7 +27,7 @@ sixelcount(int dimy, int dimx){
 // bytes, saving 7/8 of the space FIXME).
 static inline uint8_t*
 sixel_auxiliary_vector(const sprixel* s){
-  int pixels = s->cellpxy * s->cellpxx;
+  int pixels = ncplane_pile(s->n)->cellpxy * ncplane_pile(s->n)->cellpxx;
   uint8_t* ret = malloc(sizeof(*ret) * pixels * 2);
   if(ret){
     memset(ret, 0, sizeof(*ret) * pixels);
@@ -229,15 +229,17 @@ int sixel_wipe(sprixel* s, int ycell, int xcell){
   if(auxvec == NULL){
     return -1;
   }
-  memset(auxvec + s->cellpxx * s->cellpxy, 0xff, s->cellpxx * s->cellpxy);
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
+  memset(auxvec + cellpxx * cellpxy, 0xff, cellpxx * cellpxy);
   sixelmap* smap = s->smap;
-  const int startx = xcell * s->cellpxx;
-  const int starty = ycell * s->cellpxy;
-  int endx = ((xcell + 1) * s->cellpxx) - 1;
+  const int startx = xcell * cellpxx;
+  const int starty = ycell * cellpxy;
+  int endx = ((xcell + 1) * cellpxx) - 1;
   if(endx >= s->pixx){
     endx = s->pixx - 1;
   }
-  int endy = ((ycell + 1) * s->cellpxy) - 1;
+  int endy = ((ycell + 1) * cellpxy) - 1;
   if(endy >= s->pixy){
     endy = s->pixy - 1;
   }
@@ -248,7 +250,7 @@ int sixel_wipe(sprixel* s, int ycell, int xcell){
   int w = 0;
   for(int c = 0 ; c < smap->colors ; ++c){
     w |= wipe_color(smap, c, startband, endband, startx, endx, starty, endy,
-                    s->pixx, s->cellpxy, s->cellpxx, auxvec);
+                    s->pixx, cellpxy, cellpxx, auxvec);
   }
   if(w){
     s->wipes_outstanding = true;
@@ -429,8 +431,8 @@ extract_color_table(const uint32_t* data, int linesize, int cols,
   const int begx = bargs->begx;
   const int begy = bargs->begy;
   sprixel* s = bargs->u.pixel.spx;
-  const int cdimy = s->cellpxy;
-  const int cdimx = s->cellpxx;
+  const int cdimy = ncplane_pile(s->n)->cellpxy;
+  const int cdimx = ncplane_pile(s->n)->cellpxx;
   unsigned char mask = 0xc0;
   int pos = 0; // pixel position
   unsigned char* rmatrix = bargs->u.pixel.spx->needs_refresh;
@@ -876,11 +878,14 @@ sixel_reblit(sprixel* s){
 // scaled geometry in pixels. We calculate output geometry herein, and supply
 // transparent filler input for any missing rows.
 static inline int
-sixel_blit_inner(int leny, int lenx, sixeltable* stab, sprixel* s, tament* tam){
+sixel_blit_inner(int leny, int lenx, sixeltable* stab, const blitterargs* bargs, tament* tam){
   fbuf f;
   if(fbuf_init(&f)){
     return -1;
   }
+  sprixel* s = bargs->u.pixel.spx;
+  const int cellpxy = bargs->u.pixel.cellpxy;
+  const int cellpxx = bargs->u.pixel.cellpxx;
   int parse_start = 0;
   int outy = leny;
   if(leny % 6){
@@ -892,7 +897,7 @@ sixel_blit_inner(int leny, int lenx, sixeltable* stab, sprixel* s, tament* tam){
     fbuf_free(&f);
     return -1;
   }
-  scrub_tam_boundaries(tam, outy, lenx, s->cellpxy, s->cellpxx);
+  scrub_tam_boundaries(tam, outy, lenx, cellpxy, cellpxx);
   // take ownership of buf on success
   if(plane_blit_sixel(s, &f, outy, lenx, parse_start, tam, SPRIXEL_INVALIDATED) < 0){
     fbuf_free(&f);
@@ -945,7 +950,7 @@ int sixel_blit(ncplane* n, int linesize, const void* data, int leny, int lenx,
   }
   refine_color_table(data, linesize, bargs->begy, bargs->begx, leny, lenx, &stable);
   // takes ownership of sixelmap on success
-  int r = sixel_blit_inner(leny, lenx, &stable, bargs->u.pixel.spx, n->tam);
+  int r = sixel_blit_inner(leny, lenx, &stable, bargs, n->tam);
   if(r < 0){
     sixelmap_free(stable.map);
   }
@@ -1063,13 +1068,15 @@ int sixel_init_inverted(int fd){
 int sixel_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
   s->wipes_outstanding = true;
   sixelmap* smap = s->smap;
-  const int startx = xcell * s->cellpxx;
-  const int starty = ycell * s->cellpxy;
-  int endx = ((xcell + 1) * s->cellpxx) - 1;
+  const int cellpxx = ncplane_pile(s->n)->cellpxx;
+  const int cellpxy = ncplane_pile(s->n)->cellpxy;
+  const int startx = xcell * cellpxx;
+  const int starty = ycell * cellpxy;
+  int endx = ((xcell + 1) * cellpxx) - 1;
   if(endx > s->pixx){
     endx = s->pixx;
   }
-  int endy = ((ycell + 1) * s->cellpxy) - 1;
+  int endy = ((ycell + 1) * cellpxy) - 1;
   if(endy > s->pixy){
     endy = s->pixy;
   }
@@ -1077,8 +1084,8 @@ int sixel_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 //fprintf(stderr, "%d/%d start: %d/%d end: %d/%d bands: %d-%d\n", ycell, xcell, starty, startx, endy, endx, starty / 6, endy / 6);
   for(int x = startx ; x <= endx ; ++x){
     for(int y = starty ; y <= endy ; ++y){
-      int auxvecidx = (y - starty) * s->cellpxx + (x - startx);
-      int trans = auxvec[s->cellpxx * s->cellpxy + auxvecidx];
+      int auxvecidx = (y - starty) * cellpxx + (x - startx);
+      int trans = auxvec[cellpxx * cellpxy + auxvecidx];
       if(!trans){
         int color = auxvec[auxvecidx];
         int didx = ctable_to_dtable(smap->table + color * CENTSIZE);
@@ -1094,7 +1101,7 @@ int sixel_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
     }
   }
   sprixcell_e newstate;
-  if(transparent == s->cellpxx * s->cellpxy){
+  if(transparent == cellpxx * cellpxy){
     newstate = SPRIXCELL_TRANSPARENT;
   }else if(transparent){
     newstate = SPRIXCELL_MIXED_SIXEL;
@@ -1111,8 +1118,8 @@ int sixel_shutdown(fbuf* f){
   return 0;
 }
 
-uint8_t* sixel_trans_auxvec(const tinfo* ti){
-  const size_t slen = 2 * ti->cellpixy * ti->cellpixx;
+uint8_t* sixel_trans_auxvec(const ncpile* p){
+  const size_t slen = 2 * p->cellpxy * p->cellpxx;
   uint8_t* a = malloc(slen);
   if(a){
     memset(a, 0, slen);
diff --git a/src/lib/sprite.c b/src/lib/sprite.c
index 52d46db718..fc86261653 100644
--- a/src/lib/sprite.c
+++ b/src/lib/sprite.c
@@ -56,7 +56,7 @@ sprixel* sprixel_recycle(ncplane* n){
     int dimy = hides->dimy;
     int dimx = hides->dimx;
     sprixel_hide(hides);
-    return sprixel_alloc(&nc->tcache, n, dimy, dimx);
+    return sprixel_alloc(n, dimy, dimx);
   }
   sixelmap_free(n->sprite->smap);
   n->sprite->smap = NULL;
@@ -114,7 +114,7 @@ void sprixel_invalidate(sprixel* s, int y, int x){
   }
 }
 
-sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx){
+sprixel* sprixel_alloc(ncplane* n, int dimy, int dimx){
   sprixel* ret = malloc(sizeof(sprixel));
   if(ret == NULL){
     return NULL;
@@ -134,8 +134,6 @@ sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx){
     sprixelid_nonce = 1;
   }
 //fprintf(stderr, "LOOKING AT %p (p->n = %p)\n", ret, ret->n);
-  ret->cellpxy = ti->cellpixy;
-  ret->cellpxx = ti->cellpixx;
   if(ncplane_pile(ret->n)){ // rendered mode
     ncpile* np = ncplane_pile(ret->n);
     if( (ret->next = np->sprixelcache) ){
@@ -156,16 +154,6 @@ sprixel* sprixel_alloc(const tinfo* ti, ncplane* n, int dimy, int dimx){
 int sprixel_load(sprixel* spx, fbuf* f, unsigned pixy, unsigned pixx,
                  int parse_start, sprixel_e state){
   assert(spx->n);
-  if(spx->cellpxy > 0){ // don't explode on ncdirect case
-    if((pixy + spx->cellpxy - 1) / spx->cellpxy > spx->dimy){
-      logerror("bad pixy %d (cellpxy %d dimy %d)\n", pixy, spx->cellpxy, spx->dimy);
-      return -1;
-    }
-    if((pixx + spx->cellpxx - 1) / spx->cellpxx > spx->dimx){
-      logerror("bad pixx %d (cellpxx %d dimx %d)\n", pixx, spx->cellpxx, spx->dimx);
-      return -1;
-    }
-  }
   if(&spx->glyph != f){
     fbuf_free(&spx->glyph);
     memcpy(&spx->glyph, f, sizeof(*f));
@@ -180,6 +168,7 @@ int sprixel_load(sprixel* spx, fbuf* f, unsigned pixy, unsigned pixx,
 // returns 1 if already annihilated, 0 if we successfully annihilated the cell,
 // or -1 if we could not annihilate the cell (i.e. we're sixel).
 int sprite_wipe(const notcurses* nc, sprixel* s, int ycell, int xcell){
+  assert(s->n);
   int idx = s->dimx * ycell + xcell;
   if(s->n->tam[idx].state == SPRIXCELL_TRANSPARENT){
     // need to make a transparent auxvec, because a reload will force us to
@@ -187,7 +176,7 @@ int sprite_wipe(const notcurses* nc, sprixel* s, int ycell, int xcell){
     // be entirely 0s coming from pixel_trans_auxvec().
     if(s->n->tam[idx].auxvector == NULL){
       if(nc->tcache.pixel_trans_auxvec){
-        s->n->tam[idx].auxvector = nc->tcache.pixel_trans_auxvec(&nc->tcache);
+        s->n->tam[idx].auxvector = nc->tcache.pixel_trans_auxvec(ncplane_pile(s->n));
         if(s->n->tam[idx].auxvector == NULL){
           return -1;
         }
diff --git a/src/lib/sprite.h b/src/lib/sprite.h
index 342f64db25..ebb2889bfd 100644
--- a/src/lib/sprite.h
+++ b/src/lib/sprite.h
@@ -143,7 +143,6 @@ typedef struct sprixel {
   struct sprixel* prev;
   unsigned dimy, dimx;  // cell geometry
   int pixy, pixx;       // pixel geometry (might be smaller than cell geo)
-  int cellpxy, cellpxx; // cell-pixel geometry at time of creation
   // each tacache entry is one of 0 (standard opaque cell), 1 (cell with
   // some transparency), 2 (annihilated, excised)
   int movedfromy;       // for SPRIXEL_MOVED, the starting absolute position,
@@ -196,8 +195,8 @@ int kitty_remove(int id, fbuf* f);
 int kitty_clear_all(fbuf* f);
 int sixel_init(int fd);
 int sixel_init_inverted(int fd);
-uint8_t* sixel_trans_auxvec(const struct tinfo* ti);
-uint8_t* kitty_trans_auxvec(const struct tinfo* ti);
+uint8_t* sixel_trans_auxvec(const struct ncpile* p);
+uint8_t* kitty_trans_auxvec(const struct ncpile* p);
 int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll);
 int sixel_blit(struct ncplane* nc, int linesize, const void* data,
                int leny, int lenx, const struct blitterargs* bargs);
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index 7df059cfbe..07e0eb4712 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -1078,8 +1078,8 @@ int interrogate_terminfo(tinfo* ti, FILE* out, unsigned utf8,
       ti->pixx = iresp->pixx;
     }
     if(ti->default_rows && ti->default_cols){
-      ti->cellpixy = ti->pixy / ti->default_rows;
-      ti->cellpixx = ti->pixx / ti->default_cols;
+      ti->cellpxy = ti->pixy / ti->default_rows;
+      ti->cellpxx = ti->pixx / ti->default_cols;
     }
     if(iresp->got_bg){
       // reset the 0xfe000000 we loaded during initialization. if we're
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index 5b6ca4d413..b8d6944d03 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -110,8 +110,8 @@ typedef struct tinfo {
   unsigned pixx;                   // total pixel geometry, width
   // we use the cell's size in pixels for pixel blitting. this information can
   // be acquired on all terminals with pixel support.
-  unsigned cellpixy;               // cell pixel height, might be 0
-  unsigned cellpixx;               // cell pixel width, might be 0
+  unsigned cellpxy;                // cell pixel height, might be 0
+  unsigned cellpxx;                // cell pixel width, might be 0
   unsigned dimy, dimx;             // most recent cell geometry
 
   unsigned supported_styles; // bitmask over NCSTYLE_* driven via sgr/ncv
@@ -149,7 +149,7 @@ typedef struct tinfo {
   int (*pixel_commit)(fbuf* f, struct sprixel* s, unsigned noscroll);
   // scroll all graphics up. only used with fbcon.
   void (*pixel_scroll)(const struct ncpile* p, struct tinfo*, int rows);
-  uint8_t* (*pixel_trans_auxvec)(const struct tinfo* ti); // create tranparent auxvec
+  uint8_t* (*pixel_trans_auxvec)(const struct ncpile* p); // create tranparent auxvec
   // sprixel parameters. there are several different sprixel protocols, of
   // which we support sixel and kitty. the kitty protocol is used based
   // on TERM heuristics. otherwise, we attempt to detect sixel support, and
diff --git a/src/lib/visual.c b/src/lib/visual.c
index d6d866a6dd..6f5cee8dff 100644
--- a/src/lib/visual.c
+++ b/src/lib/visual.c
@@ -141,22 +141,27 @@ ncvisual_origin(const struct ncvisual_options* vopts, unsigned* restrict begy,
 // 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.
+//
+// cellpxy/cellpxx and dimy/dimx ought describe the cell-pixel and cell
+// geometry of the target pile or, in Direct Mode, the tcache.
 static void
-shape_sprixel_plane(const tinfo* ti, ncplane* parent, const ncvisual* ncv,
+shape_sprixel_plane(const tinfo* ti, unsigned cellpxy, unsigned cellpxx,
+                    unsigned dimy, unsigned dimx,
+                    ncplane* parent, const ncvisual* ncv,
                     ncscale_e scaling, unsigned* disppixy, unsigned* disppixx,
                     uint64_t flags, unsigned* outy, unsigned* 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;
+      *disppixy = dimy;
+      *disppixx = dimx;
     }else{
       ncplane_dim_yx(parent, disppixy, disppixx);
     }
     // FIXME why do we clamp only vertical, not horizontal, here?
-    if(*placey + *disppixy >= ti->dimy){
-      *disppixy = ti->dimy - *placey;
+    if(*placey + *disppixy >= dimy){
+      *disppixy = dimy - *placey;
     }
     if(!(flags & NCVISUAL_OPTION_VERALIGNED)){
       *disppixy -= *placey;
@@ -164,8 +169,8 @@ shape_sprixel_plane(const tinfo* ti, ncplane* parent, const ncvisual* ncv,
     if(!(flags & NCVISUAL_OPTION_HORALIGNED)){
       *disppixx -= *placex;
     }
-    *disppixx *= ti->cellpixx;
-    *disppixy *= ti->cellpixy;
+    *disppixx *= cellpxx;
+    *disppixy *= cellpxy;
     *disppixx += pxoffx;
     *disppixy += pxoffy;
     *outx = *disppixx;
@@ -187,10 +192,16 @@ shape_sprixel_plane(const tinfo* ti, ncplane* parent, const ncvisual* ncv,
   *disppixx -= pxoffx;
 }
 
-// in addition to the geom fields, we pass out:
+// in addition to the fields in 'geom', 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
+//  * 'bset': blitter that will be used
+// we take in:
+//  * 'p': target pile (for cell-pixel and cell geometry)
+//  * 'ti': used if p is NULL (direct mode only!)
+//  * 'n': input ncvisual
+//  * 'vopts': requested ncvisual_options
 int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
                         const struct ncvisual_options* vopts, ncvgeom* geom,
                         const struct blitset** bset,
@@ -230,7 +241,7 @@ int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
     geom->pixy = n->pixy;
     geom->pixx = n->pixx;
   }
-  // when nc is NULL, we only report properties intrinsic to the ncvisual,
+  // when ti is NULL, we only report properties intrinsic to the ncvisual,
   // i.e. only its original pixel geometry.
   if(ti == NULL){
     return 0;
@@ -241,8 +252,9 @@ int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
     logerror("couldn't get a blitter for %d\n", vopts ? vopts->blitter : NCBLIT_DEFAULT);
     return -1;
   }
-  geom->cdimy = ti->cellpixy;
-  geom->cdimx = ti->cellpixx;
+  const ncpile* p = vopts->n ? ncplane_pile_const(vopts->n) : NULL;
+  geom->cdimy = p ? p->cellpxy : ti->cellpxy;
+  geom->cdimx = p ? p->cellpxx : ti->cellpxx;
   if((geom->blitter = (*bset)->geom) == NCBLIT_PIXEL){
     geom->maxpixely = ti->sixel_maxy;
     geom->maxpixelx = ti->sixel_maxx;
@@ -301,41 +313,44 @@ int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
         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);
+      if(vopts->pxoffy >= geom->cdimy){
+        logerror("pixel y-offset %d too tall for cell %d\n", vopts->pxoffy, geom->cdimy);
         return -1;
       }
-      if(vopts->pxoffx >= ti->cellpixx){
-        logerror("pixel x-offset %d too wide for cell %d\n", vopts->pxoffx, ti->cellpixx);
+      if(vopts->pxoffx >= geom->cdimx){
+        logerror("pixel x-offset %d too wide for cell %d\n", vopts->pxoffx, geom->cdimx);
         return -1;
       }
       if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
         // FIXME clamp to sprixel limits
-        unsigned rows = ((geom->leny + ti->cellpixy - 1) / ti->cellpixy) + !!vopts->pxoffy;
+        unsigned rows = ((geom->leny + geom->cdimy - 1) / geom->cdimy) + !!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);
+                   ncplane_dim_y(vopts->n) * geom->cdimy);
           return -1;
         }
-        unsigned cols = ((geom->lenx + ti->cellpixx - 1) / ti->cellpixx) + !!vopts->pxoffx;
+        unsigned cols = ((geom->lenx + geom->cdimx - 1) / geom->cdimx) + !!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);
+                   ncplane_dim_x(vopts->n) * geom->cdimx);
           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,
+      const int dimy = p ? p->dimy : ti->dimy;
+      const int dimx = p ? p->dimx : ti->dimx;
+      shape_sprixel_plane(ti, geom->cdimy, geom->cdimx, dimy, dimx,
+                          vopts->n, n, scaling, disppixy, disppixx,
                           vopts->flags, outy, outx, placey, placex,
                           vopts->pxoffy, vopts->pxoffx);
     }else{
       if(scaling != NCSCALE_NONE && scaling != NCSCALE_NONE_HIRES){
         ncplane_dim_yx(vopts->n, disppixy, disppixx);
-        *disppixx *= ti->cellpixx;
+        *disppixx *= geom->cdimx;
         *disppixx += vopts->pxoffx;
-        *disppixy *= ti->cellpixy;
+        *disppixy *= geom->cdimy;
         *disppixy += vopts->pxoffy;
         clamp_to_sixelmax(ti, disppixy, disppixx, outy, scaling);
         int absplacex = 0, absplacey = 0;
@@ -345,8 +360,8 @@ int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
         if(!(vopts->flags & NCVISUAL_OPTION_VERALIGNED)){
           absplacey = *placey;
         }
-        *disppixx -= absplacex * ti->cellpixx;
-        *disppixy -= absplacey * ti->cellpixy;
+        *disppixx -= absplacex * geom->cdimx;
+        *disppixy -= absplacey * geom->cdimy;
       }else{
         *disppixx = geom->lenx + vopts->pxoffx;
         *disppixy = geom->leny + vopts->pxoffy;
@@ -358,7 +373,7 @@ int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
       }
       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)){
+      while((*outy + geom->cdimy - 1) / geom->cdimy > ncplane_dim_y(vopts->n)){
         *outy -= ti->sprixel_scale_height;
         *disppixy = *outy;
       }
@@ -366,11 +381,11 @@ int ncvisual_geom_inner(const tinfo* ti, const ncvisual* n,
       *disppixx -= vopts->pxoffx;
       *disppixy -= vopts->pxoffy;
     }
-    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);
+    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, geom->cdimx);
     geom->rpixy = *disppixy;
     geom->rpixx = *disppixx;
-    geom->rcellx = *outx / ti->cellpixx + !!(*outx % ti->cellpixx);
-    geom->rcelly = *outy / ti->cellpixy + !!(*outy % ti->cellpixy);
+    geom->rcellx = *outx / geom->cdimx + !!(*outx % geom->cdimx);
+    geom->rcelly = *outy / geom->cdimy + !!(*outy % geom->cdimy);
   }else{ // cellblit
     if(vopts->pxoffx || vopts->pxoffy){
       logerror("pixel offsets cannot be used with cell blitting\n");
@@ -1009,8 +1024,11 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   bargs.u.pixel.colorregs = ti->color_registers;
   bargs.u.pixel.pxoffy = pxoffy;
   bargs.u.pixel.pxoffx = pxoffx;
+  bargs.u.pixel.cellpxy = geom->cdimy;
+  bargs.u.pixel.cellpxx = geom->cdimx;
+  const ncpile* p = ncplane_pile_const(n);
   if(n->sprite == NULL){
-    if((n->sprite = sprixel_alloc(&nc->tcache, n, geom->rcelly, geom->rcellx)) == NULL){
+    if((n->sprite = sprixel_alloc(n, geom->rcelly, geom->rcellx)) == NULL){
       return NULL;
     }
     if((n->tam = create_tam(geom->rcelly, geom->rcellx)) == NULL){
@@ -1036,9 +1054,9 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   // 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)) * ti->cellpixx - geom->rpixx) / 2 / ti->cellpixx;
+      placex = (ncplane_dim_x(ncplane_parent_const(n)) * p->cellpxx - geom->rpixx) / 2 / p->cellpxx;
     }else if(placex == NCALIGN_RIGHT){
-      placex = (ncplane_dim_x(ncplane_parent_const(n)) * ti->cellpixx - geom->rpixx) / ti->cellpixx;
+      placex = (ncplane_dim_x(ncplane_parent_const(n)) * p->cellpxx - geom->rpixx) / p->cellpxx;
     }
     if(placex < 0){
       return NULL;
@@ -1046,9 +1064,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)) * ti->cellpixy - geom->rpixy) / 2 / ti->cellpixy;
+      placey = (ncplane_dim_y(ncplane_parent_const(n)) * p->cellpxy - geom->rpixy) / 2 / p->cellpxy;
     }else if(placey == NCALIGN_BOTTOM){
-      placey = (ncplane_dim_y(ncplane_parent_const(n)) * ti->cellpixy - geom->rpixy) / ti->cellpixy;
+      placey = (ncplane_dim_y(ncplane_parent_const(n)) * p->cellpxy - geom->rpixy) / p->cellpxy;
     }
     if(placey < 0){
       return NULL;
diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c
index fe985124ca..acb2133c44 100644
--- a/src/media/ffmpeg.c
+++ b/src/media/ffmpeg.c
@@ -173,7 +173,9 @@ struct ncplane* ffmpeg_subtitle(ncplane* parent, const ncvisual* ncv){
         continue;
       }
       struct notcurses* nc = ncplane_notcurses(parent);
-      if(nc->tcache.cellpixy <= 0 || nc->tcache.cellpixx <= 0){
+      const unsigned cellpxy = ncplane_pile_const(parent)->cellpxy;
+      const unsigned cellpxx = ncplane_pile_const(parent)->cellpxx;
+      if(cellpxy <= 0 || cellpxx <= 0){
         continue;
       }
       struct ncvisual* v = ncvisual_from_palidx(rect->data[0], rect->h,
@@ -182,10 +184,10 @@ struct ncplane* ffmpeg_subtitle(ncplane* parent, const ncvisual* ncv){
       if(v == NULL){
         return NULL;
       }
-      int rows = (rect->h + nc->tcache.cellpixy - 1) / nc->tcache.cellpixy;
+      int rows = (rect->h + cellpxx - 1) / cellpxy;
       struct ncplane_options nopts = {
         .rows = rows,
-        .cols = (rect->w + nc->tcache.cellpixx - 1) / nc->tcache.cellpixx,
+        .cols = (rect->w + cellpxx - 1) / cellpxx,
         .y = ncplane_dim_y(parent) - rows - 1,
         .name = "t1st",
       };


Test patch:
diff --git a/src/tests/bitmap.cpp b/src/tests/bitmap.cpp
index 50cf4eecf1..9f56ea7883 100644
--- a/src/tests/bitmap.cpp
+++ b/src/tests/bitmap.cpp
@@ -14,8 +14,8 @@ TEST_CASE("Bitmaps") {
   }
 
   SUBCASE("SprixelTermValues") {
-    CHECK(0 < nc_->tcache.cellpixy);
-    CHECK(0 < nc_->tcache.cellpixx);
+    CHECK(0 < nc_->tcache.cellpxy);
+    CHECK(0 < nc_->tcache.cellpxx);
     if(!nc_->tcache.pixel_draw_late){
       CHECK(nc_->tcache.pixel_draw);
     }
@@ -66,8 +66,8 @@ TEST_CASE("Bitmaps") {
 
   // a sprixel requires a plane large enough to hold it
   SUBCASE("SprixelTooTall") {
-    auto y = nc_->tcache.cellpixy + 6;
-    auto x = nc_->tcache.cellpixx;
+    auto y = nc_->tcache.cellpxy + 6;
+    auto x = nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xe61c28ff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -89,8 +89,8 @@ TEST_CASE("Bitmaps") {
   }
 
   SUBCASE("SprixelTooWide") {
-    auto y = nc_->tcache.cellpixy;
-    auto x = nc_->tcache.cellpixx + 1;
+    auto y = nc_->tcache.cellpxy;
+    auto x = nc_->tcache.cellpxx + 1;
     std::vector<uint32_t> v(x * y, htole(0xe61c28ff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -114,8 +114,8 @@ TEST_CASE("Bitmaps") {
 
   // should not be able to emit glyphs to a sprixelated plane
   SUBCASE("SprixelNoGlyphs") {
-    auto y = nc_->tcache.cellpixy;
-    auto x = nc_->tcache.cellpixx;
+    auto y = nc_->tcache.cellpxy;
+    auto x = nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xe61c28ff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -139,8 +139,8 @@ TEST_CASE("Bitmaps") {
   }
 
   SUBCASE("BitmapStack") {
-    auto y = nc_->tcache.cellpixy * 10;
-    auto x = nc_->tcache.cellpixx * 10;
+    auto y = nc_->tcache.cellpxy * 10;
+    auto x = nc_->tcache.cellpxx * 10;
     std::vector<uint32_t> v(x * y, htole(0xe61c28ff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -152,8 +152,8 @@ TEST_CASE("Bitmaps") {
     REQUIRE(nullptr != botn);
     // should just have a red plane
     CHECK(0 == notcurses_render(nc_));
-    y = nc_->tcache.cellpixy * 5;
-    x = nc_->tcache.cellpixx * 5;
+    y = nc_->tcache.cellpxy * 5;
+    x = nc_->tcache.cellpxx * 5;
     std::vector<uint32_t> v2(x * y, htole(0x8142f1ff));
     auto ncv2 = ncvisual_from_rgba(v2.data(), y, sizeof(decltype(v2)::value_type) * x, x);
     REQUIRE(nullptr != ncv2);
@@ -198,8 +198,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("BitmapStretch") {
     // first, assemble a visual equivalent to 1 cell
-    auto y = nc_->tcache.cellpixy;
-    auto x = nc_->tcache.cellpixx;
+    auto y = nc_->tcache.cellpxy;
+    auto x = nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -211,8 +211,8 @@ TEST_CASE("Bitmaps") {
     REQUIRE(nullptr != n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
-    CHECK(nc_->tcache.cellpixy == ncv->pixy);
-    CHECK(nc_->tcache.cellpixx == ncv->pixx);
+    CHECK(nc_->tcache.cellpxy == ncv->pixy);
+    CHECK(nc_->tcache.cellpxx == ncv->pixx);
     CHECK(0 == notcurses_render(nc_));
     struct ncplane_options nopts = {
       .y = 1,
@@ -232,8 +232,8 @@ TEST_CASE("Bitmaps") {
     CHECK(vopts.n == ncvisual_blit(nc_, ncv, &vopts));
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncvisual_resize_noninterpolative(ncv, ncv->pixy * 4, ncv->pixx * 4));
-    CHECK(4 * nc_->tcache.cellpixy == ncv->pixy);
-    CHECK(4 * nc_->tcache.cellpixx == ncv->pixx);
+    CHECK(4 * nc_->tcache.cellpxy == ncv->pixy);
+    CHECK(4 * nc_->tcache.cellpxx == ncv->pixx);
     vopts.y = 1;
     vopts.x = 6;
     vopts.n = n_;
@@ -242,7 +242,7 @@ TEST_CASE("Bitmaps") {
     auto infn = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(infn);
     if(nc_->tcache.sprixel_scale_height == 6){
-      if(4 * nc_->tcache.cellpixy % 6){
+      if(4 * nc_->tcache.cellpxy % 6){
         CHECK(5 == ncplane_dim_y(infn));
       }else{
         CHECK(4 == ncplane_dim_y(infn));
@@ -259,8 +259,8 @@ TEST_CASE("Bitmaps") {
     vopts.x = 11;
     auto resizen = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(resizen);
-    CHECK((8 + nc_->tcache.cellpixy - 1) / nc_->tcache.cellpixy == ncplane_dim_y(resizen));
-    CHECK((8 + nc_->tcache.cellpixx - 1) / nc_->tcache.cellpixx == ncplane_dim_x(resizen));
+    CHECK((8 + nc_->tcache.cellpxy - 1) / nc_->tcache.cellpxy == ncplane_dim_y(resizen));
+    CHECK((8 + nc_->tcache.cellpxx - 1) / nc_->tcache.cellpxx == ncplane_dim_x(resizen));
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(bigp));
     CHECK(0 == ncplane_destroy(resizen));
@@ -274,8 +274,8 @@ TEST_CASE("Bitmaps") {
   // resulting geometries for (rough) equality
   SUBCASE("InflateVsScale") {
     // first, assemble a visual equivalent to 1 cell
-    auto y = nc_->tcache.cellpixy;
-    auto x = nc_->tcache.cellpixx;
+    auto y = nc_->tcache.cellpxy;
+    auto x = nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xff7799dd));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -355,8 +355,8 @@ TEST_CASE("Bitmaps") {
       .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
-    auto y = nc_->tcache.cellpixy * 6;
-    auto x = nc_->tcache.cellpixx;
+    auto y = nc_->tcache.cellpxy * 6;
+    auto x = nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -399,8 +399,8 @@ TEST_CASE("Bitmaps") {
       .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
-    auto y = nc_->tcache.cellpixy * 6;
-    auto x = nc_->tcache.cellpixx;
+    auto y = nc_->tcache.cellpxy * 6;
+    auto x = nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -417,8 +417,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("PixelCellWipe") {
     // first, assemble a visual equivalent to 4 cells
-    auto y = 2 * nc_->tcache.cellpixy;
-    auto x = 2 * nc_->tcache.cellpixx;
+    auto y = 2 * nc_->tcache.cellpxy;
+    auto x = 2 * nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -446,8 +446,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("PixelCellWipePolychromatic") {
     // first, assemble a visual equivalent to 4 cells
-    auto y = 2 * nc_->tcache.cellpixy;
-    auto x = 2 * nc_->tcache.cellpixx;
+    auto y = 2 * nc_->tcache.cellpxy;
+    auto x = 2 * nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     for(auto& e : v){
       e -= htole(rand() % 0x1000000);
@@ -478,8 +478,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("PixelBigCellWipePolychromatic") {
     // first, assemble a visual equivalent to 100 cells
-    auto y = 10 * nc_->tcache.cellpixy;
-    auto x = 10 * nc_->tcache.cellpixx;
+    auto y = 10 * nc_->tcache.cellpxy;
+    auto x = 10 * nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     for(auto& e : v){
       e -= htole(rand() % 0x1000000);
@@ -513,14 +513,14 @@ TEST_CASE("Bitmaps") {
     // first, assemble a visual equivalent to 54 cells
     auto dimy = 6;
     auto dimx = 9;
-    auto y = dimy * nc_->tcache.cellpixy;
-    auto x = dimx * nc_->tcache.cellpixx;
+    auto y = dimy * nc_->tcache.cellpxy;
+    auto x = dimx * nc_->tcache.cellpxx;
     std::vector<uint32_t> v(x * y, htole(0xffffffff));
     // every other cell, set some pixels transparent
     for(int i = 0 ; i < dimy * dimx ; ++i){
       if(i % 2){
-        int py = (i / dimx) * nc_->tcache.cellpixy;
-        int px = (i % dimx) * nc_->tcache.cellpixx;
+        int py = (i / dimx) * nc_->tcache.cellpxy;
+        int px = (i % dimx) * nc_->tcache.cellpxx;
         ncpixel_set_a(&v[py * x + px], 0);
       }
     }
@@ -540,8 +540,8 @@ TEST_CASE("Bitmaps") {
     CHECK(s->dimx == dimx);
     const auto tam = n->tam;
     for(unsigned i = 0 ; i < s->dimy * s->dimx ; ++i){
-      int py = (i / dimx) * nc_->tcache.cellpixy;
-      int px = (i % dimx) * nc_->tcache.cellpixx;
+      int py = (i / dimx) * nc_->tcache.cellpxy;
+      int px = (i % dimx) * nc_->tcache.cellpxx;
       // cells with a transparent pixel ought be SPRIXCELL_MIXED;
       // cells without one ought be SPRIXCELL_OPAQUE.
       sprixcell_e state = tam[(i / dimx) + (i % dimx)].state;
@@ -613,8 +613,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("BitmapMoveOffscreenLeft") {
     // first, assemble a visual equivalent to 2x2 cells
-    auto y = nc_->tcache.cellpixy * 2;
-    auto x = nc_->tcache.cellpixx * 2;
+    auto y = nc_->tcache.cellpxy * 2;
+    auto x = nc_->tcache.cellpxx * 2;
     std::vector<uint32_t> v(x * y * 4, htole(0xffccccff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -634,8 +634,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("BitmapMoveOffscreenRight") {
     // first, assemble a visual equivalent to 2x2 cells
-    auto y = nc_->tcache.cellpixy * 2;
-    auto x = nc_->tcache.cellpixx * 2;
+    auto y = nc_->tcache.cellpxy * 2;
+    auto x = nc_->tcache.cellpxx * 2;
     std::vector<uint32_t> v(x * y * 4, htole(0xffccccff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -656,8 +656,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("BitmapMoveOffscreenDown") {
     // first, assemble a visual equivalent to 2x2 cells
-    auto y = nc_->tcache.cellpixy * 2;
-    auto x = nc_->tcache.cellpixx * 2;
+    auto y = nc_->tcache.cellpxy * 2;
+    auto x = nc_->tcache.cellpxx * 2;
     std::vector<uint32_t> v(x * y * 4, htole(0xffccccff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -677,8 +677,8 @@ TEST_CASE("Bitmaps") {
 
   SUBCASE("BitmapMoveOffscreenUp") {
     // first, assemble a visual equivalent to 2x2 cells
-    auto y = nc_->tcache.cellpixy * 2;
-    auto x = nc_->tcache.cellpixx * 2;
+    auto y = nc_->tcache.cellpxy * 2;
+    auto x = nc_->tcache.cellpxx * 2;
     std::vector<uint32_t> v(x * y * 4, htole(0xffccccff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -700,8 +700,8 @@ TEST_CASE("Bitmaps") {
   // smoothly move a bitmap diagonally across the screen
   SUBCASE("BitmapSmoothMove") {
     // first, assemble a visual equivalent to 2x2 cells
-    auto y = nc_->tcache.cellpixy * 2;
-    auto x = nc_->tcache.cellpixx * 2;
+    auto y = nc_->tcache.cellpxy * 2;
+    auto x = nc_->tcache.cellpxx * 2;
     std::vector<uint32_t> v(x * y * 4, htole(0xffccccff));
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
@@ -711,15 +711,15 @@ TEST_CASE("Bitmaps") {
     vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
     auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
-    auto xpx = (ncplane_dim_x(n_) - 2) * nc_->tcache.cellpixx;
-    auto ypx = (ncplane_dim_y(n_) - 2) * nc_->tcache.cellpixy;
+    auto xpx = (ncplane_dim_x(n_) - 2) * nc_->tcache.cellpxx;
+    auto ypx = (ncplane_dim_y(n_) - 2) * nc_->tcache.cellpxy;
     double xyrat = (double)ypx / xpx;
     for(unsigned xat = 0 ; xat < xpx ; ++xat){
-      vopts.x = xat / nc_->tcache.cellpixx;
-      vopts.pxoffx = xat % nc_->tcache.cellpixx;
+      vopts.x = xat / nc_->tcache.cellpxx;
+      vopts.pxoffx = xat % nc_->tcache.cellpxx;
       int yat = xat * xyrat;
-      vopts.y = yat / nc_->tcache.cellpixy;
-      vopts.pxoffy = yat % nc_->tcache.cellpixy;
+      vopts.y = yat / nc_->tcache.cellpxy;
+      vopts.pxoffy = yat % nc_->tcache.cellpxy;
       CHECK(0 == ncplane_destroy(n));
       n = ncvisual_blit(nc_, ncv, &vopts);
       REQUIRE(nullptr != n);
diff --git a/src/tests/visual.cpp b/src/tests/visual.cpp
index 2bc1d579a0..26907d6bb3 100644
--- a/src/tests/visual.cpp
+++ b/src/tests/visual.cpp
@@ -7,8 +7,8 @@
 void default_visual_extrinsics(const notcurses* nc, const ncvgeom& g) {
   CHECK(0 == g.pixy);
   CHECK(0 == g.pixx);
-  CHECK(nc->tcache.cellpixy == g.cdimy);
-  CHECK(nc->tcache.cellpixx == g.cdimx);
+  CHECK(nc->tcache.cellpxy == g.cdimy);
+  CHECK(nc->tcache.cellpxx == g.cdimx);
   CHECK(1 <= g.scaley);
   CHECK(1 <= g.scalex);
   CHECK(0 == g.rpixy);
@@ -91,8 +91,8 @@ TEST_CASE("Visual") {
     CHECK(0 == ncvisual_geom(nc_, nullptr, &vopts, &g));
     CHECK(0 == g.pixy);
     CHECK(0 == g.pixx);
-    CHECK(nc_->tcache.cellpixy == g.cdimy);
-    CHECK(nc_->tcache.cellpixx == g.cdimx);
+    CHECK(nc_->tcache.cellpxy == g.cdimy);
+    CHECK(nc_->tcache.cellpxx == g.cdimx);
     if(notcurses_canpixel(nc_)){
       CHECK(g.cdimy == g.scaley);
       CHECK(g.cdimx == g.scalex);