Repository: dankamongmen/notcurses (language: python)

Issue:
smooth bitmap movement with kitty graphics
Currently, bitmaps can move only a cell at a time. That leads to jerky movement. With Kitty, we can do subcell placement. With sixel, we can fake it by adding transparent rows/columns. Consider doing so.

Actually, this could apply to all blitters. Quadblitter could move in half-cell increments by adding a transparent row or column *of source pixels*. hrmmmmmmmmmmmM!
smooth bitmap movement with kitty graphics
Currently, bitmaps can move only a cell at a time. That leads to jerky movement. With Kitty, we can do subcell placement. With sixel, we can fake it by adding transparent rows/columns. Consider doing so.

Actually, this could apply to all blitters. Quadblitter could move in half-cell increments by adding a transparent row or column *of source pixels*. hrmmmmmmmmmmmM!
smooth bitmap movement with kitty graphics
Currently, bitmaps can move only a cell at a time. That leads to jerky movement. With Kitty, we can do subcell placement. With sixel, we can fake it by adding transparent rows/columns. Consider doing so.

Actually, this could apply to all blitters. Quadblitter could move in half-cell increments by adding a transparent row or column *of source pixels*. hrmmmmmmmmmmmM!
smooth bitmap movement with kitty graphics
Currently, bitmaps can move only a cell at a time. That leads to jerky movement. With Kitty, we can do subcell placement. With sixel, we can fake it by adding transparent rows/columns. Consider doing so.

Actually, this could apply to all blitters. Quadblitter could move in half-cell increments by adding a transparent row or column *of source pixels*. hrmmmmmmmmmmmM!
smooth bitmap movement with kitty graphics
Currently, bitmaps can move only a cell at a time. That leads to jerky movement. With Kitty, we can do subcell placement. With sixel, we can fake it by adding transparent rows/columns. Consider doing so.

Actually, this could apply to all blitters. Quadblitter could move in half-cell increments by adding a transparent row or column *of source pixels*. hrmmmmmmmmmmmM!


Discussion / hints:
note that this isn't just relevant to movement of planes, but also for centering. `notcurses-info` currently looks like this (see below). i can't get the logo centered, because it's 60 pixels high, on 4x20 pixel high cells. we'd thus want to be halfway into the first row, but that's unavailable, and we're stuck with either the first or second row, 10 pixels away from where we want to be.

