Repository: dankamongmen/notcurses (language: python)

Issue:
track a per-pile absx/absy
Currently, any margins are accessible only via checking the `absy`/`absx` params in the standard plane. These ought be properties of piles, not planes, IMHO. Even if the margins ought be the same across all piles, reaching into the standard plane from another pile during rendering/rasterization is a thread safety issue.
track a per-pile absx/absy
Currently, any margins are accessible only via checking the `absy`/`absx` params in the standard plane. These ought be properties of piles, not planes, IMHO. Even if the margins ought be the same across all piles, reaching into the standard plane from another pile during rendering/rasterization is a thread safety issue.
track a per-pile absx/absy
Currently, any margins are accessible only via checking the `absy`/`absx` params in the standard plane. These ought be properties of piles, not planes, IMHO. Even if the margins ought be the same across all piles, reaching into the standard plane from another pile during rendering/rasterization is a thread safety issue.
track a per-pile absx/absy
Currently, any margins are accessible only via checking the `absy`/`absx` params in the standard plane. These ought be properties of piles, not planes, IMHO. Even if the margins ought be the same across all piles, reaching into the standard plane from another pile during rendering/rasterization is a thread safety issue.


Discussion / hints:
we also ought make the `tinfo` our source of truth of the most recently `ioctl()`-reported cell geometry, as discussed in #1525 
wouldn't we just duplicate the margins into each pile? margins are a property of the *context*, **not** of piles.

so really they belong in `termdesc`. that eliminates the thread safety issue, while keeping them in one place.

`absy`/`absx` then remain properties of planes, as they ought, interpreted relative to the margins of `termdesc`. there's no reason to have any of this in the pile structure.
BTW I'm not sure this is 100% related but it made me remember the following sentence from: https://notcurses.com/notcurses_visual.3.html#options ?

>"If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"

shouldn't it say relative to the terminal dimensions or termsize?
> > "If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"
> shouldn't it say relative to the terminal dimensions or termsize?

they're the same thing, no? the standard plane is guaranteed to be the same size as the visual area (terminal size minus margins).
the standard plane does have the size of the terminal, but when you use that term, it seems too specific, and if you are working with other piles without touching the standard plane it can be confusing to refer to the standard plane as a reference...

I'm finding these things while I'm working on the higher level rust wrappers where I'm not using the concept of the standard plane, and I'm trying to simplify and clarify the concepts...
sure, if that seems clearer i'll make that change
That was a source of confusion for me sometimes. In general I think is clearer to refer to the desired properties directly instead to an object that encarnate those properties, because the object can be mistaked for the needed thing, not the properties. And if the object is an example, clarify it as such.
so the margin information is already stored in the context. it's then reduced to the `absy`/`absx`/`row`/`cols` of the standard plane. we want the standard plane to accurately reflect the visual area's geometry. other root planes may or may not, but ought respect the margins. but margins are only relevant to rendering (and resizing of the standard plane) -- everywhere else, we have to work around them.

so i say we *stop putting the margins in the standard plane's `abs{yx}`*. rendering will get them out of the `struct notcurses*`, as will resize. they'll be 0 for the standard plane at all times, simplifying numerous functions.
oh wow, i think `notcurses_resize_internal()` is super broken when we're rendering anything but the standard pile. it is always called on the standard plane from `notcurses_resize()`:

```c
// Check for a window resize on the standard pile.                                                         
static int                                                                                                 
notcurses_resize(notcurses* n, int* restrict rows, int* restrict cols){                                    
  pthread_mutex_lock(&n->pilelock);                                                                        
  int ret = notcurses_resize_internal(notcurses_stdplane(n), rows, cols);                                  
  pthread_mutex_unlock(&n->pilelock);                                                                      
  return ret;                                                                                              
}
```

and likewise `notcurses_render()` always calls `ncpile_render()` with the standard plane:

```c
int notcurses_render(notcurses* nc){                                                                       
//fprintf(stderr, "--------------- BEGIN RENDER\n");                                                       
//notcurses_debug(nc, stderr);                                                                             
  ncplane* stdn = notcurses_stdplane(nc);                                                                  
  if(ncpile_render(stdn)){                                                                                 
    return -1;                                                                                             
  }                                                                                                        
  int i = ncpile_rasterize(stdn);                                                                          
//fprintf(stderr, "----------------- END RENDER\n");                                                       
  return i;                                                                                                
}
```

but we can call `ncpile_render()` with any plane, and it calls directly through to `notcurses_resize_internal()`:

```c
int ncpile_render(ncplane* n){                                                                             
  struct timespec start, renderdone;                                                                       
  clock_gettime(CLOCK_MONOTONIC, &start);                                                                  
  notcurses* nc = ncplane_notcurses(n);                                                                    
  ncpile* pile = ncplane_pile(n);                                                                          
  // update our notion of screen geometry, and render against that                                         
  notcurses_resize_internal(n, NULL, NULL);                                                                
  if(engorge_crender_vector(pile)){                                                                        
    return -1;                                                                                             
  }                                                                                                        
  // FIXME notcurses_stdplane() doesn't belong here #1615                                                  
  ncpile_render_internal(n, pile->crender, pile->dimy, pile->dimx,                                         
                         notcurses_stdplane(nc)->absy,                                                     
                         notcurses_stdplane(nc)->absx);                                                    
  clock_gettime(CLOCK_MONOTONIC, &renderdone);                                                             
  pthread_mutex_lock(&nc->statlock);                                                                       
  update_render_stats(&renderdone, &start, &nc->stats);                                                    
  pthread_mutex_unlock(&nc->statlock);                                                                     
  return 0;                                                                                                
}
```

=[
we also ought make the `tinfo` our source of truth of the most recently `ioctl()`-reported cell geometry, as discussed in #1525 
wouldn't we just duplicate the margins into each pile? margins are a property of the *context*, **not** of piles.

so really they belong in `termdesc`. that eliminates the thread safety issue, while keeping them in one place.

`absy`/`absx` then remain properties of planes, as they ought, interpreted relative to the margins of `termdesc`. there's no reason to have any of this in the pile structure.
BTW I'm not sure this is 100% related but it made me remember the following sentence from: https://notcurses.com/notcurses_visual.3.html#options ?

>"If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"

shouldn't it say relative to the terminal dimensions or termsize?
> > "If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"
> shouldn't it say relative to the terminal dimensions or termsize?

they're the same thing, no? the standard plane is guaranteed to be the same size as the visual area (terminal size minus margins).
the standard plane does have the size of the terminal, but when you use that term, it seems too specific, and if you are working with other piles without touching the standard plane it can be confusing to refer to the standard plane as a reference...

I'm finding these things while I'm working on the higher level rust wrappers where I'm not using the concept of the standard plane, and I'm trying to simplify and clarify the concepts...
sure, if that seems clearer i'll make that change
That was a source of confusion for me sometimes. In general I think is clearer to refer to the desired properties directly instead to an object that encarnate those properties, because the object can be mistaked for the needed thing, not the properties. And if the object is an example, clarify it as such.
so the margin information is already stored in the context. it's then reduced to the `absy`/`absx`/`row`/`cols` of the standard plane. we want the standard plane to accurately reflect the visual area's geometry. other root planes may or may not, but ought respect the margins. but margins are only relevant to rendering (and resizing of the standard plane) -- everywhere else, we have to work around them.

so i say we *stop putting the margins in the standard plane's `abs{yx}`*. rendering will get them out of the `struct notcurses*`, as will resize. they'll be 0 for the standard plane at all times, simplifying numerous functions.
oh wow, i think `notcurses_resize_internal()` is super broken when we're rendering anything but the standard pile. it is always called on the standard plane from `notcurses_resize()`:

```c
// Check for a window resize on the standard pile.                                                         
static int                                                                                                 
notcurses_resize(notcurses* n, int* restrict rows, int* restrict cols){                                    
  pthread_mutex_lock(&n->pilelock);                                                                        
  int ret = notcurses_resize_internal(notcurses_stdplane(n), rows, cols);                                  
  pthread_mutex_unlock(&n->pilelock);                                                                      
  return ret;                                                                                              
}
```

and likewise `notcurses_render()` always calls `ncpile_render()` with the standard plane:

```c
int notcurses_render(notcurses* nc){                                                                       
//fprintf(stderr, "--------------- BEGIN RENDER\n");                                                       
//notcurses_debug(nc, stderr);                                                                             
  ncplane* stdn = notcurses_stdplane(nc);                                                                  
  if(ncpile_render(stdn)){                                                                                 
    return -1;                                                                                             
  }                                                                                                        
  int i = ncpile_rasterize(stdn);                                                                          
//fprintf(stderr, "----------------- END RENDER\n");                                                       
  return i;                                                                                                
}
```

but we can call `ncpile_render()` with any plane, and it calls directly through to `notcurses_resize_internal()`:

```c
int ncpile_render(ncplane* n){                                                                             
  struct timespec start, renderdone;                                                                       
  clock_gettime(CLOCK_MONOTONIC, &start);                                                                  
  notcurses* nc = ncplane_notcurses(n);                                                                    
  ncpile* pile = ncplane_pile(n);                                                                          
  // update our notion of screen geometry, and render against that                                         
  notcurses_resize_internal(n, NULL, NULL);                                                                
  if(engorge_crender_vector(pile)){                                                                        
    return -1;                                                                                             
  }                                                                                                        
  // FIXME notcurses_stdplane() doesn't belong here #1615                                                  
  ncpile_render_internal(n, pile->crender, pile->dimy, pile->dimx,                                         
                         notcurses_stdplane(nc)->absy,                                                     
                         notcurses_stdplane(nc)->absx);                                                    
  clock_gettime(CLOCK_MONOTONIC, &renderdone);                                                             
  pthread_mutex_lock(&nc->statlock);                                                                       
  update_render_stats(&renderdone, &start, &nc->stats);                                                    
  pthread_mutex_unlock(&nc->statlock);                                                                     
  return 0;                                                                                                
}
```

=[
we also ought make the `tinfo` our source of truth of the most recently `ioctl()`-reported cell geometry, as discussed in #1525 
wouldn't we just duplicate the margins into each pile? margins are a property of the *context*, **not** of piles.

so really they belong in `termdesc`. that eliminates the thread safety issue, while keeping them in one place.

`absy`/`absx` then remain properties of planes, as they ought, interpreted relative to the margins of `termdesc`. there's no reason to have any of this in the pile structure.
BTW I'm not sure this is 100% related but it made me remember the following sentence from: https://notcurses.com/notcurses_visual.3.html#options ?

>"If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"

shouldn't it say relative to the terminal dimensions or termsize?
> > "If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"
> shouldn't it say relative to the terminal dimensions or termsize?

they're the same thing, no? the standard plane is guaranteed to be the same size as the visual area (terminal size minus margins).
the standard plane does have the size of the terminal, but when you use that term, it seems too specific, and if you are working with other piles without touching the standard plane it can be confusing to refer to the standard plane as a reference...

I'm finding these things while I'm working on the higher level rust wrappers where I'm not using the concept of the standard plane, and I'm trying to simplify and clarify the concepts...
sure, if that seems clearer i'll make that change
That was a source of confusion for me sometimes. In general I think is clearer to refer to the desired properties directly instead to an object that encarnate those properties, because the object can be mistaked for the needed thing, not the properties. And if the object is an example, clarify it as such.
so the margin information is already stored in the context. it's then reduced to the `absy`/`absx`/`row`/`cols` of the standard plane. we want the standard plane to accurately reflect the visual area's geometry. other root planes may or may not, but ought respect the margins. but margins are only relevant to rendering (and resizing of the standard plane) -- everywhere else, we have to work around them.

so i say we *stop putting the margins in the standard plane's `abs{yx}`*. rendering will get them out of the `struct notcurses*`, as will resize. they'll be 0 for the standard plane at all times, simplifying numerous functions.
oh wow, i think `notcurses_resize_internal()` is super broken when we're rendering anything but the standard pile. it is always called on the standard plane from `notcurses_resize()`:

```c
// Check for a window resize on the standard pile.                                                         
static int                                                                                                 
notcurses_resize(notcurses* n, int* restrict rows, int* restrict cols){                                    
  pthread_mutex_lock(&n->pilelock);                                                                        
  int ret = notcurses_resize_internal(notcurses_stdplane(n), rows, cols);                                  
  pthread_mutex_unlock(&n->pilelock);                                                                      
  return ret;                                                                                              
}
```

and likewise `notcurses_render()` always calls `ncpile_render()` with the standard plane:

```c
int notcurses_render(notcurses* nc){                                                                       
//fprintf(stderr, "--------------- BEGIN RENDER\n");                                                       
//notcurses_debug(nc, stderr);                                                                             
  ncplane* stdn = notcurses_stdplane(nc);                                                                  
  if(ncpile_render(stdn)){                                                                                 
    return -1;                                                                                             
  }                                                                                                        
  int i = ncpile_rasterize(stdn);                                                                          
//fprintf(stderr, "----------------- END RENDER\n");                                                       
  return i;                                                                                                
}
```

but we can call `ncpile_render()` with any plane, and it calls directly through to `notcurses_resize_internal()`:

```c
int ncpile_render(ncplane* n){                                                                             
  struct timespec start, renderdone;                                                                       
  clock_gettime(CLOCK_MONOTONIC, &start);                                                                  
  notcurses* nc = ncplane_notcurses(n);                                                                    
  ncpile* pile = ncplane_pile(n);                                                                          
  // update our notion of screen geometry, and render against that                                         
  notcurses_resize_internal(n, NULL, NULL);                                                                
  if(engorge_crender_vector(pile)){                                                                        
    return -1;                                                                                             
  }                                                                                                        
  // FIXME notcurses_stdplane() doesn't belong here #1615                                                  
  ncpile_render_internal(n, pile->crender, pile->dimy, pile->dimx,                                         
                         notcurses_stdplane(nc)->absy,                                                     
                         notcurses_stdplane(nc)->absx);                                                    
  clock_gettime(CLOCK_MONOTONIC, &renderdone);                                                             
  pthread_mutex_lock(&nc->statlock);                                                                       
  update_render_stats(&renderdone, &start, &nc->stats);                                                    
  pthread_mutex_unlock(&nc->statlock);                                                                     
  return 0;                                                                                                
}
```

=[
we also ought make the `tinfo` our source of truth of the most recently `ioctl()`-reported cell geometry, as discussed in #1525 
wouldn't we just duplicate the margins into each pile? margins are a property of the *context*, **not** of piles.

so really they belong in `termdesc`. that eliminates the thread safety issue, while keeping them in one place.

`absy`/`absx` then remain properties of planes, as they ought, interpreted relative to the margins of `termdesc`. there's no reason to have any of this in the pile structure.
BTW I'm not sure this is 100% related but it made me remember the following sentence from: https://notcurses.com/notcurses_visual.3.html#options ?

>"If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"

shouldn't it say relative to the terminal dimensions or termsize?
> > "If n was NULL (new plane), they specify the origin of the new plane relative to the standard plane"
> shouldn't it say relative to the terminal dimensions or termsize?

they're the same thing, no? the standard plane is guaranteed to be the same size as the visual area (terminal size minus margins).
the standard plane does have the size of the terminal, but when you use that term, it seems too specific, and if you are working with other piles without touching the standard plane it can be confusing to refer to the standard plane as a reference...

I'm finding these things while I'm working on the higher level rust wrappers where I'm not using the concept of the standard plane, and I'm trying to simplify and clarify the concepts...
sure, if that seems clearer i'll make that change
That was a source of confusion for me sometimes. In general I think is clearer to refer to the desired properties directly instead to an object that encarnate those properties, because the object can be mistaked for the needed thing, not the properties. And if the object is an example, clarify it as such.
so the margin information is already stored in the context. it's then reduced to the `absy`/`absx`/`row`/`cols` of the standard plane. we want the standard plane to accurately reflect the visual area's geometry. other root planes may or may not, but ought respect the margins. but margins are only relevant to rendering (and resizing of the standard plane) -- everywhere else, we have to work around them.

so i say we *stop putting the margins in the standard plane's `abs{yx}`*. rendering will get them out of the `struct notcurses*`, as will resize. they'll be 0 for the standard plane at all times, simplifying numerous functions.
oh wow, i think `notcurses_resize_internal()` is super broken when we're rendering anything but the standard pile. it is always called on the standard plane from `notcurses_resize()`:

```c
// Check for a window resize on the standard pile.                                                         
static int                                                                                                 
notcurses_resize(notcurses* n, int* restrict rows, int* restrict cols){                                    
  pthread_mutex_lock(&n->pilelock);                                                                        
  int ret = notcurses_resize_internal(notcurses_stdplane(n), rows, cols);                                  
  pthread_mutex_unlock(&n->pilelock);                                                                      
  return ret;                                                                                              
}
```

and likewise `notcurses_render()` always calls `ncpile_render()` with the standard plane:

```c
int notcurses_render(notcurses* nc){                                                                       
//fprintf(stderr, "--------------- BEGIN RENDER\n");                                                       
//notcurses_debug(nc, stderr);                                                                             
  ncplane* stdn = notcurses_stdplane(nc);                                                                  
  if(ncpile_render(stdn)){                                                                                 
    return -1;                                                                                             
  }                                                                                                        
  int i = ncpile_rasterize(stdn);                                                                          
//fprintf(stderr, "----------------- END RENDER\n");                                                       
  return i;                                                                                                
}
```

but we can call `ncpile_render()` with any plane, and it calls directly through to `notcurses_resize_internal()`:

```c
int ncpile_render(ncplane* n){                                                                             
  struct timespec start, renderdone;                                                                       
  clock_gettime(CLOCK_MONOTONIC, &start);                                                                  
  notcurses* nc = ncplane_notcurses(n);                                                                    
  ncpile* pile = ncplane_pile(n);                                                                          
  // update our notion of screen geometry, and render against that                                         
  notcurses_resize_internal(n, NULL, NULL);                                                                
  if(engorge_crender_vector(pile)){                                                                        
    return -1;                                                                                             
  }                                                                                                        
  // FIXME notcurses_stdplane() doesn't belong here #1615                                                  
  ncpile_render_internal(n, pile->crender, pile->dimy, pile->dimx,                                         
                         notcurses_stdplane(nc)->absy,                                                     
                         notcurses_stdplane(nc)->absx);                                                    
  clock_gettime(CLOCK_MONOTONIC, &renderdone);                                                             
  pthread_mutex_lock(&nc->statlock);                                                                       
  update_render_stats(&renderdone, &start, &nc->stats);                                                    
  pthread_mutex_unlock(&nc->statlock);                                                                     
  return 0;                                                                                                
}
```

=[

Fix patch:
diff --git a/NEWS.md b/NEWS.md
index 17c4ca9375..c345838262 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -26,6 +26,10 @@ rearrangements of Notcurses.
     `nccells_ascii_box()`. All are `static inline`.
   * A bug was fixed in `ncplane_move_yx()`: root planes were being moved
     relatively instead of absolutely. This was never the intended behavior.
+  * It used to be possible to pass `NULL` as the second parameter of
+    `ncplane_mergedown_simple()`, and have the standard plane be used as
+    the destination. This is no longer supported, since the source plane
+    could be in another pile. An error will instead be returned.
 
 * 2.3.2 (2021-06-03)
   * Fixed a bug affecting certain scalings of `ncvisual` objects created from
diff --git a/USAGE.md b/USAGE.md
index deeb8fdb3d..59d01ea98a 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -825,9 +825,10 @@ struct ncplane* ncplane_dup(struct ncplane* n, void* opaque);
 // this operation. Do not supply the same plane for both 'src' and 'dst'.
 int ncplane_mergedown(struct ncplane* restrict src, struct ncplane* restrict dst);
 
-// If 'src' does not intersect with 'dst', 'dst' will not be changed, but it is
-// not an error. If 'dst' is NULL, the operation will target the standard plane.
-int ncplane_mergedown_simple(const ncplane* restrict src, ncplane* restrict dst);
+// Merge the entirety of 'src' down onto the ncplane 'dst'. If 'src' does not
+// intersect with 'dst', 'dst' will not be changed, but it is not an error.
+int ncplane_mergedown_simple(struct ncplane* restrict src,
+                             struct ncplane* restrict dst);
 
 // Erase every cell in the ncplane, resetting all attributes to normal, all
 // colors to the default color, and all cells to undrawn. All cells associated
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 4166dec185..c3dc04f8b4 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -1966,8 +1966,8 @@ API int ncplane_format(struct ncplane* n, int ystop, int xstop, uint32_t stylema
 API int ncplane_stain(struct ncplane* n, int ystop, int xstop, uint64_t ul,
                       uint64_t ur, uint64_t ll, uint64_t lr);
 
-// If 'src' does not intersect with 'dst', 'dst' will not be changed, but it is
-// not an error. If 'dst' is NULL, the operation will target the standard plane.
+// Merge the entirety of 'src' down onto the ncplane 'dst'. If 'src' does not
+// intersect with 'dst', 'dst' will not be changed, but it is not an error.
 API int ncplane_mergedown_simple(struct ncplane* RESTRICT src,
                                  struct ncplane* RESTRICT dst);
 
diff --git a/src/lib/kitty.c b/src/lib/kitty.c
index 720911c2cb..e2d3a411d4 100644
--- a/src/lib/kitty.c
+++ b/src/lib/kitty.c
@@ -577,21 +577,21 @@ int kitty_remove(int id, FILE* out){
 
 // removes the kitty bitmap graphic identified by s->id, and damages those
 // cells which weren't SPRIXCEL_OPAQUE
-int kitty_destroy(const notcurses* nc, const ncpile* p, FILE* out, sprixel* s){
+int kitty_destroy(const notcurses* nc __attribute__ ((unused)),
+                  const ncpile* p, FILE* out, sprixel* s){
   if(kitty_remove(s->id, out)){
     return -1;
   }
 //fprintf(stderr, "FROM: %d/%d state: %d s->n: %p\n", s->movedfromy, s->movedfromx, s->invalidated, s->n);
-  const ncplane* stdn = notcurses_stdplane_const(nc);
   for(int yy = s->movedfromy ; yy < s->movedfromy + s->dimy && yy < p->dimy ; ++yy){
     for(int xx = s->movedfromx ; xx < s->movedfromx + s->dimx && xx < p->dimx ; ++xx){
-      const int ridx = (yy - stdn->absy) * p->dimx + (xx - stdn->absx);
+      const int ridx = yy * p->dimx + xx;
       struct crender *r = &p->crender[ridx];
       if(!r->sprixel){
         if(s->n){
 //fprintf(stderr, "CHECKING %d/%d\n", yy - s->movedfromy, xx - s->movedfromx);
-          sprixcell_e state = sprixel_state(s, yy - s->movedfromy + s->n->absy - stdn->absy,
-                                              xx - s->movedfromx + s->n->absx - stdn->absx);
+          sprixcell_e state = sprixel_state(s, yy - s->movedfromy + s->n->absy,
+                                              xx - s->movedfromx + s->n->absx);
           if(state == SPRIXCELL_OPAQUE_KITTY){
             r->s.damaged = 1;
           }else if(s->invalidated == SPRIXEL_MOVED){
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 565f9f457b..4589408a9e 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -486,8 +486,7 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n,
 static ncplane*
 create_initial_ncplane(notcurses* nc, int dimy, int dimx){
   ncplane_options nopts = {
-    .y = nc->margin_t,
-    .x = nc->margin_l,
+    .y = 0, .x = 0,
     .rows = dimy - (nc->margin_t + nc->margin_b),
     .cols = dimx - (nc->margin_l + nc->margin_r),
     .userptr = NULL,
@@ -566,10 +565,8 @@ inline int ncplane_cursor_move_yx(ncplane* n, int y, int x){
 ncplane* ncplane_dup(const ncplane* n, void* opaque){
   int dimy = n->leny;
   int dimx = n->lenx;
-  // if we're duping the standard plane, we need adjust for marginalia
-  const struct notcurses* nc = ncplane_notcurses_const(n);
-  const int placey = n->absy - nc->margin_t;
-  const int placex = n->absx - nc->margin_l;
+  const int placey = n->absy;
+  const int placex = n->absx;
   struct ncplane_options nopts = {
     .y = placey,
     .x = placex,
@@ -2019,14 +2016,14 @@ int ncplane_move_yx(ncplane* n, int y, int x){
 
 int ncplane_y(const ncplane* n){
   if(n->boundto == n){
-    return n->absy - ncplane_notcurses_const(n)->margin_t;
+    return n->absy;
   }
   return n->absy - n->boundto->absy;
 }
 
 int ncplane_x(const ncplane* n){
   if(n->boundto == n){
-    return n->absx - ncplane_notcurses_const(n)->margin_t;
+    return n->absx;
   }
   return n->absx - n->boundto->absx;
 }
diff --git a/src/lib/render.c b/src/lib/render.c
index 87f9983b7b..fc391f2384 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -514,9 +514,10 @@ int ncplane_mergedown(ncplane* restrict src, ncplane* restrict dst,
 }
 
 int ncplane_mergedown_simple(ncplane* restrict src, ncplane* restrict dst){
-  const notcurses* nc = ncplane_notcurses_const(src);
+  // have to check dst, since we used to accept a NULL dst to mean the
+  // standard plane (this was unsafe, since src might be in another pile).
   if(dst == NULL){
-    dst = nc->stdplane;
+    return -1;
   }
   int dimy, dimx;
   ncplane_dim_yx(dst, &dimy, &dimx);
@@ -912,11 +913,11 @@ clean_sprixels(notcurses* nc, ncpile* p, FILE* out){
       ncplane_yx(s->n, &y, &x);
       // FIXME clean this up, don't use sprite_draw, etc.
       // without this, kitty flickers
-//fprintf(stderr, "1 MOVING BITMAP %d STATE %d AT %d/%d for %p\n", s->id, s->invalidated, y + nc->stdplane->absy, x + nc->stdplane->absx, s->n);
+//fprintf(stderr, "1 MOVING BITMAP %d STATE %d AT %d/%d for %p\n", s->id, s->invalidated, y + nc->margin_t, x + nc->margin_l, s->n);
       if(s->invalidated == SPRIXEL_MOVED){
         sprite_destroy(nc, p, out, s);
       }
-      if(goto_location(nc, out, y + nc->stdplane->absy, x + nc->stdplane->absx) == 0){
+      if(goto_location(nc, out, y + nc->margin_t, x + nc->margin_l) == 0){
         if(sprite_draw(nc, p, s, out)){
           return -1;
         }
@@ -941,8 +942,8 @@ rasterize_sprixels(notcurses* nc, ncpile* p, FILE* out){
     if(s->invalidated == SPRIXEL_INVALIDATED){
       int y, x;
       ncplane_yx(s->n, &y, &x);
-//fprintf(stderr, "3 DRAWING BITMAP %d STATE %d AT %d/%d for %p\n", s->id, s->invalidated, y + nc->stdplane->absy, x + nc->stdplane->absx, s->n);
-      if(goto_location(nc, out, y + nc->stdplane->absy, x + nc->stdplane->absx) == 0){
+//fprintf(stderr, "3 DRAWING BITMAP %d STATE %d AT %d/%d for %p\n", s->id, s->invalidated, y + nc->margin_t, x + nc->margin_l, s->n);
+      if(goto_location(nc, out, y + nc->margin_t, x + nc->margin_l) == 0){
         if(sprite_draw(nc, p, s, out)){
           return -1;
         }
@@ -970,10 +971,10 @@ rasterize_sprixels(notcurses* nc, ncpile* p, FILE* out){
 static int
 rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
   struct crender* rvec = p->crender;
-  for(int y = nc->stdplane->absy ; y < p->dimy + nc->stdplane->absy ; ++y){
-    const int innery = y - nc->stdplane->absy;
-    for(int x = nc->stdplane->absx ; x < p->dimx + nc->stdplane->absx ; ++x){
-      const int innerx = x - nc->stdplane->absx;
+  for(int y = nc->margin_t; y < p->dimy + nc->margin_t ; ++y){
+    const int innery = y - nc->margin_t;
+    for(int x = nc->margin_l ; x < p->dimx + nc->margin_l ; ++x){
+      const int innerx = x - nc->margin_l;
       const size_t damageidx = innery * nc->lfdimx + innerx;
       unsigned r, g, b, br, bg, bb;
       const nccell* srccell = &nc->lastframe[damageidx];
@@ -1062,7 +1063,7 @@ rasterize_core(notcurses* nc, const ncpile* p, FILE* out, unsigned phase){
         // this is used to invalidate the sprixel in the first text round,
         // which is only necessary for sixel, not kitty.
         if(rvec[damageidx].sprixel){
-          sprixcell_e scstate = sprixel_state(rvec[damageidx].sprixel, y - nc->stdplane->absy, x - nc->stdplane->absx);
+          sprixcell_e scstate = sprixel_state(rvec[damageidx].sprixel, y - nc->margin_t, x - nc->margin_l);
           if((scstate == SPRIXCELL_MIXED_SIXEL || scstate == SPRIXCELL_OPAQUE_SIXEL)
              && !rvec[damageidx].s.p_beats_sprixel){
 //fprintf(stderr, "INVALIDATING at %d/%d (%u)\n", y, x, rvec[damageidx].s.p_beats_sprixel);
@@ -1094,7 +1095,7 @@ notcurses_rasterize_inner(notcurses* nc, ncpile* p, FILE* out){
   // don't write a clearscreen. we only update things that have been changed.
   // we explicitly move the cursor at the beginning of each output line, so no
   // need to home it expliticly.
-//fprintf(stderr, "pile %p ymax: %d xmax: %d\n", p, p->dimy + nc->stdplane->absy, p->dimx + nc->stdplane->absx);
+//fprintf(stderr, "pile %p ymax: %d xmax: %d\n", p, p->dimy + nc->margin_t, p->dimx + nc->margin_l);
   if(clean_sprixels(nc, p, out) < 0){
     return -1;
   }
@@ -1194,8 +1195,8 @@ int notcurses_refresh(notcurses* nc, int* restrict dimy, int* restrict dimx){
     return -1;
   }
   ncpile p = {};
-  p.dimy = nc->stdplane->leny;
-  p.dimx = nc->stdplane->lenx;
+  p.dimy = nc->margin_t;
+  p.dimx = nc->margin_l;
   const int count = (nc->lfdimx > p.dimx ? nc->lfdimx : p.dimx) *
                     (nc->lfdimy > p.dimy ? nc->lfdimy : p.dimy);
   p.crender = malloc(count * sizeof(*p.crender));
@@ -1226,8 +1227,8 @@ int notcurses_render_to_file(notcurses* nc, FILE* fp){
     return -1;
   }
   ncpile p;
-  p.dimy = nc->stdplane->leny;
-  p.dimx = nc->stdplane->lenx;
+  p.dimy = nc->margin_t;
+  p.dimx = nc->margin_l;
   const int count = (nc->lfdimx > p.dimx ? nc->lfdimx : p.dimx) *
                     (nc->lfdimy > p.dimy ? nc->lfdimy : p.dimy);
   p.crender = malloc(count * sizeof(*p.crender));
@@ -1261,13 +1262,13 @@ int notcurses_render_to_file(notcurses* nc, FILE* fp){
 // down the z-buffer, looking at intersections with ncplanes. This implies
 // locking down the EGC, the attributes, and the channels for each cell.
 static void
-ncpile_render_internal(ncplane* n, struct crender* rvec, int leny, int lenx,
-                       int absy, int absx){
+ncpile_render_internal(ncplane* n, struct crender* rvec, int leny, int lenx){
+//fprintf(stderr, "rendering %dx%d\n", leny, lenx);
   ncpile* np = ncplane_pile(n);
   ncplane* p = np->top;
   sprixel* sprixel_list = NULL;
   while(p){
-    paint(p, rvec, leny, lenx, absy, absx, &sprixel_list);
+    paint(p, rvec, leny, lenx, 0, 0, &sprixel_list);
     p = p->below;
   }
   if(sprixel_list){
@@ -1338,10 +1339,7 @@ int ncpile_render(ncplane* n){
   if(engorge_crender_vector(pile)){
     return -1;
   }
-  // FIXME notcurses_stdplane() doesn't belong here #1615
-  ncpile_render_internal(n, pile->crender, pile->dimy, pile->dimx,
-                         notcurses_stdplane(nc)->absy,
-                         notcurses_stdplane(nc)->absx);
+  ncpile_render_internal(n, pile->crender, pile->dimy, pile->dimx);
   clock_gettime(CLOCK_MONOTONIC, &renderdone);
   pthread_mutex_lock(&nc->statlock);
   update_render_stats(&renderdone, &start, &nc->stats);
@@ -1458,7 +1456,7 @@ int notcurses_cursor_enable(notcurses* nc, int y, int x){
     logerror(nc, "Illegal cursor placement: %d, %d\n", y, x);
     return -1;
   }
-  if(y >= nc->stdplane->leny || x >= nc->stdplane->lenx){
+  if(y >= nc->margin_t || x >= nc->margin_l){
     logerror(nc, "Illegal cursor placement: %d, %d\n", y, x);
     return -1;
   }
@@ -1472,7 +1470,7 @@ int notcurses_cursor_enable(notcurses* nc, int y, int x){
     return -1;
   }
   // updates nc->rstate.cursor{y,x}
-  if(goto_location(nc, nc->ttyfp, y + nc->stdplane->absy, x + nc->stdplane->absx)){
+  if(goto_location(nc, nc->ttyfp, y + nc->margin_t, x + nc->margin_l)){
     return -1;
   }
   // if we were already positive, we're already visible, no need to write cnorm
diff --git a/src/lib/sixel.c b/src/lib/sixel.c
index c3bd2deb98..df1ca6a938 100644
--- a/src/lib/sixel.c
+++ b/src/lib/sixel.c
@@ -809,16 +809,15 @@ int sixel_destroy(const notcurses* nc, const ncpile* p, FILE* out, sprixel* s){
   (void)out;
   int starty = s->movedfromy;
   int startx = s->movedfromx;
-  const ncplane* stdn = notcurses_stdplane_const(nc);
   for(int yy = starty ; yy < starty + s->dimy && yy < p->dimy ; ++yy){
     for(int xx = startx ; xx < startx + s->dimx && xx < p->dimx ; ++xx){
-      int ridx = (yy - stdn->absy) * p->dimx + (xx - stdn->absx);
+      int ridx = yy * p->dimx + xx;
       struct crender *r = &p->crender[ridx];
       if(!r->sprixel){
         if(s->n){
 //fprintf(stderr, "CHECKING %d/%d\n", yy - s->movedfromy, xx - s->movedfromx);
-          sprixcell_e state = sprixel_state(s, yy - s->movedfromy + s->n->absy - stdn->absy,
-                                            xx - s->movedfromx + s->n->absx - stdn->absx);
+          sprixcell_e state = sprixel_state(s, yy - s->movedfromy + s->n->absy,
+                                            xx - s->movedfromx + s->n->absx);
           if(state == SPRIXCELL_OPAQUE_SIXEL || state == SPRIXCELL_MIXED_SIXEL){
             r->s.damaged = 1;
           }else if(s->invalidated == SPRIXEL_MOVED){


Test patch:
diff --git a/src/tests/fills.cpp b/src/tests/fills.cpp
index 6eac56c669..7f7a5987fa 100644
--- a/src/tests/fills.cpp
+++ b/src/tests/fills.cpp
@@ -434,7 +434,7 @@ TEST_CASE("Fills") {
     CHECK(0 == ncplane_cursor_move_yx(p3, 0, 0));
     // make sure glyphs replace glyps
     CHECK(0 < ncplane_putstr(p3, "🞵🞶🞷🞸🞹█▀▄▌▐"));
-    CHECK(0 == ncplane_mergedown_simple(p3, nullptr));
+    CHECK(0 == ncplane_mergedown_simple(p3, n_));
     nccell c3 = CELL_TRIVIAL_INITIALIZER;
     for(int i = 0 ; i < 10 ; ++i){
       CHECK(0 < ncplane_at_yx_cell(n_, 0, i, &cbase));
@@ -444,7 +444,7 @@ TEST_CASE("Fills") {
     CHECK(0 == notcurses_render(nc_));
     // make sure nulls do not replace glyphs
     auto p2 = ncplane_create(n_, &nopts);
-    CHECK(0 == ncplane_mergedown_simple(p2, nullptr));
+    CHECK(0 == ncplane_mergedown_simple(p2, n_));
     ncplane_destroy(p2);
     for(int i = 0 ; i < 10 ; ++i){
       CHECK(0 < ncplane_at_yx_cell(n_, 0, i, &cbase));