![2021-06-30-180436_800x1420_scrot](https://user-images.githubusercontent.com/143473/124037474-a3872c00-d9cd-11eb-9447-14dcc08f2690.png)

yeah we ought definitely do this. i think the API is basically "expose pixel coordinates for `ncvisual_render()`, and get as close to it as possible". with kitty bitmaps, we can get directly there with no further help. with sixel, we'd need insert the transparencies. with cells, we'd always have to do it manually.
i think it might be better, as mentioned in #2230, to add two new fields to `ncvisual_options`, `pixoffy` and `pixoffx`.
or maybe we'd like a new function, `ncvisual_render_bitmap()` which is restricted to `NCBLIT_PIXEL` and uses absolute x,y pixel coordinates...
hrmmm. let's go ahead and add the fields to `ncvisual_options` (not implemented for now) for #1777's sake. we'll only use it with cells when the offsets are 0 or perhaps half. if we want an absolute positioning function later, we can add one.
so we've got these fields added to `ncvisual_options`. let's try using them. Kitty support will be trivial, as it allows us to directly specify a cell pixel offset. Sixel will be tougher. Let's create a distinct bug for it, and limit this one to kitty graphics.
so things this will require:

* if offsetx is set, our cellwidth goes up by 1
* if offsety is set, our cellheight goes up by 1
  * these will be reflected in the `sprixel` cell geometry and the TAM size
 * if offsetx >= cellpixx, error
 * if offsety >= cellpixy, error
   * maybe not? remember that a font size change could invalidate these conditionals later
* if offsetx and/or offsety, but blitter is not `NCBLIT_PIXEL`, error
* pass offsetx/offsety down through `blitterargs` pixel union
* in kitty, when preparing TAM, account for offsets
* in kitty, actual encoded data doesn't change at all, good
* in kitty, add `X` and `Y` params in display (`a=d`) command
note that this isn't just relevant to movement of planes, but also for centering. `notcurses-info` currently looks like this (see below). i can't get the logo centered, because it's 60 pixels high, on 4x20 pixel high cells. we'd thus want to be halfway into the first row, but that's unavailable, and we're stuck with either the first or second row, 10 pixels away from where we want to be.

![2021-06-30-180436_800x1420_scrot](https://user-images.githubusercontent.com/143473/124037474-a3872c00-d9cd-11eb-9447-14dcc08f2690.png)

yeah we ought definitely do this. i think the API is basically "expose pixel coordinates for `ncvisual_render()`, and get as close to it as possible". with kitty bitmaps, we can get directly there with no further help. with sixel, we'd need insert the transparencies. with cells, we'd always have to do it manually.
i think it might be better, as mentioned in #2230, to add two new fields to `ncvisual_options`, `pixoffy` and `pixoffx`.
or maybe we'd like a new function, `ncvisual_render_bitmap()` which is restricted to `NCBLIT_PIXEL` and uses absolute x,y pixel coordinates...
hrmmm. let's go ahead and add the fields to `ncvisual_options` (not implemented for now) for #1777's sake. we'll only use it with cells when the offsets are 0 or perhaps half. if we want an absolute positioning function later, we can add one.
so we've got these fields added to `ncvisual_options`. let's try using them. Kitty support will be trivial, as it allows us to directly specify a cell pixel offset. Sixel will be tougher. Let's create a distinct bug for it, and limit this one to kitty graphics.
so things this will require:

* if offsetx is set, our cellwidth goes up by 1
* if offsety is set, our cellheight goes up by 1
  * these will be reflected in the `sprixel` cell geometry and the TAM size
 * if offsetx >= cellpixx, error
 * if offsety >= cellpixy, error
   * maybe not? remember that a font size change could invalidate these conditionals later
* if offsetx and/or offsety, but blitter is not `NCBLIT_PIXEL`, error
* pass offsetx/offsety down through `blitterargs` pixel union
* in kitty, when preparing TAM, account for offsets
* in kitty, actual encoded data doesn't change at all, good
* in kitty, add `X` and `Y` params in display (`a=d`) command
note that this isn't just relevant to movement of planes, but also for centering. `notcurses-info` currently looks like this (see below). i can't get the logo centered, because it's 60 pixels high, on 4x20 pixel high cells. we'd thus want to be halfway into the first row, but that's unavailable, and we're stuck with either the first or second row, 10 pixels away from where we want to be.

![2021-06-30-180436_800x1420_scrot](https://user-images.githubusercontent.com/143473/124037474-a3872c00-d9cd-11eb-9447-14dcc08f2690.png)

yeah we ought definitely do this. i think the API is basically "expose pixel coordinates for `ncvisual_render()`, and get as close to it as possible". with kitty bitmaps, we can get directly there with no further help. with sixel, we'd need insert the transparencies. with cells, we'd always have to do it manually.
i think it might be better, as mentioned in #2230, to add two new fields to `ncvisual_options`, `pixoffy` and `pixoffx`.
or maybe we'd like a new function, `ncvisual_render_bitmap()` which is restricted to `NCBLIT_PIXEL` and uses absolute x,y pixel coordinates...
hrmmm. let's go ahead and add the fields to `ncvisual_options` (not implemented for now) for #1777's sake. we'll only use it with cells when the offsets are 0 or perhaps half. if we want an absolute positioning function later, we can add one.
so we've got these fields added to `ncvisual_options`. let's try using them. Kitty support will be trivial, as it allows us to directly specify a cell pixel offset. Sixel will be tougher. Let's create a distinct bug for it, and limit this one to kitty graphics.
so things this will require:

* if offsetx is set, our cellwidth goes up by 1
* if offsety is set, our cellheight goes up by 1
  * these will be reflected in the `sprixel` cell geometry and the TAM size
 * if offsetx >= cellpixx, error
 * if offsety >= cellpixy, error
   * maybe not? remember that a font size change could invalidate these conditionals later
* if offsetx and/or offsety, but blitter is not `NCBLIT_PIXEL`, error
* pass offsetx/offsety down through `blitterargs` pixel union
* in kitty, when preparing TAM, account for offsets
* in kitty, actual encoded data doesn't change at all, good
* in kitty, add `X` and `Y` params in display (`a=d`) command
note that this isn't just relevant to movement of planes, but also for centering. `notcurses-info` currently looks like this (see below). i can't get the logo centered, because it's 60 pixels high, on 4x20 pixel high cells. we'd thus want to be halfway into the first row, but that's unavailable, and we're stuck with either the first or second row, 10 pixels away from where we want to be.

![2021-06-30-180436_800x1420_scrot](https://user-images.githubusercontent.com/143473/124037474-a3872c00-d9cd-11eb-9447-14dcc08f2690.png)

yeah we ought definitely do this. i think the API is basically "expose pixel coordinates for `ncvisual_render()`, and get as close to it as possible". with kitty bitmaps, we can get directly there with no further help. with sixel, we'd need insert the transparencies. with cells, we'd always have to do it manually.
i think it might be better, as mentioned in #2230, to add two new fields to `ncvisual_options`, `pixoffy` and `pixoffx`.
or maybe we'd like a new function, `ncvisual_render_bitmap()` which is restricted to `NCBLIT_PIXEL` and uses absolute x,y pixel coordinates...
hrmmm. let's go ahead and add the fields to `ncvisual_options` (not implemented for now) for #1777's sake. we'll only use it with cells when the offsets are 0 or perhaps half. if we want an absolute positioning function later, we can add one.
so we've got these fields added to `ncvisual_options`. let's try using them. Kitty support will be trivial, as it allows us to directly specify a cell pixel offset. Sixel will be tougher. Let's create a distinct bug for it, and limit this one to kitty graphics.
so things this will require:

* if offsetx is set, our cellwidth goes up by 1
* if offsety is set, our cellheight goes up by 1
  * these will be reflected in the `sprixel` cell geometry and the TAM size
 * if offsetx >= cellpixx, error
 * if offsety >= cellpixy, error
   * maybe not? remember that a font size change could invalidate these conditionals later
* if offsetx and/or offsety, but blitter is not `NCBLIT_PIXEL`, error
* pass offsetx/offsety down through `blitterargs` pixel union
* in kitty, when preparing TAM, account for offsets
* in kitty, actual encoded data doesn't change at all, good
* in kitty, add `X` and `Y` params in display (`a=d`) command
note that this isn't just relevant to movement of planes, but also for centering. `notcurses-info` currently looks like this (see below). i can't get the logo centered, because it's 60 pixels high, on 4x20 pixel high cells. we'd thus want to be halfway into the first row, but that's unavailable, and we're stuck with either the first or second row, 10 pixels away from where we want to be.

![2021-06-30-180436_800x1420_scrot](https://user-images.githubusercontent.com/143473/124037474-a3872c00-d9cd-11eb-9447-14dcc08f2690.png)

yeah we ought definitely do this. i think the API is basically "expose pixel coordinates for `ncvisual_render()`, and get as close to it as possible". with kitty bitmaps, we can get directly there with no further help. with sixel, we'd need insert the transparencies. with cells, we'd always have to do it manually.
i think it might be better, as mentioned in #2230, to add two new fields to `ncvisual_options`, `pixoffy` and `pixoffx`.
or maybe we'd like a new function, `ncvisual_render_bitmap()` which is restricted to `NCBLIT_PIXEL` and uses absolute x,y pixel coordinates...
hrmmm. let's go ahead and add the fields to `ncvisual_options` (not implemented for now) for #1777's sake. we'll only use it with cells when the offsets are 0 or perhaps half. if we want an absolute positioning function later, we can add one.
so we've got these fields added to `ncvisual_options`. let's try using them. Kitty support will be trivial, as it allows us to directly specify a cell pixel offset. Sixel will be tougher. Let's create a distinct bug for it, and limit this one to kitty graphics.
so things this will require:

* if offsetx is set, our cellwidth goes up by 1
* if offsety is set, our cellheight goes up by 1
  * these will be reflected in the `sprixel` cell geometry and the TAM size
 * if offsetx >= cellpixx, error
 * if offsety >= cellpixy, error
   * maybe not? remember that a font size change could invalidate these conditionals later
* if offsetx and/or offsety, but blitter is not `NCBLIT_PIXEL`, error
* pass offsetx/offsety down through `blitterargs` pixel union
* in kitty, when preparing TAM, account for offsets
* in kitty, actual encoded data doesn't change at all, good
* in kitty, add `X` and `Y` params in display (`a=d`) command

Fix patch:
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 273a0bb97f..ae4411ade2 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -2850,7 +2850,6 @@ struct ncvisual_options {
   // an error if either number exceeds the cell-pixel geometry in its
   // dimension. if NCBLIT_PIXEL is not used, these fields are ignored.
   // this functionality can be used for smooth bitmap movement.
-  // FIXME not yet implemented.
   unsigned pxoffy, pxoffx;
 };
 
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 65d55e28af..f303a9b32a 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -659,8 +659,8 @@ ncdirect_render_visual(ncdirect* n, ncvisual* ncv,
   }
   int ymax = vopts->leny / bset->height;
   int xmax = vopts->lenx / bset->width;
-  int dimy = vopts->leny > 0 ? ymax : ncdirect_dim_y(n);
-  int dimx = vopts->lenx > 0 ? xmax : ncdirect_dim_x(n);
+  unsigned dimy = vopts->leny > 0 ? ymax : ncdirect_dim_y(n);
+  unsigned dimx = vopts->lenx > 0 ? xmax : ncdirect_dim_x(n);
   int disprows, dispcols, outy;
   if(vopts->scaling != NCSCALE_NONE && vopts->scaling != NCSCALE_NONE_HIRES){
     if(bset->geom != NCBLIT_PIXEL){
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 831e9ccc5f..f65ccfd29b 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -401,6 +401,8 @@ typedef struct blitterargs {
     struct {
       int colorregs;   // number of color registers
       sprixel* spx;    // sprixel object
+      int pxoffy;      // pixel y-offset within origin cell
+      int pxoffx;      // pixel x-offset within origin cell
     } pixel;           // for pixels
   } u;
 } blitterargs;
@@ -414,8 +416,8 @@ typedef int (*ncblitter)(struct ncplane* n, int linesize, const void* data,
 // a system for rendering RGBA pixels as text glyphs or sixel/kitty bitmaps
 struct blitset {
   ncblitter_e geom;
-  int width;        // number of input pixels per output cell, width
-  int height;       // number of input pixels per output cell, height
+  unsigned width;   // number of input pixels per output cell, width
+  unsigned height;  // number of input pixels per output cell, height
   // the EGCs which form the blitter. bits grow left to right, and then top to
   // bottom. the first character is always a space, the last a full block.
   const wchar_t* egcs;
diff --git a/src/lib/kitty.c b/src/lib/kitty.c
index a3f016973f..cd76f6b7a0 100644
--- a/src/lib/kitty.c
+++ b/src/lib/kitty.c
@@ -533,7 +533,16 @@ int kitty_wipe(sprixel* s, int ycell, int xcell){
 
 int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll){
   loginfo("Committing Kitty graphic id %u\n", s->id);
-  fbuf_printf(f, "\e_Ga=p,i=%u,p=1,q=2%s\e\\", s->id, noscroll ? ",C=1" : "");
+  int i;
+  if(s->pxoffx || s->pxoffy){
+    i = fbuf_printf(f, "\e_Ga=p,i=%u,p=1,X=%u,Y=%u%s\e\\", s->id,
+                    s->pxoffx, s->pxoffy, noscroll ? ",C=1" : "");
+  }else{
+    i = fbuf_printf(f, "\e_Ga=p,i=%u,p=1,q=2%s\e\\", s->id, noscroll ? ",C=1" : "");
+  }
+  if(i < 0){
+    return -1;
+  }
   s->invalidated = SPRIXEL_QUIESCENT;
   return 0;
 }
@@ -730,9 +739,8 @@ static int
 write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
                  const uint32_t* data, const blitterargs* bargs,
                  tament* tam, int* parse_start, ncpixelimpl_e level){
-//fprintf(stderr, "drawing kitty %p\n", tam);
   if(linesize % sizeof(*data)){
-    logerror("Stride (%d) badly aligned\n", linesize);
+    logerror("stride (%d) badly aligned\n", linesize);
     return -1;
   }
   // we only deflate if we're using animation, since otherwise we need be able
@@ -805,7 +813,6 @@ write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
         if(translucent){
           ncpixel_set_a(&source[e], ncpixel_a(source[e]) / 2);
         }
-//fprintf(stderr, "%u/%u/%u -> %c%c%c%c %u %u %u %u\n", r, g, b, b64[0], b64[1], b64[2], b64[3], b64[0], b64[1], b64[2], b64[3]);
         int xcell = x / cdimx;
         int ycell = y / cdimy;
         int tyx = xcell + ycell * cols;
@@ -820,6 +827,7 @@ write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
                                     data, linesize, tam[tyx].auxvector,
                                     transcolor);
             if(tmp == NULL){
+              logerror("got a NULL auxvec at %d/%d\n", y, x);
               goto err;
             }
             tam[tyx].auxvector = tmp;
@@ -827,6 +835,7 @@ write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
             if(tam[tyx].auxvector == NULL){
               tam[tyx].auxvector = malloc(sizeof(tam[tyx].state));
               if(tam[tyx].auxvector == NULL){
+                logerror("got a NULL auxvec at %d\n", tyx);
                 goto err;
               }
             }
@@ -1052,7 +1061,6 @@ int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
 static inline int
 kitty_blit_core(ncplane* n, int linesize, const void* data, int leny, int lenx,
                 const blitterargs* bargs, ncpixelimpl_e level){
-//fprintf(stderr, "IMAGE: start %p end %p\n", data, (const char*)data + leny * linesize);
   int cols = bargs->u.pixel.spx->dimx;
   sprixel* s = bargs->u.pixel.spx;
   if(init_sprixel_animation(s)){
@@ -1060,17 +1068,23 @@ kitty_blit_core(ncplane* n, int linesize, const void* data, int leny, int lenx,
   }
   int parse_start = 0;
   fbuf* f = &s->glyph;
+  int pxoffx = bargs->u.pixel.pxoffx;
+  int pxoffy = bargs->u.pixel.pxoffy;
   if(write_kitty_data(f, linesize, leny, lenx, cols, data,
                       bargs, n->tam, &parse_start, level)){
     goto error;
   }
+  // FIXME need set pxoffx and pxoffy in sprixel
   if(level == NCPIXEL_KITTY_STATIC){
     s->animating = false;
   }
   // take ownership of |buf| and |tam| on success.
-  if(plane_blit_sixel(s, &s->glyph, leny, lenx, parse_start, n->tam, SPRIXEL_UNSEEN) < 0){
+  if(plane_blit_sixel(s, &s->glyph, leny + pxoffy, lenx + pxoffx, parse_start,
+                      n->tam, SPRIXEL_UNSEEN) < 0){
     goto error;
   }
+  s->pxoffx = pxoffx;
+  s->pxoffy = pxoffy;
   return 1;
 
 error:
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index b195cecace..3dddd6a9d3 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -2900,8 +2900,8 @@ ncplane_as_rgba_internal(const ncplane* nc, ncblitter_e blit,
         ncchannels_bg_rgb8(channels, &br, &bb, &bg);
         ba = ncchannels_bg_alpha(channels);
         // handle each destination pixel from this cell
-        for(int py = 0 ; py < bset->height ; ++py){
-          for(int px = 0 ; px < bset->width ; ++px){
+        for(unsigned py = 0 ; py < bset->height ; ++py){
+          for(unsigned px = 0 ; px < bset->width ; ++px){
             uint32_t* p = &ret[(targy + py) * (lenx * bset->width) + (targx + px)];
             bool background = is_bg_p(idx, py, px, bset->width);
             if(background){
diff --git a/src/lib/plot.c b/src/lib/plot.c
index 6e0579fb32..f52981fcf1 100644
--- a/src/lib/plot.c
+++ b/src/lib/plot.c
@@ -458,8 +458,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 int scaleddim = dimx * (bset->geom == NCBLIT_PIXEL ? ncplane_notcurses(n)->tcache.cellpixx : bset->width); \
-  const int scaledprefixlen = PREFIXCOLUMNS * (bset->geom == NCBLIT_PIXEL ? ncplane_notcurses(n)->tcache.cellpixx : bset->width); \
+  const unsigned scaleddim = dimx * (bset->geom == NCBLIT_PIXEL ? ncplane_notcurses(n)->tcache.cellpixx : bset->width); \
+  const unsigned scaledprefixlen = PREFIXCOLUMNS * (bset->geom == NCBLIT_PIXEL ? ncplane_notcurses(n)->tcache.cellpixx : bset->width); \
   if((ncpp->plot.slotcount = ncpp->plot.rangex) == 0){ \
     ncpp->plot.slotcount = scaleddim; \
   } \
diff --git a/src/lib/sprite.c b/src/lib/sprite.c
index d923c20169..57ecd7c111 100644
--- a/src/lib/sprite.c
+++ b/src/lib/sprite.c
@@ -158,9 +158,11 @@ int sprixel_load(sprixel* spx, fbuf* f, int pixy, int pixx,
   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;
     }
   }
diff --git a/src/lib/sprite.h b/src/lib/sprite.h
index 1a604af7d8..598e7d9995 100644
--- a/src/lib/sprite.h
+++ b/src/lib/sprite.h
@@ -150,6 +150,7 @@ typedef struct sprixel {
   int movedfromx;       // so that we can damage old cells when redrawn
   // only used for kitty-based sprixels
   int parse_start;      // where to start parsing for cell wipes
+  int pxoffy, pxoffx;   // X and Y parameters to display command
   // only used for sixel-based sprixels
   unsigned char* needs_refresh; // one per cell, whether new frame needs damage
   struct sixelmap* smap;  // copy of palette indices + transparency bits
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index e0d28551ff..239767bfee 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -105,8 +105,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.
-  int cellpixy;                    // cell pixel height, might be 0
-  int cellpixx;                    // cell pixel width, might be 0
+  unsigned cellpixy;               // cell pixel height, might be 0
+  unsigned cellpixx;               // cell pixel width, might be 0
   int dimy, dimx;                  // most recent cell geometry
 
   unsigned supported_styles; // bitmask over NCSTYLE_* driven via sgr/ncv
diff --git a/src/lib/visual.c b/src/lib/visual.c
index 04f1dea3ca..337d0e5b3f 100644
--- a/src/lib/visual.c
+++ b/src/lib/visual.c
@@ -195,34 +195,53 @@ int ncvisual_blitset_geom(const notcurses* nc, const tinfo* tcache,
   if(blitter){
     *blitter = bset;
   }
-  if(bset->geom == NCBLIT_PIXEL && vopts){
-    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){
+    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);
-        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;
-      }
-      // FIXME clamp to sprixel limits
-      if(vopts->scaling == NCSCALE_NONE || vopts->scaling == NCSCALE_NONE_HIRES){
-        int rows = (*leny + nc->tcache.cellpixy - 1) / nc->tcache.cellpixy;
-        if(rows > ncplane_dim_y(vopts->n)){
-          logerror("Sprixel too tall %d for plane %d\n", *leny, ncplane_dim_y(vopts->n) * nc->tcache.cellpixy);
+        if(vopts->y && !(vopts->flags & (NCVISUAL_OPTION_VERALIGNED | NCVISUAL_OPTION_CHILDPLANE))){
+          logerror("Non-origin y placement %d for sprixel\n", vopts->y);
           return -1;
         }
-        int cols = (*lenx + nc->tcache.cellpixx - 1) / nc->tcache.cellpixx;
-        if(cols > ncplane_dim_x(vopts->n)){
-          logerror("Sprixel too wide %d for plane %d\n", *lenx, ncplane_dim_x(vopts->n) * nc->tcache.cellpixx);
+        if(vopts->x && !(vopts->flags & (NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_CHILDPLANE))){
+          logerror("Non-origin x placement %d for sprixel\n", vopts->x);
           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;
+          }
+        }
+      }
+    }else{
+      if(vopts->pxoffx || vopts->pxoffy){
+        logerror("pixel offsets cannot be used with cell blitting\n");
+        return -1;
       }
     }
   }
@@ -877,12 +896,18 @@ ncplane* ncvisual_render_cells(notcurses* nc, ncvisual* ncv, const struct blitse
 
 // 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| is
-// output height on output, and unused on input.
+// 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* placey, int* placex){
+                   uint64_t flags, int* outy, int* outx,
+                   int* placey, int* placex, int pxoffy, int pxoffx){
   if(scaling != NCSCALE_NONE && scaling != NCSCALE_NONE_HIRES){
     ncplane_dim_yx(parent, disppixy, disppixx);
     // FIXME why do we clamp only vertical, not horizontal, here?
@@ -897,14 +922,18 @@ make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
     }
     *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
       clamp_to_sixelmax(&nc->tcache, disppixy, disppixx, outy, scaling);
     }
   }else{
-    *disppixx = ncv->pixx;
-    *disppixy = ncv->pixy;
+    *disppixx = ncv->pixx + pxoffx;
+    *disppixy = ncv->pixy + pxoffy;
+    *outx = *disppixx;
     clamp_to_sixelmax(&nc->tcache, disppixy, disppixx, outy, scaling);
   }
   struct ncplane_options nopts = {
@@ -924,6 +953,10 @@ make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
   if(n == NULL){
     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;
@@ -950,20 +983,22 @@ make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
 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){
+                                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");
+    logerror("won't blit bitmaps to the standard plane\n");
     return NULL;
   }
-  int disppixy = 0, disppixx = 0, outy = 0;
+  int disppixy = 0, disppixx = 0, outy = 0, outx = 0;
   ncplane* createdn = NULL;
   if(n == NULL || (flags & NCVISUAL_OPTION_CHILDPLANE)){ // create plane
     if(n == NULL){
       n = notcurses_stdplane(nc);
     }
     if((createdn = make_sprixel_plane(nc, n, ncv, scaling, &disppixy, &disppixx,
-                                      flags, &outy, &placey, &placex)) == NULL){
+                                      flags, &outy, &outx, &placey, &placex,
+                                      pxoffy, pxoffx)) == NULL){
       return NULL;
     }
     n = createdn;
@@ -971,7 +1006,9 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
     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)){
@@ -983,8 +1020,8 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
       disppixx -= absplacex * nc->tcache.cellpixx;
       disppixy -= absplacey * nc->tcache.cellpixy;
     }else{
-      disppixx = lenx;
-      disppixy = leny;
+      disppixx = lenx + pxoffx;
+      disppixy = leny + pxoffy;
     }
     if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
       clamp_to_sixelmax(&nc->tcache, &disppixy, &disppixx, &outy, scaling);
@@ -992,12 +1029,15 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
     }
     clamp_to_sixelmax(&nc->tcache, &disppixy, &disppixx, &outy, scaling);
     // FIXME use a closed form
-    while((outy + nc->tcache.cellpixy - 1) / nc->tcache.cellpixy > ncplane_dim_y(n)){
+    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;
   }
-//fprintf(stderr, "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: %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);
   blitterargs bargs;
   if(flags & NCVISUAL_OPTION_ADDALPHA){
     bargs.transcolor = transcolor | 0x1000000ul;
@@ -1010,7 +1050,9 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   bargs.lenx = lenx;
   bargs.flags = flags;
   bargs.u.pixel.colorregs = nc->tcache.color_registers;
-  int cols = disppixx / nc->tcache.cellpixx + !!(disppixx % nc->tcache.cellpixx);
+  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);
   if(n->sprite == NULL){
     if((n->sprite = sprixel_alloc(&nc->tcache, n, rows, cols)) == NULL){
@@ -1108,7 +1150,9 @@ ncplane* ncvisual_render(notcurses* nc, ncvisual* ncv, const struct ncvisual_opt
   }else{
     n = ncvisual_render_pixels(nc, ncv, bset, placey, placex, begy, begx,
                                leny, lenx, n, scaling,
-                               vopts ? vopts->flags : 0, transcolor);
+                               vopts ? vopts->flags : 0, transcolor,
+                               vopts ? vopts->pxoffy : 0,
+                               vopts ? vopts->pxoffx : 0);
   }
   return n;
 }


Test patch:
diff --git a/src/tests/bitmap.cpp b/src/tests/bitmap.cpp
index ca0758b8dd..fd09fa61a0 100644
--- a/src/tests/bitmap.cpp
+++ b/src/tests/bitmap.cpp
@@ -595,11 +595,13 @@ TEST_CASE("Bitmaps") {
     vopts.blitter = NCBLIT_PIXEL;
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
     auto n = ncvisual_render(nc_, ncv, &vopts);
+    REQUIRE(nullptr != n);
     for(int i = 0 ; i <= ncplane_dim_x(n_) ; ++i){
       CHECK(0 == ncplane_move_yx(n, 0, i));
       CHECK(0 == notcurses_render(nc_));
     }
-    REQUIRE(nullptr != n);
+    ncvisual_destroy(ncv);
+    CHECK(0 == ncplane_destroy(n));
   }
 
   SUBCASE("BitmapMoveOffscreenRight") {
@@ -614,11 +616,13 @@ TEST_CASE("Bitmaps") {
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
     vopts.x = ncplane_dim_x(n_) - 3;
     auto n = ncvisual_render(nc_, ncv, &vopts);
+    REQUIRE(nullptr != n);
     for(int i = ncplane_dim_x(n_) - 3 ; i >= 0 ; --i){
       CHECK(0 == ncplane_move_yx(n, 0, i));
       CHECK(0 == notcurses_render(nc_));
     }
-    REQUIRE(nullptr != n);
+    ncvisual_destroy(ncv);
+    CHECK(0 == ncplane_destroy(n));
   }
 
   SUBCASE("BitmapMoveOffscreenDown") {
@@ -632,11 +636,13 @@ TEST_CASE("Bitmaps") {
     vopts.blitter = NCBLIT_PIXEL;
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
     auto n = ncvisual_render(nc_, ncv, &vopts);
+    REQUIRE(nullptr != n);
     for(int i = 0 ; i <= ncplane_dim_y(n_) ; ++i){
       CHECK(0 == ncplane_move_yx(n, i, 0));
       CHECK(0 == notcurses_render(nc_));
     }
-    REQUIRE(nullptr != n);
+    ncvisual_destroy(ncv);
+    CHECK(0 == ncplane_destroy(n));
   }
 
   SUBCASE("BitmapMoveOffscreenUp") {
@@ -651,11 +657,45 @@ TEST_CASE("Bitmaps") {
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
     vopts.y = ncplane_dim_y(n_) - 3;
     auto n = ncvisual_render(nc_, ncv, &vopts);
+    REQUIRE(nullptr != n);
     for(int i = ncplane_dim_y(n_) - 3 ; i >= 0 ; --i){
       CHECK(0 == ncplane_move_yx(n, i, 0));
       CHECK(0 == notcurses_render(nc_));
     }
+    ncvisual_destroy(ncv);
+    CHECK(0 == ncplane_destroy(n));
+  }
+
+  // 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;
+    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);
+    struct ncvisual_options vopts{};
+    vopts.blitter = NCBLIT_PIXEL;
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
+    auto n = ncvisual_render(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;
+    double xyrat = (double)ypx / xpx;
+    for(unsigned xat = 0 ; xat < xpx ; ++xat){
+      vopts.x = xat / nc_->tcache.cellpixx;
+      vopts.pxoffx = xat % nc_->tcache.cellpixx;
+      int yat = xat * xyrat;
+      vopts.y = yat / nc_->tcache.cellpixy;
+      vopts.pxoffy = yat % nc_->tcache.cellpixy;
+      CHECK(0 == ncplane_destroy(n));
+      n = ncvisual_render(nc_, ncv, &vopts);
+      REQUIRE(nullptr != n);
+      CHECK(0 == notcurses_render(nc_));
+    }
+    ncvisual_destroy(ncv);
+    CHECK(0 == ncplane_destroy(n));
+    CHECK(0 == notcurses_render(nc_));
   }
 
   CHECK(!notcurses_stop(nc_));