Repository: dankamongmen/notcurses (language: python)

Issue:
ncvisual_render needs be pile-aware
When `ncvisual_render()` is called with a `ncvisual_options` containing a `NULL` value for `n` (i.e. the create a new plane case), the `y` and `x` fields are interpreted as origins of the new plane. Said plane will always be placed relative to the standard pile. We should be able to create a visual plane in a different pile. Sure, the user can always immediately move the plane, but this means `ncvisual_render()` cannot be used without a target plane by some other pile when the standard pile is being rendered (or any number of other things).

ncvisual_render needs be pile-aware
When `ncvisual_render()` is called with a `ncvisual_options` containing a `NULL` value for `n` (i.e. the create a new plane case), the `y` and `x` fields are interpreted as origins of the new plane. Said plane will always be placed relative to the standard pile. We should be able to create a visual plane in a different pile. Sure, the user can always immediately move the plane, but this means `ncvisual_render()` cannot be used without a target plane by some other pile when the standard pile is being rendered (or any number of other things).

ncvisual_render needs be pile-aware
When `ncvisual_render()` is called with a `ncvisual_options` containing a `NULL` value for `n` (i.e. the create a new plane case), the `y` and `x` fields are interpreted as origins of the new plane. Said plane will always be placed relative to the standard pile. We should be able to create a visual plane in a different pile. Sure, the user can always immediately move the plane, but this means `ncvisual_render()` cannot be used without a target plane by some other pile when the standard pile is being rendered (or any number of other things).



Discussion / hints:
Breaking the sprixel cache up among piles isn't hard, and indeed I'm about to check that in. The hard part is ensuring, say, that a sprixel cleaned up by the destruction of a pile gets turned into an actual kill, since it won't show up in the render cycle. Probably need to keep a context-wide list of those. Figure this out for 3.0.0.
one thing i'm pretty sure we need is to move the damage detection entiretly into rasterization. right now we do it primarily in rendering, but under the multipile model, the most recent frame (against which we must perform damage detection) isn't decided until we rasterize. we'll still need to set damage in the rendering phase for e.g. sprixel invalidations, but all `cellcmp_and_dupfar()` needs to take place during rasterization for sure. this is a major outstanding bug in our model, unless i'm completely missing something.
so the problem is down in `ncvisual_render_pixels()` and `ncvisual_render_cells()`, where we always go to `notcurses_stdplane()` for a `NULL` n. we need preserve that behavior in `ncvisual_render()` unless/until we remove it.

alright, let's introduce a new `ncvisual_blit()`, so to at least eliminate the confusion that can arise from `ncpile_render()` vs `ncvisual_render()`, and to fit in with `ncvisual_blitter_geom()`, and to change the semantics of a `NULL` `vopts->n`. prototype:

`struct ncplane* ncvisual_blit(struct ncplane* p, const struct ncvisual* ncv, const ncvisual_options *vopts);`

same as `ncvisual_render()`, actually. just the semantics change for `NULL` `vopts->n`; it now gets you a new pile. at the same time, we ought introduce code to `ncvisualplane_create()` to call `ncpile_create()` with a `NULL` n. which means we would need add `notcurses*nc` to its signature, but it's `static inline`, so we can do that freely.
I've made the switch to `ncvisualplane_create()`, and documented it in `NEWS.md`, in `master`. I'm making the other change in `dankamongmen/ncvisual_blit`.
Breaking the sprixel cache up among piles isn't hard, and indeed I'm about to check that in. The hard part is ensuring, say, that a sprixel cleaned up by the destruction of a pile gets turned into an actual kill, since it won't show up in the render cycle. Probably need to keep a context-wide list of those. Figure this out for 3.0.0.
one thing i'm pretty sure we need is to move the damage detection entiretly into rasterization. right now we do it primarily in rendering, but under the multipile model, the most recent frame (against which we must perform damage detection) isn't decided until we rasterize. we'll still need to set damage in the rendering phase for e.g. sprixel invalidations, but all `cellcmp_and_dupfar()` needs to take place during rasterization for sure. this is a major outstanding bug in our model, unless i'm completely missing something.
so the problem is down in `ncvisual_render_pixels()` and `ncvisual_render_cells()`, where we always go to `notcurses_stdplane()` for a `NULL` n. we need preserve that behavior in `ncvisual_render()` unless/until we remove it.

alright, let's introduce a new `ncvisual_blit()`, so to at least eliminate the confusion that can arise from `ncpile_render()` vs `ncvisual_render()`, and to fit in with `ncvisual_blitter_geom()`, and to change the semantics of a `NULL` `vopts->n`. prototype:

`struct ncplane* ncvisual_blit(struct ncplane* p, const struct ncvisual* ncv, const ncvisual_options *vopts);`

same as `ncvisual_render()`, actually. just the semantics change for `NULL` `vopts->n`; it now gets you a new pile. at the same time, we ought introduce code to `ncvisualplane_create()` to call `ncpile_create()` with a `NULL` n. which means we would need add `notcurses*nc` to its signature, but it's `static inline`, so we can do that freely.
I've made the switch to `ncvisualplane_create()`, and documented it in `NEWS.md`, in `master`. I'm making the other change in `dankamongmen/ncvisual_blit`.
Breaking the sprixel cache up among piles isn't hard, and indeed I'm about to check that in. The hard part is ensuring, say, that a sprixel cleaned up by the destruction of a pile gets turned into an actual kill, since it won't show up in the render cycle. Probably need to keep a context-wide list of those. Figure this out for 3.0.0.
one thing i'm pretty sure we need is to move the damage detection entiretly into rasterization. right now we do it primarily in rendering, but under the multipile model, the most recent frame (against which we must perform damage detection) isn't decided until we rasterize. we'll still need to set damage in the rendering phase for e.g. sprixel invalidations, but all `cellcmp_and_dupfar()` needs to take place during rasterization for sure. this is a major outstanding bug in our model, unless i'm completely missing something.
so the problem is down in `ncvisual_render_pixels()` and `ncvisual_render_cells()`, where we always go to `notcurses_stdplane()` for a `NULL` n. we need preserve that behavior in `ncvisual_render()` unless/until we remove it.

alright, let's introduce a new `ncvisual_blit()`, so to at least eliminate the confusion that can arise from `ncpile_render()` vs `ncvisual_render()`, and to fit in with `ncvisual_blitter_geom()`, and to change the semantics of a `NULL` `vopts->n`. prototype:

`struct ncplane* ncvisual_blit(struct ncplane* p, const struct ncvisual* ncv, const ncvisual_options *vopts);`

same as `ncvisual_render()`, actually. just the semantics change for `NULL` `vopts->n`; it now gets you a new pile. at the same time, we ought introduce code to `ncvisualplane_create()` to call `ncpile_create()` with a `NULL` n. which means we would need add `notcurses*nc` to its signature, but it's `static inline`, so we can do that freely.
I've made the switch to `ncvisualplane_create()`, and documented it in `NEWS.md`, in `master`. I'm making the other change in `dankamongmen/ncvisual_blit`.

Fix patch:
diff --git a/NEWS.md b/NEWS.md
index fb42b2b72f..9e3b772fb1 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,7 +1,17 @@
 This document attempts to list user-visible changes and any major internal
 rearrangements of Notcurses.
 
-* 2.4.6 (not yet released) **"In the A"**
+* 3.0.0 (not yet released) **"In the A"**
+  * Made the ABI changes that have been planned/collected during 2.x
+    development. This primarily involved removing deprecated functions,
+    and making some `static inline` (and thus no longer linkable symbols).
+    There have been a few small renamings (i.e. `ncplane_pixelgeom()` to
+    `ncplane_pixel_geom()`) for purposes of regularity. The only thing removed
+    without an obvious replacement is the `renderfp` field of
+    `notcurses_options`, for which I make no apology. If you've been avoiding
+    deprecated functionality, ABI3 ought require small changes, if any.
+
+* 2.4.6 (not yet released)
   * Features 1, 2, and 8 of the Kitty keyboard protocol are now supported. This
     provides much more detailed and fine-grained keyboard reports, including
     key repeat and release events, and modifier events (i.e. pressing Shift by
@@ -13,6 +23,20 @@ rearrangements of Notcurses.
     DA1 response will be considered as claiming support for Sixel with 256
     color registers. If you're a terminal author, please do `XTSMGRAPHICS`.
     Actually, please implement the vastly superior Kitty graphics protocol.
+  * `ncvisualplane_create()` now allows for a new pile to be created, by
+    passing a `NULL` ancestor `ncplane` in `vopts`. The first argument is
+    now a `struct notcurses*` rather than a `struct ncplane*`.
+  * `ncvisual_render()` has been deprecated in favor of the new function
+    `ncvisual_blit()`. When a `NULL` `vopts->n` is passed to `ncvisual_blit()`,
+    a new plane is created (as it was in `ncvisual_render()`, but that plane
+    is the root of a new pile, rather than a child of the standard plane.
+    The only tricky conversion is if you previously had `vopts.n` as `NULL`,
+    and were not using `NCVISUAL_OPTION_CHILDPLANE` (or were passing `NULL`
+    as `vopts`). This would result in a new plane bound to the standard plane
+    with `ncvisual_render()`, but with `ncvisual_blit()` it will create a new
+    pile. To keep the behavior, explicitly pass the standard plane as
+    `vopts->n`, and include `NCVISUAL_OPTION_CHILDPLANE` in `vopts->flags`.
+    All other cases will continue to work as they did before.
 
 * 2.4.5 (2021-10-06)
   * The poorly-considered function `ncplane_boundlist()`, added in 2.3.17, has
diff --git a/README.md b/README.md
index 42c8c820f3..771700d53d 100644
--- a/README.md
+++ b/README.md
@@ -295,7 +295,7 @@ If things break or seem otherwise lackluster, **please** consult the
   Notcurses will not make use of bitmap protocols unless the terminal positively
   indicates support for them, even if `NCBLIT_PIXEL` has been requested. Likewise,
   sextants (`NCBLIT_3x2`) won't be used without Unicode 13 support, etc.
-  `ncvisual_render()` will use the best blitter available, unless
+  `ncvisual_blit()` will use the best blitter available, unless
   `NCVISUAL_OPTION_NODEGRADE` is provided (in which case it will fail).
 </details>
 
diff --git a/USAGE.md b/USAGE.md
index 04bd641510..8db2430e24 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -3301,24 +3301,37 @@ int ncvisual_set_yx(const struct ncvisual* n, int y, int x, uint32_t pixel);
 // If a subtitle ought be displayed at this time, return a new plane (bound
 // to 'parent' containing the subtitle, which might be text or graphics
 // (depending on the input format).
-struct ncplane* ncvisual_subtitle(struct ncplane* parent,
-                                  const struct ncvisual* ncv);
+struct ncplane* ncvisual_subtitle_plane(struct ncplane* parent,
+                                        const struct ncvisual* ncv);
 ```
 
 And finally, the `ncvisual` can be blitted to one or more `ncplane`s:
 
 ```c
-// Render the decoded frame to the specified ncplane. If one is not provided,
-// one will be created, having the exact size necessary to display the visual.
+// Render the decoded frame according to the provided options (which may be
+// NULL). The plane used for rendering depends on vopts->n and vopts->flags.
+// If NCVISUAL_OPTION_CHILDPLANE is set, vopts->n must not be NULL, and the
+// plane will always be created as a child of vopts->n. If this flag is not
+// set, and vopts->n is NULL, a new plane is created as root of a new pile.
+// If the flag is not set and vopts->n is not NULL, we render to vopts->n.
 // A subregion of the visual can be rendered using 'begx', 'begy', 'lenx', and
 // 'leny'. Negative values for 'begy' or 'begx' are an error. It is an error to
-// specify any region beyond the boundaries of the frame. Returns the
-// (possibly newly-created) plane to which we drew.
-struct ncplane* ncvisual_render(struct notcurses* nc, struct ncvisual* ncv,
-                                    const struct ncvisual_options* vopts)
+// specify any region beyond the boundaries of the frame. Returns the (possibly
+// newly-created) plane to which we drew. Pixels may not be blitted to the
+// standard plane.
+struct ncplane* ncvisual_blit(struct notcurses* nc, struct ncvisual* ncv,
+                              const struct ncvisual_options* vopts);
+
+// Create a new plane as prescribed in opts, either as a child of 'vopts->n',
+// or the root of a new pile if 'vopts->n' is NULL (or 'vopts' itself is NULL).
+// Blit 'ncv' to the created plane according to 'vopts'. If 'vopts->n' is
+// non-NULL, NCVISUAL_OPTION_CHILDPLANE must be supplied.
+static inline struct ncplane*
+ncvisualplane_create(struct notcurses* nc, const struct ncplane_options* opts,
+                     struct ncvisual* ncv, struct ncvisual_options* vopts);
 
 // decode the next frame ala ncvisual_decode(), but if we have reached the end,
-// rewind to the first frame of the ncvisual. a subsequent `ncvisual_render()`
+// rewind to the first frame of the ncvisual. a subsequent `ncvisual_blit()`
 // will render the first frame, as if the ncvisual had been closed and reopened.
 // the return values remain the same as those of ncvisual_decode().
 int ncvisual_decode_loop(struct ncvisual* nc);
@@ -3411,27 +3424,8 @@ typedef int (*streamcb)(struct ncplane*, struct ncvisual*,
 // Shut up and display my frames! Provide as an argument to ncvisual_stream().
 // If you'd like subtitles to be decoded, provide an ncplane as the curry. If the
 // curry is NULL, subtitles will not be displayed.
-static inline int
-ncvisual_simple_streamer(struct ncplane* n, struct ncvisual* ncv,
-                         const struct timespec* tspec, void* curry){
-  if(notcurses_render(ncplane_notcurses(n))){
-    return -1;
-  }
-  int ret = 0;
-  if(curry){
-    // need a cast for C++ callers
-    struct ncplane* subncp = (struct ncplane*)curry;
-    char* subtitle = ncvisual_subtitle(ncv);
-    if(subtitle){
-      if(ncplane_putstr_yx(subncp, 0, 0, subtitle) < 0){
-        ret = -1;
-      }
-      free(subtitle);
-    }
-  }
-  clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, tspec, NULL);
-  return ret;
-}
+int ncvisual_simple_streamer(struct ncvisual* ncv, struct ncvisual_options* vopts,
+                             const struct timespec* tspec, void* curry);
 
 // Stream the entirety of the media, according to its own timing. Blocking,
 // obviously. streamer may be NULL; it is otherwise called for each frame, and
diff --git a/doc/HACKING.md b/doc/HACKING.md
index 540e431678..1071f34d7f 100644
--- a/doc/HACKING.md
+++ b/doc/HACKING.md
@@ -331,7 +331,7 @@ Its `data` field holds RGBA pixels as provided from disk or memory. Its `pixx`,
 `rowstride` bytes, each containing `pixx` RGBA pixels at the front, plus any
 necessary padding (external libraries might generate padded output).
 
-`ncvisual_render` works with at least four geometries:
+`ncvisual_blit` works with at least four geometries:
 * `vopts->begy`/`begx`: offsets into unscaled data (pixels)
 * `vopts->leny`/`lenx`: lengths of unscaled data to use (pixels)
   * These geometries, when summed, must not exceed `ncv->pixy`/`ncv->pixx`.
diff --git a/doc/man/man3/notcurses.3.md b/doc/man/man3/notcurses.3.md
index 69824883ed..52bcc57fd1 100644
--- a/doc/man/man3/notcurses.3.md
+++ b/doc/man/man3/notcurses.3.md
@@ -89,7 +89,7 @@ Following initialization, a single ncplane exists, the "standard plane" (see
 **notcurses_stdplane(3)**). This plane cannot be destroyed nor manually resized,
 and is always exactly as large as the screen (if run without a TTY, the "screen"
 is assumed to be 80x24 cells). Further ncplanes can be created with
-**ncplane_new(3)**. A total z-ordering always exists on the set of ncplanes,
+**ncplane_create(3)**. A total z-ordering always exists on the set of ncplanes,
 and new ncplanes are placed at the top of the z-buffer. Ncplanes can be larger,
 smaller, or the same size as the physical screen, and can be placed anywhere
 relative to it (including entirely off-screen). Ncplanes are made up of
diff --git a/doc/man/man3/notcurses_plane.3.md b/doc/man/man3/notcurses_plane.3.md
index f6407e5e01..92ed725c4e 100644
--- a/doc/man/man3/notcurses_plane.3.md
+++ b/doc/man/man3/notcurses_plane.3.md
@@ -439,7 +439,7 @@ the size of a cell in pixels (these ought be the same across planes).
 **maxbmapy** and **maxbmapx** describe the largest bitmap which can be
 displayed in the plane. Any parameter (save **n**) may be **NULL**.
 
-When a plane is blitted to using **ncvisual_render** and **NCBLIT_PIXEL** (see
+When a plane is blitted to using **ncvisual_blit** and **NCBLIT_PIXEL** (see
 **notcurses_visual(3)**), it ceases to accept cell-based output. The sprixel
 will remain associated until a new sprixel is blitted to the plane, the plane
 is resized, the plane is erased, or the plane is destroyed. The base cell of a
diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md
index a36cba34d4..b7022c12a7 100644
--- a/doc/man/man3/notcurses_visual.3.md
+++ b/doc/man/man3/notcurses_visual.3.md
@@ -75,7 +75,9 @@ typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*);
 
 **int ncvisual_decode_loop(struct ncvisual* ***ncv***);**
 
-**struct ncplane* ncvisual_render(struct notcurses* ***nc***, struct ncvisual* ***ncv***, const struct ncvisual_options* ***vopts***);**
+**struct ncplane* ncvisual_blit(struct notcurses* ***nc***, struct ncvisual* ***ncv***, const struct ncvisual_options* ***vopts***);**
+
+**static inline struct ncplane* ncvisualplane_create(struct notcurses* ***nc***, const struct ncplane_options* ***opts***, struct ncvisual* ***ncv***, struct ncvisual_options* ***vopts***);**
 
 **int ncvisual_simple_streamer(struct ncplane* ***n***, struct ncvisual* ***ncv***, const struct timespec* ***disptime***, void* ***curry***);**
 
@@ -93,7 +95,7 @@ typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*);
 
 **int ncvisual_set_yx(const struct ncvisual* ***n***, int ***y***, int ***x***, uint32_t ***pixel***);**
 
-**struct ncplane* ncvisual_subtitle(struct ncplane* ***parent***, const struct ncvisual* ***ncv***);**
+**struct ncplane* ncvisual_subtitle_plane(struct ncplane* ***parent***, const struct ncvisual* ***ncv***);**
 
 **int notcurses_lex_scalemode(const char* ***op***, ncscale_e* ***scaling***);**
 
@@ -124,7 +126,7 @@ Once the visual is loaded, it can be transformed using **ncvisual_rotate**,
 **ncvisual_resize**, and **ncvisual_resize_noninterpolative**. These are
 persistent operations, unlike any scaling that takes place at render time. If a
 subtitle is associated with the frame, it can be acquired with
-**ncvisual_subtitle**. **ncvisual_resize** uses the media layer's best scheme
+**ncvisual_subtitle_plane**. **ncvisual_resize** uses the media layer's best scheme
 to enlarge or shrink the original data, typically involving some interpolation.
 **ncvisual_resize_noninterpolative** performs a naive linear sampling,
 retaining only original colors.
@@ -152,11 +154,12 @@ glyphs within this region are those used by the specified blitter.
 **ncvisual_rotate** executes a rotation of ***rads*** radians, in the clockwise
 (positive) or counterclockwise (negative) direction.
 
-**ncvisual_subtitle** will return a UTF-8-encoded subtitle corresponding to
-the current frame if such a subtitle was decoded. Note that a subtitle might
-be returned for multiple frames, or might not.
+**ncvisual_subtitle_plane** returns a **struct ncplane** suitable for display,
+if the current frame had such a subtitle. Note that the same subtitle might
+be returned for multiple frames, or might not. It is atypical for all frames
+to have subtitles. Subtitles can be text or graphics.
 
-**ncvisual_render** blits the visual to an **ncplane**, based on the contents
+**ncvisual_blit** draws the visual to an **ncplane**, based on the contents
 of its **struct ncvisual_options**. If ***n*** is not **NULL**, it specifies the
 plane on which to render, and ***y***/***x*** specify a location within that plane.
 Otherwise, a new plane will be created, and placed at ***y***/***x*** relative to
@@ -270,7 +273,7 @@ information.
 
 Some terminals support pixel-based output via one of a number of protocols.
 **NCBLIT_PIXEL** has some stringent requirements on the type of planes it can
-be used with; it is usually best to let **ncvisual_render** create the backing
+be used with; it is usually best to let **ncvisual_blit** create the backing
 plane by providing a **NULL** value for **n**. If you must bring your own
 plane, it must be perfectly sized for the bitmap (i.e. large enough, and not
 more than a full cell larger in either dimension--the bitmap, always placed at
@@ -304,12 +307,12 @@ that the entire file is properly-formed.
 failure. It is only necessary for multimedia-based visuals. It advances one
 frame for each call. **ncvisual_decode_loop** has the same return values: when
 called following decoding of the last frame, it will return 1, but a subsequent
-**ncvisual_render** will return the first frame.
+**ncvisual_blit** will return the first frame.
 
 **ncvisual_from_plane** returns **NULL** if the **ncvisual** cannot be created
 and bound. This is usually due to illegal content in the source **ncplane**.
 
-**ncvisual_render** returns **NULL** on error, and otherwise the plane to
+**ncvisual_blit** returns **NULL** on error, and otherwise the plane to
 which the visual was rendered. If **opts->n** is provided, this will be
 **opts->n**. Otherwise, a plane will be created, perfectly sized for the
 visual and the specified blitter.
@@ -328,7 +331,8 @@ aspect-preserving **NCBLIT_2x1** will be returned. If sextants are available
 Multimedia decoding requires that Notcurses be built with either FFmpeg or
 OpenImageIO support. What formats can be decoded is totally dependent on the
 linked library. OpenImageIO does not support subtitles. Functions requiring
-a multimedia backend include **ncvisual_from_file** and **ncvisual_subtitle**.
+a multimedia backend include **ncvisual_from_file** and
+**ncvisual_subtitle_plane**.
 
 Sixel documentation can be found at [Dankwiki](https://nick-black.com/dankwiki/index.php?title=Sixel).
 Kitty's graphics protocol is specified in [its documentation](https://sw.kovidgoyal.net/kitty/graphics-protocol.html).
@@ -365,7 +369,7 @@ sprixel cannot be accessed.
 **ncvisual_rotate** currently supports only **M_PI**/2 and -**M_PI**/2
 radians for **rads**, but this will change soon.
 
-**ncvisual_render** should be able to create new planes in piles other than
+**ncvisual_blit** should be able to create new planes in piles other than
 the standard pile. This ought become a reality soon.
 
 **ncvisual_stream** currently requires a multimedia engine, which is silly.
@@ -374,7 +378,7 @@ This will change in the near future.
 Sprixels interact poorly with multiple planes, and such usage is discouraged.
 This situation might improve in the future.
 
-Multiple threads may not currently call **ncvisual_render** concurrently
+Multiple threads may not currently call **ncvisual_blit** concurrently
 using the same **ncvisual**, even if targeting distinct **ncplane**s. This
 will likely change in the future.
 
diff --git a/include/ncpp/Visual.hh b/include/ncpp/Visual.hh
index 088ebe3e70..e22c8e1390 100644
--- a/include/ncpp/Visual.hh
+++ b/include/ncpp/Visual.hh
@@ -66,9 +66,9 @@ namespace ncpp
 			return ncvisual_decode_loop (visual);
 		}
 
-		ncplane* render (const ncvisual_options* vopts) const NOEXCEPT_MAYBE
+		ncplane* blit (const ncvisual_options* vopts) const NOEXCEPT_MAYBE
 		{
-			return error_guard<ncplane*, ncplane*> (ncvisual_render (get_notcurses (), visual, vopts), nullptr);
+			return error_guard<ncplane*, ncplane*> (ncvisual_blit (get_notcurses (), visual, vopts), nullptr);
 		}
 
 		int stream (const ncvisual_options* vopts, float timescale, ncstreamcb streamer, void *curry = nullptr) const NOEXCEPT_MAYBE
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index e81a136708..6703798f63 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -2889,7 +2889,7 @@ API int ncvisual_decode(struct ncvisual* nc)
   __attribute__ ((nonnull (1)));
 
 // decode the next frame ala ncvisual_decode(), but if we have reached the end,
-// rewind to the first frame of the ncvisual. a subsequent 'ncvisual_render()'
+// rewind to the first frame of the ncvisual. a subsequent 'ncvisual_blit()'
 // will render the first frame, as if the ncvisual had been closed and reopened.
 // the return values remain the same as those of ncvisual_decode().
 API int ncvisual_decode_loop(struct ncvisual* nc)
@@ -2922,35 +2922,65 @@ API int ncvisual_at_yx(const struct ncvisual* n, int y, int x, uint32_t* pixel)
 API int ncvisual_set_yx(const struct ncvisual* n, int y, int x, uint32_t pixel)
   __attribute__ ((nonnull (1)));
 
-// Render the decoded frame to the specified ncplane. If one is not provided,
-// one will be created, having the exact size necessary to display the visual.
-// A subregion of the visual can be rendered using 'begx', 'begy', 'lenx', and
+// Render the decoded frame according to the provided options (which may be
+// NULL). The plane used for rendering depends on vopts->n and vopts->flags. If
+// NCVISUAL_OPTION_CHILDPLANE is set, vopts->n must not be NULL, and the plane
+// will always be created as a child of vopts->n. If this flag is not set, and
+// vopts->n is NULL, a new plane is created as a child of the standard plane.
+// If the flag is not set and vopts->n is not NULL, we render to vopts->n. A
+// subregion of the visual can be rendered using 'begx', 'begy', 'lenx', and
 // 'leny'. Negative values for 'begy' or 'begx' are an error. It is an error to
-// specify any region beyond the boundaries of the frame. Returns the
-// (possibly newly-created) plane to which we drew.
+// specify any region beyond the boundaries of the frame. Returns the (possibly
+// newly-created) plane to which we drew. Pixels may not be blitted to the
+// standard plane.
 API struct ncplane* ncvisual_render(struct notcurses* nc, struct ncvisual* ncv,
                                     const struct ncvisual_options* vopts)
+  __attribute__ ((deprecated)) __attribute__ ((nonnull (2)));
+
+// Render the decoded frame according to the provided options (which may be
+// NULL). The plane used for rendering depends on vopts->n and vopts->flags.
+// If NCVISUAL_OPTION_CHILDPLANE is set, vopts->n must not be NULL, and the
+// plane will always be created as a child of vopts->n. If this flag is not
+// set, and vopts->n is NULL, a new plane is created as root of a new pile.
+// If the flag is not set and vopts->n is not NULL, we render to vopts->n.
+// A subregion of the visual can be rendered using 'begx', 'begy', 'lenx', and
+// 'leny'. Negative values for 'begy' or 'begx' are an error. It is an error to
+// specify any region beyond the boundaries of the frame. Returns the (possibly
+// newly-created) plane to which we drew. Pixels may not be blitted to the
+// standard plane.
+API struct ncplane* ncvisual_blit(struct notcurses* nc, struct ncvisual* ncv,
+                                  const struct ncvisual_options* vopts)
   __attribute__ ((nonnull (2)));
 
+// Create a new plane as prescribed in opts, either as a child of 'vopts->n',
+// or the root of a new pile if 'vopts->n' is NULL (or 'vopts' itself is NULL).
+// Blit 'ncv' to the created plane according to 'vopts'. If 'vopts->n' is
+// non-NULL, NCVISUAL_OPTION_CHILDPLANE must be supplied.
 __attribute__ ((nonnull (1, 2, 3))) static inline struct ncplane*
-ncvisualplane_create(struct ncplane* n, const struct ncplane_options* opts,
+ncvisualplane_create(struct notcurses* nc, const struct ncplane_options* opts,
                      struct ncvisual* ncv, struct ncvisual_options* vopts){
-  if(vopts && vopts->n){ // the whole point is to create a new plane
+  struct ncplane* newn;
+  if(vopts && vopts->n){
+    if(vopts->flags & NCVISUAL_OPTION_CHILDPLANE){
+      return NULL; // the whole point is to create a new plane
+    }
+    newn = ncplane_create(vopts->n, opts);
+  }else{
+    newn = ncpile_create(nc, opts);
+  }
+  if(newn == NULL){
     return NULL;
   }
-  struct ncplane* newn = ncplane_create(n, opts);
-  if(newn){
-    struct ncvisual_options v;
-    if(!vopts){
-      vopts = &v;
-      memset(vopts, 0, sizeof(*vopts));
-    }
-    vopts->n = newn;
-    if(ncvisual_render(ncplane_notcurses(n), ncv, vopts) == NULL){
-      ncplane_destroy(newn);
-      vopts->n = NULL;
-      return NULL;
-    }
+  struct ncvisual_options v;
+  if(!vopts){
+    vopts = &v;
+    memset(vopts, 0, sizeof(*vopts));
+  }
+  vopts->n = newn;
+  if(ncvisual_blit(nc, ncv, vopts) == NULL){
+    ncplane_destroy(newn);
+    vopts->n = NULL;
+    return NULL;
   }
   return newn;
 }
@@ -3022,7 +3052,7 @@ API int ncblit_rgb_loose(const void* data, int linesize,
 
 // The ncpixel API facilitates direct management of the pixels within an
 // ncvisual (ncvisuals keep a backing store of 32-bit RGBA pixels, and render
-// them down to terminal graphics in ncvisual_render()).
+// them down to terminal graphics in ncvisual_blit()).
 //
 // Per libav, we "store as BGRA on little-endian, and ARGB on big-endian".
 // This is an RGBA *byte-order* scheme. libav emits bytes, not words. Those
diff --git a/src/demo/boxdemo.c b/src/demo/boxdemo.c
index 0a9fdb43a5..bd1ddecf20 100644
--- a/src/demo/boxdemo.c
+++ b/src/demo/boxdemo.c
@@ -135,13 +135,14 @@ get_ships(struct notcurses* nc, struct ship* ships, unsigned shipcount){
     return -1;
   }
   struct ncvisual_options vopts = {
+    .n = notcurses_stdplane(nc),
     .y = 30,//rand() % (ncplane_dim_y(notcurses_stdplane_const(nc)) - SHIPHEIGHT),
     .x = 30,//rand() % (ncplane_dim_x(notcurses_stdplane_const(nc)) - SHIPWIDTH),
     .blitter = NCBLIT_PIXEL,
-    .flags = NCVISUAL_OPTION_NODEGRADE,
+    .flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE,
   };
   for(unsigned s = 0 ; s < shipcount ; ++s){
-    if((ships[s].n = ncvisual_render(nc, wmv, &vopts)) == NULL){
+    if((ships[s].n = ncvisual_blit(nc, wmv, &vopts)) == NULL){
       while(s--){
         ncplane_destroy(ships[s].n);
         ncvisual_destroy(wmv);
diff --git a/src/demo/chunli.c b/src/demo/chunli.c
index ec1b000bda..51adb34304 100644
--- a/src/demo/chunli.c
+++ b/src/demo/chunli.c
@@ -23,7 +23,11 @@ chunli_draw(struct notcurses* nc, const char* ext, int count, const nccell* b){
     if(chuns[i].ncv == NULL){
       return -1;
     }
-    if((chuns[i].n = ncvisual_render(nc, chuns[i].ncv, NULL)) == NULL){
+    struct ncvisual_options vopts = {
+      .n = notcurses_stdplane(nc),
+      .flags = NCVISUAL_OPTION_CHILDPLANE,
+    };
+    if((chuns[i].n = ncvisual_blit(nc, chuns[i].ncv, &vopts)) == NULL){
       return -1;
     }
     ncplane_set_base_cell(chuns[i].n, b);
@@ -68,7 +72,11 @@ int chunli_demo(struct notcurses* nc){
     }
     free(path);
     struct ncplane* ncp;
-    if((ncp = ncvisual_render(nc, ncv, NULL)) == NULL){
+    struct ncvisual_options vopts = {
+      .n = notcurses_stdplane(nc),
+      .flags = NCVISUAL_OPTION_CHILDPLANE,
+    };
+    if((ncp = ncvisual_blit(nc, ncv, &vopts)) == NULL){
       return -1;
     }
     ncplane_set_base_cell(ncp, &b);
diff --git a/src/demo/dragon.c b/src/demo/dragon.c
index 18a41b0aa3..bcb96c3e10 100644
--- a/src/demo/dragon.c
+++ b/src/demo/dragon.c
@@ -101,7 +101,7 @@ int dragon_demo(struct notcurses* nc){
       .y = 1,
       .scaling = NCSCALE_STRETCH,
     };
-    if(ncvisual_render(nc, ncv, &vopts) == NULL){
+    if(ncvisual_blit(nc, ncv, &vopts) == NULL){
       ncvisual_destroy(ncv);
       return -1;
     }
diff --git a/src/demo/eagle.c b/src/demo/eagle.c
index 13e5cda8b6..c38ae1d277 100644
--- a/src/demo/eagle.c
+++ b/src/demo/eagle.c
@@ -76,7 +76,7 @@ zoom_map(struct notcurses* nc, const char* map, int* ret){
   vwidth /= xscale;
   vopts.n = zncp;
   vopts.scaling = NCSCALE_STRETCH;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL || (*ret = demo_render(nc))){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL || (*ret = demo_render(nc))){
     ncvisual_destroy(ncv);
     ncplane_destroy(zncp);
     return NULL;
@@ -99,7 +99,7 @@ zoom_map(struct notcurses* nc, const char* map, int* ret){
     if((truex += deltx) > vwidth){
       truex = vwidth;
     }
-    if(ncvisual_render(nc, ncv, &vopts) == NULL){
+    if(ncvisual_blit(nc, ncv, &vopts) == NULL){
       ncvisual_destroy(ncv);
       ncplane_destroy(zncp);
       return NULL;
diff --git a/src/demo/fission.c b/src/demo/fission.c
index db72ed0b9b..63e6dbd9ee 100644
--- a/src/demo/fission.c
+++ b/src/demo/fission.c
@@ -206,8 +206,9 @@ int fission_demo(struct notcurses* nc){
   struct ncvisual_options vopts = {
     .n = stdn,
     .scaling = NCSCALE_STRETCH,
+    .flags = NCVISUAL_OPTION_CHILDPLANE,
   };
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     ncvisual_destroy(ncv);
     goto err;
   }
diff --git a/src/demo/hud.c b/src/demo/hud.c
index 112641160c..f7bfccadbe 100644
--- a/src/demo/hud.c
+++ b/src/demo/hud.c
@@ -29,6 +29,8 @@ static int plot_grab_y = -1;
 // position of the plot *when grab started*
 static int plot_pos_y;
 
+#define FPSHZ 2
+
 #define FPSGRAPH_MAX_COLS 72 // give it some room on each side of an 80-column term
 
 // how many columns for runtime?
@@ -585,7 +587,7 @@ int demo_render(struct notcurses* nc){
     if(!plot_hidden){
       ncplane_move_family_top(ncuplot_plane(plot));
     }
-    uint64_t ns = (timespec_to_ns(&ts) - plottimestart) / NANOSECS_IN_SEC;
+    uint64_t ns = (timespec_to_ns(&ts) - plottimestart) / (NANOSECS_IN_SEC / FPSHZ);
     ncuplot_add_sample(plot, ns, 1);
   }
   if(menu){
@@ -664,7 +666,7 @@ int fpsgraph_init(struct notcurses* nc){
                NCPLOT_OPTION_PRINTSAMPLE;
   opts.gridtype = NCBLIT_BRAILLE;
   opts.legendstyle = NCSTYLE_ITALIC | NCSTYLE_BOLD;
-  opts.title = "frames per second";
+  opts.title = "frames per semisecond";
   ncchannels_set_fg_rgb8(&opts.minchannels, 0x80, 0x80, 0xff);
   ncchannels_set_bg_rgb(&opts.minchannels, 0x201020);
   ncchannels_set_bg_alpha(&opts.minchannels, NCALPHA_BLEND);
diff --git a/src/demo/intro.c b/src/demo/intro.c
index 6e88cea860..3ebfca08ab 100644
--- a/src/demo/intro.c
+++ b/src/demo/intro.c
@@ -58,7 +58,7 @@ greatscott(struct notcurses* nc, int dimx){
     .scaling = NCSCALE_STRETCH,
     .flags = NCVISUAL_OPTION_NODEGRADE,
   };
-  struct ncplane* ret = ncvisual_render(nc, ncv, &vopts);
+  struct ncplane* ret = ncvisual_blit(nc, ncv, &vopts);
   ncvisual_destroy(ncv);
   return ret;
 }
@@ -80,7 +80,7 @@ orcashow(struct notcurses* nc, int dimy, int dimx){
   };
   int cellpxy, cellpxx;
   ncplane_pixelgeom(notcurses_stdplane_const(nc), NULL, NULL,
-                    &cellpxy, &cellpxx, NULL, NULL);
+                     &cellpxy, &cellpxx, NULL, NULL);
   int odimy, odimx, scaley, scalex;
   ncvisual_blitter_geom(nc, ncv, &vopts, &odimy, &odimx, &scaley, &scalex, NULL);
   // even if we can't do pixel output, we want her the same size as if weu
@@ -108,7 +108,7 @@ orcashow(struct notcurses* nc, int dimy, int dimx){
     return NULL;
   }
   vopts.n = n;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     ncplane_destroy(n);
     ncvisual_destroy(ncv);
   }
diff --git a/src/demo/keller.c b/src/demo/keller.c
index 9b7e4364aa..cc5daf3397 100644
--- a/src/demo/keller.c
+++ b/src/demo/keller.c
@@ -23,10 +23,13 @@ visualize(struct notcurses* nc, struct ncvisual* ncv){
   ncplane_set_base(stdn, "", 0, channels);
   for(size_t i = 0 ; i < sizeof(bs) / sizeof(*bs) ; ++i){
     struct ncvisual_options vopts = {
+      .n = notcurses_stdplane(nc),
       .scaling = NCSCALE_SCALE,
       .blitter = bs[i],
-      .flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_HORALIGNED
-                | NCVISUAL_OPTION_VERALIGNED,
+      .flags = NCVISUAL_OPTION_NODEGRADE
+                | NCVISUAL_OPTION_HORALIGNED
+                | NCVISUAL_OPTION_VERALIGNED
+                | NCVISUAL_OPTION_CHILDPLANE,
     };
     int scalex, scaley, truey, truex;
     vopts.x = NCALIGN_CENTER;
@@ -34,7 +37,7 @@ visualize(struct notcurses* nc, struct ncvisual* ncv){
     ncplane_erase(stdn); // to clear out old text
     struct ncplane* n = NULL;
     if(ncvisual_blitter_geom(nc, ncv, &vopts, &truey, &truex, &scaley, &scalex, NULL) == 0){
-      if( (n = ncvisual_render(nc, ncv, &vopts)) ){
+      if( (n = ncvisual_blit(nc, ncv, &vopts)) ){
         ncplane_move_below(n, stdn);
         ncplane_printf_aligned(stdn, ncplane_dim_y(stdn) / 2 - 1, NCALIGN_CENTER,
                               "%03dx%03d", truex, truey);
diff --git a/src/demo/luigi.c b/src/demo/luigi.c
index d8cc5b623e..e1f9d7fa0e 100644
--- a/src/demo/luigi.c
+++ b/src/demo/luigi.c
@@ -158,7 +158,7 @@ int luigi_demo(struct notcurses* nc){
     .scaling = NCSCALE_STRETCH,
     .flags = NCVISUAL_OPTION_NOINTERPOLATE,
   };
-  if(ncvisual_render(nc, nv, &vopts) == NULL){
+  if(ncvisual_blit(nc, nv, &vopts) == NULL){
     return -1;
   }
   assert(1 == ncvisual_decode(nv));
@@ -200,7 +200,11 @@ int luigi_demo(struct notcurses* nc){
   uint64_t channels = 0;
   ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
-  struct ncplane* wmplane = ncvisual_render(nc, wmncv, NULL);
+  struct ncvisual_options wvopts = {
+    .n = notcurses_stdplane(nc),
+    .flags = NCVISUAL_OPTION_CHILDPLANE,
+  };
+  struct ncplane* wmplane = ncvisual_blit(nc, wmncv, &wvopts);
   if(wmplane == NULL){
     ncvisual_destroy(wmncv);
     return -1;
diff --git a/src/demo/normal.c b/src/demo/normal.c
index f2224adf9b..571c5c818c 100644
--- a/src/demo/normal.c
+++ b/src/demo/normal.c
@@ -84,7 +84,7 @@ rotate_visual(struct notcurses* nc, struct ncplane* n, int dy, int dx){
           .n = stdn,
           .scaling = NCSCALE_STRETCH,
         };
-        if(ncvisual_render(nc, nncv, &nvopts) == NULL){
+        if(ncvisual_blit(nc, nncv, &nvopts) == NULL){
           return -1;
         }
       }
@@ -97,11 +97,14 @@ rotate_visual(struct notcurses* nc, struct ncplane* n, int dy, int dx){
       r = -1;
       break;
     }
+    vopts.n = notcurses_stdplane(nc);
     vopts.x = NCALIGN_CENTER;
     vopts.y = NCALIGN_CENTER;
-    vopts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED;
+    vopts.flags |= NCVISUAL_OPTION_HORALIGNED
+                   | NCVISUAL_OPTION_VERALIGNED
+                   | NCVISUAL_OPTION_CHILDPLANE;
     struct ncplane* newn;
-    if((newn = ncvisual_render(nc, ncv, &vopts)) == NULL){
+    if((newn = ncvisual_blit(nc, ncv, &vopts)) == NULL){
       r = -1;
       break;
     }
diff --git a/src/demo/outro.c b/src/demo/outro.c
index 2c2124df5c..eba8eabdac 100644
--- a/src/demo/outro.c
+++ b/src/demo/outro.c
@@ -59,9 +59,10 @@ videothread(void* vnc){
     .n = ncp,
     .y = 1,
     .flags = NCVISUAL_OPTION_ADDALPHA,
+    .blitter = NCBLIT_PIXEL,
   };
   int three = 3;
-  if(ncvisual_render(nc, ncv, &ovopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &ovopts) == NULL){
     return NULL;
   }
   struct timespec fade;
@@ -197,8 +198,9 @@ int outro(struct notcurses* nc){
       return -1;
     }
     vopts.scaling = NCSCALE_STRETCH;
-    vopts.flags = NCVISUAL_OPTION_BLEND;
-    if((vopts.n = ncvisual_render(nc, chncv, &vopts)) == NULL){
+    vopts.flags = NCVISUAL_OPTION_BLEND | NCVISUAL_OPTION_CHILDPLANE;
+    vopts.n = notcurses_stdplane(nc);
+    if((vopts.n = ncvisual_blit(nc, chncv, &vopts)) == NULL){
       ncvisual_destroy(chncv);
       return -1;
     }
diff --git a/src/demo/view.c b/src/demo/view.c
index 1debc10fd9..f4bdf5709a 100644
--- a/src/demo/view.c
+++ b/src/demo/view.c
@@ -26,7 +26,7 @@ streamer(struct ncvisual* ncv, struct ncvisual_options* vopts,
       marsh.pipopts.scaling = NCSCALE_STRETCH;
     }
     if(marsh.pipopts.n){
-      ncvisual_render(ncplane_notcurses(marsh.pipopts.n), ncv, &marsh.pipopts);
+      ncvisual_blit(ncplane_notcurses(marsh.pipopts.n), ncv, &marsh.pipopts);
       ncplane_move_above(marsh.pipopts.n, vopts->n);
     }
   }
@@ -130,7 +130,7 @@ view_images(struct notcurses* nc, struct ncplane* nstd, int dimy, int dimx){
     .scaling = NCSCALE_STRETCH,
     .y = 1,
   };
-  if(ncvisual_render(nc, ncv2, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv2, &vopts) == NULL){
     ncvisual_destroy(ncv2);
     ncplane_destroy(dsplane);
     return -1;
@@ -153,7 +153,7 @@ view_images(struct notcurses* nc, struct ncplane* nstd, int dimy, int dimx){
   }
   free(pic);
   vopts.n = notcurses_stdplane(nc);
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     ncvisual_destroy(ncv);
     ncplane_destroy(dsplane);
     return -1;
diff --git a/src/demo/xray.c b/src/demo/xray.c
index 63d07bbc6f..eca6ce6266 100644
--- a/src/demo/xray.c
+++ b/src/demo/xray.c
@@ -101,7 +101,7 @@ get_next_frame(struct ncvisual* ncv, struct ncvisual_options* vopts){
   ret = marsh.next_frame++;
   if(ncvisual_decode(ncv)){
     ret = -1;
-  }else if(ncvisual_render(marsh.nc, ncv, vopts) == NULL){
+  }else if(ncvisual_blit(marsh.nc, ncv, vopts) == NULL){
     ret = -1;
   }
   pthread_mutex_unlock(&lock);
diff --git a/src/demo/yield.c b/src/demo/yield.c
index 8417762166..518cdbd199 100644
--- a/src/demo/yield.c
+++ b/src/demo/yield.c
@@ -39,7 +39,7 @@ int yield_demo(struct notcurses* nc){
     ncplane_destroy(vopts.n);
     return -1;
   }
-  if(ncvisual_render(nc, wmv, &vopts) == NULL){
+  if(ncvisual_blit(nc, wmv, &vopts) == NULL){
     ncvisual_destroy(wmv);
     ncplane_destroy(vopts.n);
     return -1;
@@ -111,7 +111,7 @@ int yield_demo(struct notcurses* nc){
       DEMO_RENDER(nc);
     }while(pfilled == 0);
     tfilled += pfilled;
-    if(ncvisual_render(nc, wmv, &vopts) == NULL){
+    if(ncvisual_blit(nc, wmv, &vopts) == NULL){
       ncvisual_destroy(wmv);
       ncplane_destroy(vopts.n);
       return -1;
diff --git a/src/demo/zoo.c b/src/demo/zoo.c
index f75fa6e474..3c693ae3ac 100644
--- a/src/demo/zoo.c
+++ b/src/demo/zoo.c
@@ -14,8 +14,9 @@ draw_background(struct notcurses* nc){
     struct ncvisual_options vopts = {
       .scaling = NCSCALE_STRETCH,
       .n = n,
+      .flags = NCVISUAL_OPTION_CHILDPLANE,
     };
-    if(ncvisual_render(nc, ncv, &vopts) == NULL){
+    if(ncvisual_blit(nc, ncv, &vopts) == NULL){
       ncvisual_destroy(ncv);
       return -1;
     }
@@ -27,7 +28,7 @@ draw_background(struct notcurses* nc){
 
 // we list all distributions on which notcurses is known to exist
 static struct ncselector_item select_items[] = {
-#define SITEM(s, l) { s, l, }
+#define SITEM(short, long) { short, long, }
   SITEM("fbsd", "FreeBSD"),
   SITEM("deb", "Debian Unstable Linux"),
   SITEM("rpm", "Fedora Rawhide Linux"),
diff --git a/src/fetch/main.c b/src/fetch/main.c
index 6a112f1a3e..ecf522569c 100644
--- a/src/fetch/main.c
+++ b/src/fetch/main.c
@@ -607,12 +607,13 @@ display_thread(void* vmarshal){
         pixeling = true;
       }
       struct ncvisual_options vopts = {
+        .n = notcurses_stdplane(m->nc),
         .x = NCALIGN_CENTER,
         .blitter = pixeling ? NCBLIT_PIXEL : NCBLIT_3x2,
         .scaling = pixeling ? NCSCALE_NONE : NCSCALE_SCALE_HIRES,
-        .flags = NCVISUAL_OPTION_HORALIGNED,
+        .flags = NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_CHILDPLANE,
       };
-      struct ncplane* iplane = ncvisual_render(m->nc, ncv, &vopts);
+      struct ncplane* iplane = ncvisual_blit(m->nc, ncv, &vopts);
       ncvisual_destroy(ncv);
       if(iplane){
         int x = ncplane_x(iplane);
diff --git a/src/info/main.c b/src/info/main.c
index cdbda53a9c..edbc1d1d4e 100644
--- a/src/info/main.c
+++ b/src/info/main.c
@@ -104,7 +104,21 @@ finish_line(struct ncplane* n){
 
 static int
 emoji_viz(struct ncplane* n){
-  static const char emoji[] = "👾🏴🤘🚬🌍🌎🌏🥆💣🗡🔫⚗️⚛️☢️☣️"
+  static const char emoji[] = "\U0001f47e" // alien monster
+                              "\U0001f3f4" // waving black flag
+                              "\U0001f918" // sign of the horns
+                              "\U0001f6ac" // cigarette, delicious
+                              "\U0001f30d" // globe europe/africa
+                              "\U0001f30e" // globe americas
+                              "\U0001f30f" // globe asia/australia
+                              "\U0001F946" // rifle
+                              "\U0001f4a3" // bomb
+                              "\U0001f5e1" // dagger
+                              "\U0001F52B" // pistol
+                              "\u2697\ufe0f" // alembic
+                              "\u269b\ufe0f" // atom
+                              "\u2622\ufe0f" // radiation sign
+                              "\u2623\ufe0f" // biohazard
                               "\U0001F33F" // herb
                               "\U0001F3B1" // billiards
                               "\U0001F3E7" // automated teller machine
@@ -114,6 +128,7 @@ emoji_viz(struct ncplane* n){
                               "\U0001F4E1" // satellite antenna
                               "\U0001F93B" // modern pentathlon
                               "\U0001F991" // squid
+                              "\U0001f1e6\U0001f1f6" // regional indicators AQ (antarctica)
                               "\U0001f469\u200d\U0001f52c" // woman scientist
                               "\U0001faa4" // mouse trap
                               "\U0001f6b1" // non-potable water
@@ -332,7 +347,7 @@ display_logo(struct ncplane* n, const char* path){
     .blitter = NCBLIT_PIXEL,
     .flags = NCVISUAL_OPTION_CHILDPLANE | NCVISUAL_OPTION_NODEGRADE,
   };
-  struct ncplane* bitm = ncvisual_render(ncplane_notcurses(n), ncv, &vopts);
+  struct ncplane* bitm = ncvisual_blit(ncplane_notcurses(n), ncv, &vopts);
   if(bitm == NULL){
     ncvisual_destroy(ncv);
     return -1;
diff --git a/src/lib/direct.c b/src/lib/direct.c
index f303a9b32a..3033bea196 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -737,7 +737,7 @@ ncdirect_render_visual(ncdirect* n, ncvisual* ncv,
     }
     ncdv->sprite = bargs.u.pixel.spx;
   }
-  if(ncvisual_blit(ncv, disprows, dispcols, ncdv, bset, &bargs)){
+  if(ncvisual_blit_internal(ncv, disprows, dispcols, ncdv, bset, &bargs)){
     free_plane(ncdv);
     return NULL;
   }
diff --git a/src/lib/fill.c b/src/lib/fill.c
index 2af185e53a..4679fad17b 100644
--- a/src/lib/fill.c
+++ b/src/lib/fill.c
@@ -641,7 +641,7 @@ int ncplane_qrcode(ncplane* n, int* ymax, int* xmax, const void* data, size_t le
           .n = n,
           .blitter = blitfxn,
         };
-        if(ncvisual_render(ncplane_notcurses(n), ncv, &vopts) == n){
+        if(ncvisual_blit(ncplane_notcurses(n), ncv, &vopts) == n){
           ret = square;
         }
         ncvisual_blitter_geom(ncplane_notcurses(n), ncv, &vopts, NULL, NULL,
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 01e86ac424..3b88b463c1 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -993,7 +993,7 @@ calc_gradient_channels(uint64_t* channels, uint64_t ul, uint64_t ur,
 }
 
 // ncdirect needs to "fake" an isolated ncplane as a drawing surface for
-// ncvisual_render(), and thus calls these low-level internal functions.
+// ncvisual_blit(), and thus calls these low-level internal functions.
 // they are not for general use -- check ncplane_new() and ncplane_destroy().
 ncplane* ncplane_new_internal(notcurses* nc, ncplane* n, const ncplane_options* nopts);
 
@@ -1004,9 +1004,9 @@ ALLOC char* ncplane_vprintf_prep(const char* format, va_list ap);
 
 // Resize the provided ncvisual to the specified 'rows' x 'cols', but do not
 // change the internals of the ncvisual. Uses oframe.
-int ncvisual_blit(struct ncvisual* ncv, int rows, int cols,
-                  ncplane* n, const struct blitset* bset,
-                  const blitterargs* bargs);
+int ncvisual_blit_internal(struct ncvisual* ncv, int rows, int cols,
+                           ncplane* n, const struct blitset* bset,
+                           const blitterargs* bargs);
 
 static inline int
 tty_emit(const char* seq, int fd){
diff --git a/src/lib/plot.c b/src/lib/plot.c
index f52981fcf1..9d993a0d60 100644
--- a/src/lib/plot.c
+++ b/src/lib/plot.c
@@ -220,7 +220,7 @@ int redraw_pixelplot_##T(nc##X##plot* ncp){ \
     .blitter = NCBLIT_PIXEL, \
     .flags = NCVISUAL_OPTION_NODEGRADE, \
   }; \
-  if(ncvisual_render(ncplane_notcurses(ncp->plot.ncp), ncv, &vopts) == NULL){ \
+  if(ncvisual_blit(ncplane_notcurses(ncp->plot.ncp), ncv, &vopts) == NULL){ \
     ncvisual_destroy(ncv); \
     return -1; \
   } \
diff --git a/src/lib/visual.c b/src/lib/visual.c
index 337d0e5b3f..fade30e6f6 100644
--- a/src/lib/visual.c
+++ b/src/lib/visual.c
@@ -32,7 +32,7 @@ void ncvisual_printbanner(fbuf* f){
 
 // you need an actual multimedia implementation for functions which work with
 // codecs, including ncvisual_decode(), ncvisual_decode_loop(),
-// ncvisual_from_file(), ncvisual_stream(), and ncvisual_subtitle().
+// ncvisual_from_file(), ncvisual_stream(), and ncvisual_subtitle_plane().
 int ncvisual_decode(ncvisual* nc){
   if(!visual_implementation.visual_decode){
     return -1;
@@ -75,8 +75,8 @@ char* ncvisual_subtitle(const ncvisual* ncv){
   return NULL;
 }
 
-int ncvisual_blit(ncvisual* ncv, int rows, int cols, ncplane* n,
-                  const struct blitset* bset, const blitterargs* barg){
+int ncvisual_blit_internal(ncvisual* ncv, int rows, int cols, ncplane* n,
+                           const struct blitset* bset, const blitterargs* barg){
   if(!(barg->flags & NCVISUAL_OPTION_NOINTERPOLATE)){
     if(visual_implementation.visual_blit){
       if(visual_implementation.visual_blit(ncv, rows, cols, n, bset, barg) < 0){
@@ -809,14 +809,15 @@ ncplane* ncvisual_render_cells(notcurses* nc, ncvisual* ncv, const struct blitse
   ncplane* createdn = NULL;
 //fprintf(stderr, "INPUT N: %p\n", n);
   if(n == NULL || (flags & NCVISUAL_OPTION_CHILDPLANE)){ // create plane
-    if(n == NULL){
-      n = notcurses_stdplane(nc);
-    }
     if(scaling == NCSCALE_NONE || scaling == NCSCALE_NONE_HIRES){
       dispcols = lenx;
       disprows = leny;
     }else{
-      ncplane_dim_yx(n, &disprows, &dispcols);
+      if(n == NULL){
+        ncplane_dim_yx(notcurses_stdplane(nc), &disprows, &dispcols);
+      }else{
+        ncplane_dim_yx(n, &disprows, &dispcols);
+      }
       dispcols *= encoding_x_scale(&nc->tcache, bset);
       disprows *= encoding_y_scale(&nc->tcache, bset);
       if(scaling == NCSCALE_SCALE || scaling == NCSCALE_SCALE_HIRES){
@@ -842,7 +843,12 @@ ncplane* ncvisual_render_cells(notcurses* nc, ncvisual* ncv, const struct blitse
     if(flags & NCVISUAL_OPTION_VERALIGNED){
       nopts.flags |= NCPLANE_OPTION_VERALIGNED;
     }
-    if((n = ncplane_create(n, &nopts)) == NULL){
+    if(n){
+      n = ncplane_create(n, &nopts);
+    }else{
+      n = ncpile_create(nc, &nopts);
+    }
+    if(n == NULL){
       return NULL;
     }
     createdn = n;
@@ -887,7 +893,7 @@ ncplane* ncvisual_render_cells(notcurses* nc, ncvisual* ncv, const struct blitse
   bargs.flags = flags;
   bargs.u.cell.placey = placey;
   bargs.u.cell.placex = placex;
-  if(ncvisual_blit(ncv, disprows, dispcols, n, bset, &bargs)){
+  if(ncvisual_blit_internal(ncv, disprows, dispcols, n, bset, &bargs)){
     ncplane_destroy(createdn);
     return NULL;
   }
@@ -909,7 +915,11 @@ make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
                    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);
+    if(parent == NULL){
+      ncplane_dim_yx(notcurses_stdplane(nc), disppixy, disppixx);
+    }else{
+      ncplane_dim_yx(parent, disppixy, disppixx);
+    }
     // FIXME why do we clamp only vertical, not horizontal, here?
     if(*placey + *disppixy >= ncplane_dim_y(notcurses_stdplane_const(nc))){
       *disppixy = ncplane_dim_y(notcurses_stdplane_const(nc)) - *placey;
@@ -948,8 +958,12 @@ make_sprixel_plane(notcurses* nc, ncplane* parent, ncvisual* ncv,
            | ((flags & NCVISUAL_OPTION_VERALIGNED) ? NCPLANE_OPTION_VERALIGNED : 0),
   };
 //fprintf(stderr, "PLACING NEW PLANE: %d/%d @ %d/%d 0x%016lx\n", nopts.rows, nopts.cols, nopts.y, nopts.x, nopts.flags);
-  // FIXME might need some particular pile
-  ncplane* n = ncplane_create(parent, &nopts);
+  ncplane* n;
+  if(parent == NULL){
+    n = ncpile_create(nc, &nopts);
+  }else{
+    n = ncplane_create(parent, &nopts);
+  }
   if(n == NULL){
     return NULL;
   }
@@ -993,9 +1007,6 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   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, &outx, &placey, &placex,
                                       pxoffy, pxoffx)) == NULL){
@@ -1080,7 +1091,7 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   // at this point, disppixy/disppixx are the output geometry (might be larger
   // than scaledy/scaledx for sixel), while scaledy/scaledx are the scaled
   // geometry. cells occupied are determined based off disppixy/disppixx.
-  if(ncvisual_blit(ncv, disppixy, disppixx, n, bset, &bargs)){
+  if(ncvisual_blit_internal(ncv, disppixy, disppixx, n, bset, &bargs)){
     ncplane_destroy(createdn);
     return NULL;
   }
@@ -1123,7 +1134,7 @@ ncplane* ncvisual_render_pixels(notcurses* nc, ncvisual* ncv, const struct blits
   return n;
 }
 
-ncplane* ncvisual_render(notcurses* nc, ncvisual* ncv, const struct ncvisual_options* vopts){
+ncplane* ncvisual_blit(notcurses* nc, ncvisual* ncv, const struct ncvisual_options* vopts){
   const struct blitset* bset;
   int leny, lenx;
   if(ncvisual_blitset_geom(nc, &nc->tcache, ncv, vopts, NULL, NULL, NULL, NULL,
@@ -1157,6 +1168,23 @@ ncplane* ncvisual_render(notcurses* nc, ncvisual* ncv, const struct ncvisual_opt
   return n;
 }
 
+// compatability wrapper around ncvisual_blit that provides the standard plane
+// plus NCVISUAL_OPTION_CHILDPLANE if vopts->n is NULL.
+ncplane* ncvisual_render(notcurses* nc, ncvisual* ncv, const struct ncvisual_options* vopts){
+  struct ncvisual_options fakevopts;
+  if(vopts == NULL){
+    memset(&fakevopts, 0, sizeof(fakevopts));
+  }else{
+    memcpy(&fakevopts, vopts, sizeof(fakevopts));
+  }
+  vopts = &fakevopts;
+  if(vopts->n == NULL){
+    fakevopts.n = notcurses_stdplane(nc);
+    fakevopts.flags |= NCVISUAL_OPTION_CHILDPLANE;
+  }
+  return ncvisual_blit(nc, ncv, vopts);
+}
+
 ncvisual* ncvisual_from_plane(const ncplane* n, ncblitter_e blit, int begy, int begx,
                               int leny, int lenx){
   int py, px;
diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c
index fe521a8ab9..dba857949b 100644
--- a/src/media/ffmpeg.c
+++ b/src/media/ffmpeg.c
@@ -200,7 +200,7 @@ struct ncplane* ffmpeg_subtitle(ncplane* parent, const ncvisual* ncv){
         .blitter = NCBLIT_PIXEL,
         .scaling = NCSCALE_STRETCH,
       };
-      if(ncvisual_render(nc, v, &vopts) == NULL){
+      if(ncvisual_blit(nc, v, &vopts) == NULL){
         ncplane_destroy(vn);
         ncvisual_destroy(v);
         return NULL;
@@ -501,7 +501,7 @@ int ffmpeg_stream(notcurses* nc, ncvisual* ncv, float timescale,
     // was actually rendered with
     ncvisual_blitter_geom(nc, ncv, &activevopts, NULL, NULL, NULL, NULL,
                           &activevopts.blitter);
-    if((newn = ncvisual_render(nc, ncv, &activevopts)) == NULL){
+    if((newn = ncvisual_blit(nc, ncv, &activevopts)) == NULL){
       if(activevopts.n != vopts->n){
         ncplane_destroy(activevopts.n);
       }
diff --git a/src/media/oiio-indep.c b/src/media/oiio-indep.c
index d7d92fd895..61a2569ea9 100644
--- a/src/media/oiio-indep.c
+++ b/src/media/oiio-indep.c
@@ -28,7 +28,7 @@ int oiio_stream(struct notcurses* nc, ncvisual* ncv, float timescale,
     // decay the blitter explicitly, so that the callback knows the blitter it
     // was actually rendered with
     ncvisual_blitter_geom(nc, ncv, &activevopts, NULL, NULL, NULL, NULL, &activevopts.blitter);
-    if((newn = ncvisual_render(nc, ncv, &activevopts)) == NULL){
+    if((newn = ncvisual_blit(nc, ncv, &activevopts)) == NULL){
       if(activevopts.n != vopts->n){
         ncplane_destroy(activevopts.n);
       }
diff --git a/src/poc/bitmapstates.c b/src/poc/bitmapstates.c
index 775c53cb5e..890049bc76 100644
--- a/src/poc/bitmapstates.c
+++ b/src/poc/bitmapstates.c
@@ -13,7 +13,7 @@ static int
 wipebitmap(struct notcurses* nc){
   int cellpxy, cellpxx;
   ncplane_pixelgeom(notcurses_stdplane(nc), NULL, NULL,
-                    &cellpxy, &cellpxx, NULL, NULL);
+                     &cellpxy, &cellpxx, NULL, NULL);
   int pixy = cellpxy * 6;
   int pixx = cellpxx * 6;
   uint32_t pixels[pixx * pixy];
@@ -27,8 +27,10 @@ wipebitmap(struct notcurses* nc){
   }
   struct ncvisual_options vopts = {
     .blitter = NCBLIT_PIXEL,
+    .n = notcurses_stdplane(nc),
+    .flags = NCVISUAL_OPTION_CHILDPLANE,
   };
-  struct ncplane* n = ncvisual_render(nc, ncv, &vopts);
+  struct ncplane* n = ncvisual_blit(nc, ncv, &vopts);
   if(n == NULL){
     return -1;
   }
@@ -84,7 +86,7 @@ wipebitmap(struct notcurses* nc){
   if(ncve == NULL){
     return -1;
   }
-  if((n = ncvisual_render(nc, ncve, &vopts)) == NULL){
+  if((n = ncvisual_blit(nc, ncve, &vopts)) == NULL){
     return -1;
   }
   emit(notcurses_stdplane(nc), "Ought see empty square");
@@ -96,7 +98,7 @@ wipebitmap(struct notcurses* nc){
 
   // now, actually wipe the middle with *s, and then ensure that a new render
   // gets wiped out before being displayed
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     return -1;
   }
   emit(notcurses_stdplane(nc), "Ought see full square");
@@ -114,7 +116,7 @@ wipebitmap(struct notcurses* nc){
   notcurses_render(nc);
   sleep(2);
 
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     return -1;
   }
   emit(notcurses_stdplane(nc), "Ought *still* see 16 *s");
diff --git a/src/poc/blitters.c b/src/poc/blitters.c
index d8b7080f7f..ca383c46d3 100644
--- a/src/poc/blitters.c
+++ b/src/poc/blitters.c
@@ -45,9 +45,11 @@ int main(int argc, char** argv){
         struct ncvisual_options vopts = {
           .scaling = scaling,
           .blitter = *blitter,
+          .n = notcurses_stdplane(nc),
+          .flags = NCVISUAL_OPTION_CHILDPLANE,
         };
         struct ncplane* cn;
-        if((cn = ncvisual_render(nc, ncv, &vopts)) == NULL){
+        if((cn = ncvisual_blit(nc, ncv, &vopts)) == NULL){
           ncvisual_destroy(ncv);
           goto err;
         }
diff --git a/src/poc/grid.c b/src/poc/grid.c
index 16d8841eb4..a46869bdd9 100644
--- a/src/poc/grid.c
+++ b/src/poc/grid.c
@@ -67,10 +67,11 @@ int main(void){
     return EXIT_FAILURE;
   }
   struct ncvisual_options vopts = {
+    .n = stdn,
     .blitter = NCBLIT_PIXEL,
-    .flags = NCVISUAL_OPTION_NODEGRADE,
+    .flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE,
   };
-  struct ncplane* bitn = ncvisual_render(nc, ncv, &vopts);
+  struct ncplane* bitn = ncvisual_blit(nc, ncv, &vopts);
   ncvisual_destroy(ncv);
   if(bitn == NULL){
     notcurses_stop(nc);
diff --git a/src/poc/interp.c b/src/poc/interp.c
index 13f3b7a5cd..a098a0e0ae 100644
--- a/src/poc/interp.c
+++ b/src/poc/interp.c
@@ -19,10 +19,12 @@ interp(struct notcurses* nc, int cellpixy, int cellpixx){
     return -1;
   }
   struct ncvisual_options vopts = {
+    .n = stdn,
     .y = 1,
     .blitter = NCBLIT_PIXEL,
+    .flags = NCVISUAL_OPTION_CHILDPLANE | NCVISUAL_OPTION_NODEGRADE,
   };
-  struct ncplane* ncvp = ncvisual_render(nc, ncv, &vopts);
+  struct ncplane* ncvp = ncvisual_blit(nc, ncv, &vopts);
   if(ncvp == NULL){
     free(randrgb);
     return -1;
@@ -38,7 +40,7 @@ interp(struct notcurses* nc, int cellpixy, int cellpixx){
   vopts.n = scalep;
   vopts.scaling = NCSCALE_STRETCH;
   popts.x += ncplane_dim_x(scalep) + 1;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     free(randrgb);
     return -1;
   }
@@ -46,7 +48,7 @@ interp(struct notcurses* nc, int cellpixy, int cellpixx){
   struct ncplane* scalepni = ncplane_create(stdn, &popts);
   vopts.n = scalepni;
   vopts.flags = NCVISUAL_OPTION_NOINTERPOLATE;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     free(randrgb);
     return -1;
   }
@@ -64,7 +66,7 @@ interp(struct notcurses* nc, int cellpixy, int cellpixx){
   vopts.flags = 0;
   vopts.n = resizep;
   vopts.scaling = NCSCALE_NONE;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     free(randrgb);
     return -1;
   }
@@ -81,7 +83,7 @@ interp(struct notcurses* nc, int cellpixy, int cellpixx){
   if(ncvisual_resize_noninterpolative(ncv, popts.rows * cellpixy, popts.cols * cellpixx)){
     return -1;
   }
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     return -1;
   }
   ncplane_putstr_yx(stdn, 2, 41, "resize(no)");
@@ -108,7 +110,9 @@ int main(void){
     goto err;
   }
   ncinput ni;
-  notcurses_getc_blocking(nc, &ni);
+  do{
+    notcurses_getc_blocking(nc, &ni);
+  }while(ni.id != (uint32_t)-1 && ni.evtype != NCTYPE_RELEASE);
   notcurses_stop(nc);
   return EXIT_SUCCESS;
 
diff --git a/src/poc/multiselect.c b/src/poc/multiselect.c
index cec03ea31c..b976ad966b 100644
--- a/src/poc/multiselect.c
+++ b/src/poc/multiselect.c
@@ -100,7 +100,7 @@ int main(void){
       .scaling = NCSCALE_STRETCH,
       .n = n,
     };
-    if(ncvisual_render(nc, ncv, &vopts) == NULL){
+    if(ncvisual_blit(nc, ncv, &vopts) == NULL){
       goto err;
     }
   }
diff --git a/src/poc/pixel.c b/src/poc/pixel.c
index ea2eca7546..7a66e869ce 100644
--- a/src/poc/pixel.c
+++ b/src/poc/pixel.c
@@ -15,12 +15,14 @@ handle(struct notcurses* nc, const char *fn){
       uint64_t channels = NCCHANNELS_INITIALIZER(rand() % 256, rand() % 256, 100, rand() % 256, 100, 140);
       ncplane_set_base(stdn, "a", 0, channels);
       struct ncvisual_options vopts = {
+        .n = notcurses_stdplane(nc),
         .y = y,
         .x = x,
         .scaling = NCSCALE_NONE_HIRES,
         .blitter = NCBLIT_PIXEL,
+        .flags = NCVISUAL_OPTION_CHILDPLANE | NCVISUAL_OPTION_NODEGRADE,
       };
-      struct ncplane* nv = ncvisual_render(nc, ncv, &vopts);
+      struct ncplane* nv = ncvisual_blit(nc, ncv, &vopts);
       if(nv == NULL){
         ncvisual_destroy(ncv);
         return -1;
diff --git a/src/poc/rotate.c b/src/poc/rotate.c
index ea1369c063..3cb89c020a 100644
--- a/src/poc/rotate.c
+++ b/src/poc/rotate.c
@@ -43,7 +43,7 @@ int main(int argc, char** argv){
   }
   int scaley, scalex;
   vopts.n = n;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if((ncvisual_blit(nc, ncv, &vopts)) == NULL){
     goto err;
   }
   if(notcurses_render(nc)){
@@ -56,8 +56,7 @@ int main(int argc, char** argv){
   if(ncvisual_resize(ncv, dimy * scaley, dimx * scalex)){
     goto err;
   }
-  vopts.n = n;
-  if(ncvisual_render(nc, ncv, &vopts) == NULL){
+  if(ncvisual_blit(nc, ncv, &vopts) == NULL){
     goto err;
   }
   if(notcurses_render(nc)){
@@ -65,11 +64,12 @@ int main(int argc, char** argv){
   }
   clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL);
 
-  vopts.n = NULL;
   vopts.x = NCALIGN_CENTER;
   vopts.y = NCALIGN_CENTER;
-  vopts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED;
-  ncplane_destroy(n);
+  vopts.flags |= NCVISUAL_OPTION_HORALIGNED
+                 | NCVISUAL_OPTION_VERALIGNED
+                 | NCVISUAL_OPTION_CHILDPLANE;
+  vopts.n = notcurses_stdplane(nc);
   for(double i = 0 ; i < 256 ; ++i){
     clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL);
     if(ncvisual_rotate(ncv, M_PI / ((i / 32) + 2))){
@@ -77,7 +77,7 @@ int main(int argc, char** argv){
       break;
     }
     struct ncplane* newn;
-    if((newn = ncvisual_render(nc, ncv, &vopts)) == NULL){
+    if((newn = ncvisual_blit(nc, ncv, &vopts)) == NULL){
       failed = true;
       break;
     }
diff --git a/src/poc/selector.c b/src/poc/selector.c
index 1e55bbd2c2..ff329db5bd 100644
--- a/src/poc/selector.c
+++ b/src/poc/selector.c
@@ -90,7 +90,7 @@ int main(void){
       .scaling = NCSCALE_STRETCH,
       .n = n,
     };
-    if(ncvisual_render(nc, ncv, &vopts) == NULL){
+    if(ncvisual_blit(nc, ncv, &vopts) == NULL){
       goto err;
     }
     ncvisual_destroy(ncv);
diff --git a/src/poc/statepixel.c b/src/poc/statepixel.c
index 4487926a03..9d71efdf8f 100644
--- a/src/poc/statepixel.c
+++ b/src/poc/statepixel.c
@@ -41,10 +41,11 @@ handle(struct notcurses* nc, const char* fn){
   }
   // render by itself
   struct ncvisual_options vopts = {
+    .n = notcurses_stdplane(nc),
     .blitter = NCBLIT_PIXEL,
-    .flags = NCVISUAL_OPTION_NODEGRADE,
+    .flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE,
   };
-  struct ncplane* n = ncvisual_render(nc, ncv, &vopts);
+  struct ncplane* n = ncvisual_blit(nc, ncv, &vopts);
   if(n == NULL){
     ncvisual_destroy(ncv);
     return -1;
diff --git a/src/pocpp/resize.cpp b/src/pocpp/resize.cpp
index bae398f828..4ee8520ae9 100644
--- a/src/pocpp/resize.cpp
+++ b/src/pocpp/resize.cpp
@@ -30,9 +30,11 @@ int main(int argc, char** argv){
   if(!ncv){
     goto err;
   }
+  vopts.n = notcurses_stdplane(nc);
   vopts.scaling = NCSCALE_STRETCH;
+  vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
   struct ncplane* ntarg;
-  if((ntarg = ncvisual_render(nc, ncv, &vopts)) == nullptr){
+  if((ntarg = ncvisual_blit(nc, ncv, &vopts)) == nullptr){
     goto err;
   }
   if(notcurses_render(nc)){
diff --git a/src/tetris/background.h b/src/tetris/background.h
index 7bea2df01d..ec85179bf7 100644
--- a/src/tetris/background.h
+++ b/src/tetris/background.h
@@ -3,7 +3,9 @@ void DrawBackground(const std::string& s) { // drawn to the standard plane
   backg_->decode();
   ncvisual_options opts{};
   opts.scaling = NCSCALE_STRETCH;
-  backg_->render(&opts);
+  opts.n = *nc_.get_stdplane();
+  opts.flags = NCVISUAL_OPTION_CHILDPLANE;
+  backg_->blit(&opts);
 }
 
 void DrawBoard() { // draw all fixed components of the game


Test patch:
diff --git a/src/tests/bitmap.cpp b/src/tests/bitmap.cpp
index 6a4cb9db09..c222fa7f1f 100644
--- a/src/tests/bitmap.cpp
+++ b/src/tests/bitmap.cpp
@@ -28,10 +28,11 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
     CHECK(0 == ncvisual_resize(ncv, 1, 1));
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
@@ -54,7 +55,7 @@ TEST_CASE("Bitmaps") {
     int maxy, maxx;
     ncplane_pixelgeom(n_, nullptr, nullptr, nullptr, nullptr, &maxy, &maxx);
     CHECK(0 == ncvisual_resize(ncv, maxy, maxx));
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nn == n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
@@ -81,7 +82,7 @@ TEST_CASE("Bitmaps") {
     vopts.n = n;
     vopts.blitter = NCBLIT_PIXEL;
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    CHECK(nullptr == ncvisual_render(nc_, ncv, &vopts));
+    CHECK(nullptr == ncvisual_blit(nc_, ncv, &vopts));
     CHECK(0 == notcurses_render(nc_));
     ncvisual_destroy(ncv);
     CHECK(0 == ncplane_destroy(n));
@@ -104,7 +105,7 @@ TEST_CASE("Bitmaps") {
     vopts.n = n;
     vopts.blitter = NCBLIT_PIXEL;
     vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    CHECK(nullptr == ncvisual_render(nc_, ncv, &vopts));
+    CHECK(nullptr == ncvisual_blit(nc_, ncv, &vopts));
     CHECK(0 == notcurses_render(nc_));
     ncvisual_destroy(ncv);
     CHECK(0 == ncplane_destroy(n));
@@ -126,9 +127,10 @@ TEST_CASE("Bitmaps") {
     };
     auto n = ncplane_create(n_, &nopts);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    CHECK(nullptr != ncvisual_render(nc_, ncv, &vopts));
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    CHECK(nullptr != ncvisual_blit(nc_, ncv, &vopts));
     CHECK(0 > ncplane_putchar_yx(n, ' ', 0, 0));
     CHECK(0 == notcurses_render(nc_));
     ncvisual_destroy(ncv);
@@ -143,9 +145,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto botn = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto botn = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != botn);
     // should just have a red plane
     CHECK(0 == notcurses_render(nc_));
@@ -154,7 +157,7 @@ TEST_CASE("Bitmaps") {
     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);
-    auto topn = ncvisual_render(nc_, ncv2, &vopts);
+    auto topn = ncvisual_blit(nc_, ncv2, &vopts);
     REQUIRE(nullptr != topn);
     // should have a yellow plane partially obscuring a red one
     CHECK(0 == notcurses_render(nc_));
@@ -179,9 +182,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_file(find_data("worldmap.png").get());
     REQUIRE(ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto newn = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(newn);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(newn));
@@ -199,9 +203,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
@@ -220,18 +225,20 @@ TEST_CASE("Bitmaps") {
     auto bigp = ncplane_create(n_, &nopts);
     REQUIRE(bigp);
     vopts.n = bigp;
+    vopts.flags &= ~NCVISUAL_OPTION_CHILDPLANE;
     uint64_t white = NCCHANNELS_INITIALIZER(0xff, 0xff, 0xff, 0xff, 0xff, 0xff);
     ncplane_set_base(bigp, "x", 0, white);
-    CHECK(vopts.n == ncvisual_render(nc_, ncv, &vopts));
+    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);
     vopts.y = 1;
     vopts.x = 6;
-    vopts.n = nullptr;
+    vopts.n = n_;
     vopts.scaling = NCSCALE_NONE;
-    auto infn = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags |= NCVISUAL_OPTION_CHILDPLANE;
+    auto infn = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(infn);
     if(nc_->tcache.sprixel_scale_height == 6){
       if(4 * nc_->tcache.cellpixy % 6){
@@ -249,7 +256,7 @@ TEST_CASE("Bitmaps") {
     CHECK(ncv->pixy == 8);
     CHECK(ncv->pixx == 8);
     vopts.x = 11;
-    auto resizen = ncvisual_render(nc_, ncv, &vopts);
+    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));
@@ -272,14 +279,15 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     struct ncplane_options nopts = {
       .y = 2,
       .x = 0,
-      .rows = 5,
+      .rows = 4,
       .cols = 4,
       .userptr = nullptr, .name = "scale", .resizecb = nullptr,
       .flags = 0, .margin_b = 0, .margin_r = 0,
@@ -288,14 +296,15 @@ TEST_CASE("Bitmaps") {
     REQUIRE(nullptr != nres);
     vopts.n = nres;
     vopts.scaling = NCSCALE_SCALE;
-    ncvisual_render(nc_, ncv, &vopts);
+    ncvisual_blit(nc_, ncv, &vopts);
     CHECK(4 == ncplane_dim_x(vopts.n));
     CHECK(0 == ncvisual_resize_noninterpolative(ncv, ncv->pixy * 4, ncv->pixx * 4));
-    vopts.n = nullptr;
+    vopts.n = nres;
     vopts.y = 2;
     vopts.x = 5;
     vopts.scaling = NCSCALE_NONE;
-    auto ninf = ncvisual_render(nc_, ncv, &vopts);
+    auto ninf = ncvisual_blit(nc_, ncv, &vopts);
+notcurses_debug(nc_, stderr);
     REQUIRE(nullptr != ninf);
     CHECK(ncplane_dim_y(nres) == ncplane_dim_y(ninf));
     CHECK(ncplane_dim_x(nres) == ncplane_dim_x(ninf));
@@ -339,7 +348,7 @@ TEST_CASE("Bitmaps") {
     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);
-    auto child = ncvisual_render(nc_, ncv, &vopts);
+    auto child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
     CHECK(18 == ncplane_dim_y(child));
     CHECK(5 == ncplane_dim_x(child));
@@ -382,7 +391,7 @@ TEST_CASE("Bitmaps") {
     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);
-    auto child = ncvisual_render(nc_, ncv, &vopts);
+    auto child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
     CHECK(6 == ncplane_dim_y(child));
     CHECK(1 == ncplane_dim_x(child));
@@ -401,9 +410,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
@@ -432,9 +442,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
@@ -463,9 +474,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     auto s = n->sprite;
     REQUIRE(nullptr != s);
@@ -502,9 +514,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     ncvisual_destroy(ncv);
     CHECK(0 == notcurses_render(nc_));
@@ -561,10 +574,11 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_file(find_data("worldmap.png").get());
     REQUIRE(ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
     vopts.scaling = NCSCALE_STRETCH;
-    auto newn = ncvisual_render(nc_, ncv, &vopts);
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(newn);
     ncplane_move_bottom(newn);
     CHECK(0 == notcurses_render(nc_));
@@ -592,9 +606,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     for(int i = 0 ; i <= ncplane_dim_x(n_) ; ++i){
       CHECK(0 == ncplane_move_yx(n, 0, i));
@@ -612,10 +627,11 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
     vopts.x = ncplane_dim_x(n_) - 3;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(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));
@@ -633,9 +649,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     for(int i = 0 ; i <= ncplane_dim_y(n_) ; ++i){
       CHECK(0 == ncplane_move_yx(n, i, 0));
@@ -653,10 +670,11 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
     vopts.y = ncplane_dim_y(n_) - 3;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto n = ncvisual_blit(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));
@@ -675,9 +693,10 @@ TEST_CASE("Bitmaps") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    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;
@@ -689,10 +708,8 @@ TEST_CASE("Bitmaps") {
       vopts.y = yat / nc_->tcache.cellpixy;
       vopts.pxoffy = yat % nc_->tcache.cellpixy;
       CHECK(0 == ncplane_destroy(n));
-      n = ncvisual_render(nc_, ncv, &vopts);
-      // FIXME disabled REQUIRE until sixel works
-      // FIXME REQUIRE(nullptr != n);
-      WARN(nullptr != n);
+      n = ncvisual_blit(nc_, ncv, &vopts);
+      REQUIRE(nullptr != n);
       CHECK(0 == notcurses_render(nc_));
     }
     ncvisual_destroy(ncv);
diff --git a/src/tests/blit.cpp b/src/tests/blit.cpp
index 5d4f9aa801..ed4ce428b8 100644
--- a/src/tests/blit.cpp
+++ b/src/tests/blit.cpp
@@ -123,7 +123,9 @@ TEST_CASE("Blit") {
         REQUIRE(nullptr != ncv);
         struct ncvisual_options vopts{};
         vopts.blitter = NCBLIT_2x2;
-        auto ncp = ncvisual_render(nc_, ncv, &vopts);
+        vopts.n = n_;
+        vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
+        auto ncp = ncvisual_blit(nc_, ncv, &vopts);
         ncvisual_destroy(ncv);
         REQUIRE(nullptr != ncp);
         CHECK(0 == notcurses_render(nc_));
@@ -149,7 +151,9 @@ TEST_CASE("Blit") {
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts{};
     vopts.blitter = NCBLIT_1x1;
-    auto p = ncvisual_render(nc_, ncv, &vopts);
+    vopts.n = n_;
+    vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
+    auto p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(1 == ncplane_dim_y(p));
@@ -175,7 +179,9 @@ TEST_CASE("Blit") {
       REQUIRE(nullptr != ncv);
       struct ncvisual_options vopts{};
       vopts.blitter = NCBLIT_2x1;
-      auto p = ncvisual_render(nc_, ncv, &vopts);
+      vopts.n = n_;
+      vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
+      auto p = ncvisual_blit(nc_, ncv, &vopts);
       REQUIRE(nullptr != p);
       CHECK(1 == ncplane_dim_y(p));
       CHECK(4 == ncplane_dim_x(p));
@@ -218,7 +224,9 @@ TEST_CASE("Blit") {
       REQUIRE(nullptr != ncv);
       struct ncvisual_options vopts{};
       vopts.blitter = NCBLIT_2x2;
-      auto p = ncvisual_render(nc_, ncv, &vopts);
+      vopts.n = n_;
+      vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
+      auto p = ncvisual_blit(nc_, ncv, &vopts);
       REQUIRE(nullptr != p);
       CHECK(1 == ncplane_dim_y(p));
       CHECK(16 == ncplane_dim_x(p));
diff --git a/src/tests/media.cpp b/src/tests/media.cpp
index a31424c87f..fce2f4c1ef 100644
--- a/src/tests/media.cpp
+++ b/src/tests/media.cpp
@@ -28,21 +28,23 @@ TEST_CASE("Media") {
     struct ncvisual_options vopts{};
     vopts.x = NCALIGN_CENTER;
     vopts.y = NCALIGN_CENTER;
-    vopts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED;
-    auto p = ncvisual_render(nc_, ncv, &vopts);
+    vopts.n = n_;
+    vopts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED
+                    | NCVISUAL_OPTION_CHILDPLANE;
+    auto p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
     CHECK(0 == ncvisual_resize(ncv, 20, 20));
-    p = ncvisual_render(nc_, ncv, &vopts);
+    p = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
-    p = ncvisual_render(nc_, ncv, &vopts);
+    p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
     CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
-    p = ncvisual_render(nc_, ncv, &vopts);
+    p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
@@ -58,7 +60,10 @@ TEST_CASE("Media") {
     CHECK(dimx == frame->width); FIXME */
     struct ncvisual_options opts{};
     opts.scaling = NCSCALE_STRETCH;
-    auto newn = ncvisual_render(nc_, ncv, &opts);
+    opts.n = n_;
+    opts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED
+                   | NCVISUAL_OPTION_CHILDPLANE;
+    auto newn = ncvisual_blit(nc_, ncv, &opts);
     CHECK(newn);
     CHECK(0 == notcurses_render(nc_));
     CHECK(1 == ncvisual_decode(ncv));
@@ -76,7 +81,7 @@ TEST_CASE("Media") {
     struct ncvisual_options opts{};
     opts.scaling = NCSCALE_STRETCH;
     opts.n = ncp_;
-    CHECK(ncvisual_render(nc_, ncv, &opts));
+    CHECK(ncvisual_blit(nc_, ncv, &opts));
     CHECK(0 == notcurses_render(nc_));
     CHECK(1 == ncvisual_decode(ncv));
     ncvisual_destroy(ncv);
@@ -91,13 +96,13 @@ TEST_CASE("Media") {
     struct ncvisual_options opts{};
     opts.n = ncp_;
     CHECK(0 == ncvisual_blitter_geom(nc_, ncv, &opts, &odimy, &odimx, nullptr, nullptr, nullptr));
-    CHECK(ncvisual_render(nc_, ncv, &opts));
+    CHECK(ncvisual_blit(nc_, ncv, &opts));
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncvisual_resize_noninterpolative(ncv, ncv->pixy * 2, ncv->pixx * 2));
     CHECK(0 == ncvisual_blitter_geom(nc_, ncv, &opts, &ndimy, &ndimx, nullptr, nullptr, nullptr));
     CHECK(ndimy == odimy * 2);
     CHECK(ndimx == odimx * 2);
-    CHECK(ncvisual_render(nc_, ncv, &opts));
+    CHECK(ncvisual_blit(nc_, ncv, &opts));
     CHECK(0 == notcurses_render(nc_));
     ncvisual_destroy(ncv);
   }
@@ -110,7 +115,7 @@ TEST_CASE("Media") {
     struct ncvisual_options opts{};
     opts.n = ncp_;
     opts.scaling = NCSCALE_STRETCH;
-    CHECK(ncvisual_render(nc_, ncv, &opts));
+    CHECK(ncvisual_blit(nc_, ncv, &opts));
     void* needle = malloc(1);
     REQUIRE(nullptr != needle);
     struct ncplane* newn = ncplane_dup(ncp_, needle);
@@ -142,7 +147,7 @@ TEST_CASE("Media") {
         opts.scaling = NCSCALE_SCALE_HIRES;
         opts.n = ncp_;
         opts.blitter = NCBLIT_1x1;
-        CHECK(ncvisual_render(nc_, ncv, &opts));
+        CHECK(ncvisual_blit(nc_, ncv, &opts));
         CHECK(0 == notcurses_render(nc_));
       }
       ncvisual_destroy(ncv);
@@ -165,7 +170,7 @@ TEST_CASE("Media") {
         opts.scaling = NCSCALE_SCALE_HIRES;
         opts.n = ncp_;
         opts.blitter = NCBLIT_2x1;
-        CHECK(ncvisual_render(nc_, ncv, &opts));
+        CHECK(ncvisual_blit(nc_, ncv, &opts));
         CHECK(0 == notcurses_render(nc_));
       }
       ncvisual_destroy(ncv);
@@ -189,7 +194,7 @@ TEST_CASE("Media") {
         opts.scaling = NCSCALE_SCALE_HIRES;
         opts.n = ncp_;
         opts.blitter = NCBLIT_2x2;
-        CHECK(ncvisual_render(nc_, ncv, &opts));
+        CHECK(ncvisual_blit(nc_, ncv, &opts));
         CHECK(0 == notcurses_render(nc_));
       }
       ncvisual_destroy(ncv);
@@ -212,7 +217,7 @@ TEST_CASE("Media") {
         opts.scaling = NCSCALE_SCALE_HIRES;
         opts.n = ncp_;
         opts.blitter = NCBLIT_3x2;
-        CHECK(ncvisual_render(nc_, ncv, &opts));
+        CHECK(ncvisual_blit(nc_, ncv, &opts));
         CHECK(0 == notcurses_render(nc_));
       }
       ncvisual_destroy(ncv);
@@ -235,7 +240,7 @@ TEST_CASE("Media") {
         opts.scaling = NCSCALE_SCALE_HIRES;
         opts.n = ncp_;
         opts.blitter = NCBLIT_BRAILLE;
-        CHECK(ncvisual_render(nc_, ncv, &opts));
+        CHECK(ncvisual_blit(nc_, ncv, &opts));
         CHECK(0 == notcurses_render(nc_));
       }
       ncvisual_destroy(ncv);
@@ -261,7 +266,7 @@ TEST_CASE("Media") {
           opts.scaling = NCSCALE_SCALE;
           opts.blitter = NCBLIT_PIXEL;
           opts.n = n;
-          n = ncvisual_render(nc_, ncv, &opts);
+          n = ncvisual_blit(nc_, ncv, &opts);
           REQUIRE(nullptr != n);
           CHECK(0 == notcurses_render(nc_));
         }
@@ -288,7 +293,7 @@ TEST_CASE("Media") {
           struct ncvisual_options opts{};
           opts.scaling = NCSCALE_SCALE;
           opts.blitter = NCBLIT_PIXEL;
-          REQUIRE(ncvisual_render(nc_, ncv, &opts));
+          REQUIRE(ncvisual_blit(nc_, ncv, &opts));
           CHECK(0 == notcurses_render(nc_));
         }
         ncvisual_destroy(ncv);
@@ -310,13 +315,13 @@ TEST_CASE("Media") {
       CHECK(1 == ret);
       ret = ncvisual_decode_loop(ncv);
       CHECK(1 == ret);
-      struct ncplane* ncp = ncvisual_render(nc_, ncv, nullptr);
+      struct ncplane* ncp = ncvisual_blit(nc_, ncv, nullptr);
       CHECK(nullptr != ncp);
       CHECK(0 == ncplane_destroy(ncp));
       // FIXME verify that it is first frame, not last?
       ret = ncvisual_decode_loop(ncv);
       CHECK(0 == ret);
-      ncp = ncvisual_render(nc_, ncv, nullptr);
+      ncp = ncvisual_blit(nc_, ncv, nullptr);
       CHECK(nullptr != ncp);
       CHECK(0 == ncplane_destroy(ncp));
       ncvisual_destroy(ncv);
@@ -334,7 +339,7 @@ TEST_CASE("Media") {
       CHECK(dimx == frame->width); FIXME */
       struct ncvisual_options opts{};
       opts.scaling = NCSCALE_STRETCH;
-      auto newn = ncvisual_render(nc_, ncv, &opts);
+      auto newn = ncvisual_blit(nc_, ncv, &opts);
       CHECK(newn);
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncplane_destroy(newn));
@@ -363,7 +368,7 @@ TEST_CASE("Media") {
     vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
     auto ncv = ncvisual_from_file(find_data("onedot.png").get());
     REQUIRE(ncv);
-    auto child = ncvisual_render(nc_, ncv, &vopts);
+    auto child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
     CHECK(5 == ncplane_dim_y(child));
     CHECK(5 == ncplane_dim_x(child));
diff --git a/src/tests/rotate.cpp b/src/tests/rotate.cpp
index 6a24aa1dfc..258b25003a 100644
--- a/src/tests/rotate.cpp
+++ b/src/tests/rotate.cpp
@@ -171,7 +171,9 @@ TEST_CASE("Rotate") {
     auto ncv = ncvisual_from_rgba(rgba.data(), height, width * 4, width);
     REQUIRE(ncv);
     ncvisual_options opts{};
-    auto rendered = ncvisual_render(nc_, ncv, &opts);
+    opts.n = n_;
+    opts.flags = NCVISUAL_OPTION_CHILDPLANE;
+    auto rendered = ncvisual_blit(nc_, ncv, &opts);
     REQUIRE(rendered);
     int pxdimy, pxdimx;
     uint32_t* rgbaret = ncplane_as_rgba(rendered, NCBLIT_2x1,
@@ -207,16 +209,16 @@ TEST_CASE("Rotate") {
       opts.n = rendered;
       // FIXME check pixels after all rotations
       CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
     }
     ncvisual_destroy(ncv);
@@ -231,7 +233,9 @@ TEST_CASE("Rotate") {
     auto ncv = ncvisual_from_rgba(rgba.data(), height, width * 4, width);
     REQUIRE(ncv);
     ncvisual_options opts{};
-    auto rendered = ncvisual_render(nc_, ncv, &opts);
+    opts.n = n_;
+    opts.flags = NCVISUAL_OPTION_CHILDPLANE;
+    auto rendered = ncvisual_blit(nc_, ncv, &opts);
     REQUIRE(rendered);
     int pxdimy, pxdimx;
     uint32_t* rgbaret = ncplane_as_rgba(rendered, NCBLIT_2x1,
@@ -267,16 +271,16 @@ TEST_CASE("Rotate") {
       // FIXME check pixels after all rotations
       opts.n = rendered;
       CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
       CHECK(0 == ncvisual_rotate(ncv, -M_PI / 2));
-      CHECK(ncvisual_render(nc_, ncv, &opts));
+      CHECK(ncvisual_blit(nc_, ncv, &opts));
       CHECK(0 == notcurses_render(nc_));
     }
     ncvisual_destroy(ncv);
diff --git a/src/tests/sixel.cpp b/src/tests/sixel.cpp
index 500ec76b16..84c9a16f9f 100644
--- a/src/tests/sixel.cpp
+++ b/src/tests/sixel.cpp
@@ -162,9 +162,10 @@ TEST_CASE("Sixels") {
     auto ncv = ncvisual_from_file(find_data("worldmap.png").get());
     REQUIRE(ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto newn = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(newn);
     CHECK(0 == notcurses_render(nc_));
     auto rgb = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
@@ -184,9 +185,10 @@ TEST_CASE("Sixels") {
     auto ncv = ncvisual_from_file(find_data("natasha-blur.png").get());
     REQUIRE(ncv);
     struct ncvisual_options vopts{};
+    vopts.n = n_;
     vopts.blitter = NCBLIT_PIXEL;
-    vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-    auto newn = ncvisual_render(nc_, ncv, &vopts);
+    vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(newn);
     auto rgbold = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
                                newn->sprite->pixy, newn->sprite->pixx);
diff --git a/src/tests/stacking.cpp b/src/tests/stacking.cpp
index 1d4ec4ec95..54f4aeb02e 100644
--- a/src/tests/stacking.cpp
+++ b/src/tests/stacking.cpp
@@ -45,7 +45,7 @@ TEST_CASE("Stacking") {
       .leny = 2, .lenx = 1, .blitter = NCBLIT_2x1, .flags = 0,
       .transcolor = 0, .pxoffy = 0, .pxoffx = 0,
     };
-    CHECK(top == ncvisual_render(nc_, ncv, &vopts));
+    CHECK(top == ncvisual_blit(nc_, ncv, &vopts));
     ncvisual_destroy(ncv);
 
     // create an ncvisual of 2 rows, 1 column, with the top 0xffffff
@@ -53,19 +53,24 @@ TEST_CASE("Stacking") {
     ncv = ncvisual_from_rgba(botv, 2, 4, 1);
     REQUIRE(nullptr != ncv);
     vopts.n = n_;
-    CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+    vopts.flags |= NCVISUAL_OPTION_CHILDPLANE;
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
+    REQUIRE(nullptr != newn);
     ncvisual_destroy(ncv);
+    ncplane_move_below(newn, top);
 
     CHECK(0 == notcurses_render(nc_));
     uint64_t channels;
     auto egc = notcurses_at_yx(nc_, 0, 0, nullptr, &channels);
     REQUIRE(nullptr != egc);
+notcurses_debug(nc_, stderr);
     // ought yield space with white background FIXME currently just yields
     // a lower half block
     CHECK(0 == strcmp("\u2584", egc));
     CHECK(0xffffff == ncchannels_fg_rgb(channels));
     CHECK(0xffffff == ncchannels_bg_rgb(channels));
-    ncplane_destroy(top);
+    CHECK(0 == ncplane_destroy(top));
+    CHECK(0 == ncplane_destroy(newn));
   }
 
   SUBCASE("UpperAtopLowerWhite") {
@@ -83,7 +88,7 @@ TEST_CASE("Stacking") {
       .leny = 2, .lenx = 1, .blitter = NCBLIT_2x1, .flags = 0,
       .transcolor = 0, .pxoffy = 0, .pxoffx = 0,
     };
-    CHECK(top == ncvisual_render(nc_, ncv, &vopts));
+    CHECK(top == ncvisual_blit(nc_, ncv, &vopts));
     ncvisual_destroy(ncv);
 
     // create an ncvisual of 2 rows, 1 column, with the bottom 0xffffff
@@ -91,8 +96,11 @@ TEST_CASE("Stacking") {
     ncv = ncvisual_from_rgba(botv, 2, 4, 1);
     REQUIRE(nullptr != ncv);
     vopts.n = n_;
-    CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+    vopts.flags |= NCVISUAL_OPTION_CHILDPLANE;
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
+    REQUIRE(nullptr != newn);
     ncvisual_destroy(ncv);
+    ncplane_move_below(newn, top);
 
     CHECK(0 == notcurses_render(nc_));
     uint64_t channels;
@@ -103,7 +111,8 @@ TEST_CASE("Stacking") {
     CHECK(0 == strcmp("\u2580", egc));
     CHECK(0xffffff == ncchannels_fg_rgb(channels));
     CHECK(0xffffff == ncchannels_bg_rgb(channels));
-    ncplane_destroy(top);
+    CHECK(0 == ncplane_destroy(top));
+    CHECK(0 == ncplane_destroy(newn));
   }
 
   SUBCASE("StackedQuadHalves") {
@@ -122,7 +131,7 @@ TEST_CASE("Stacking") {
         .leny = 2, .lenx = 2, .blitter = NCBLIT_2x2, .flags = 0,
         .transcolor = 0, .pxoffy = 0, .pxoffx = 0,
       };
-      CHECK(top == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(top == ncvisual_blit(nc_, ncv, &vopts));
       ncvisual_destroy(ncv);
 
       // create an ncvisual of 2 rows, 2 columns, with the bottom 0xffffff
@@ -130,8 +139,11 @@ TEST_CASE("Stacking") {
       ncv = ncvisual_from_rgba(botv, 2, 8, 2);
       REQUIRE(nullptr != ncv);
       vopts.n = n_;
-      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
+      auto newn = ncvisual_blit(nc_, ncv, &vopts); 
+      REQUIRE(nullptr != newn);
       ncvisual_destroy(ncv);
+      ncplane_move_below(newn, top);
 
       CHECK(0 == notcurses_render(nc_));
       uint64_t channels;
@@ -142,7 +154,8 @@ TEST_CASE("Stacking") {
       CHECK(0 == strcmp("\u2580", egc));
       CHECK(0x00ff00 == ncchannels_fg_rgb(channels));
       CHECK(0x00ff00 == ncchannels_bg_rgb(channels));
-      ncplane_destroy(top);
+      CHECK(0 == ncplane_destroy(top));
+      CHECK(0 == ncplane_destroy(newn));
     }
   }
 
@@ -164,7 +177,7 @@ TEST_CASE("Stacking") {
         .leny = 2, .lenx = 2, .blitter = NCBLIT_2x2, .flags = 0,
         .transcolor = 0, .pxoffy = 0, .pxoffx = 0,
       };
-      CHECK(top == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(top == ncvisual_blit(nc_, ncv, &vopts));
       ncvisual_destroy(ncv);
 
       // create an ncvisual of 2 rows, 2 columns, with the tr, bl 0xffffff
@@ -172,8 +185,11 @@ TEST_CASE("Stacking") {
       ncv = ncvisual_from_rgba(botv, 2, 8, 2);
       REQUIRE(nullptr != ncv);
       vopts.n = n_;
-      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
+      auto newn = ncvisual_blit(nc_, ncv, &vopts);
+      REQUIRE(nullptr != newn);
       ncvisual_destroy(ncv);
+      ncplane_move_below(newn, top);
 
       CHECK(0 == notcurses_render(nc_));
       uint64_t channels;
@@ -184,7 +200,8 @@ TEST_CASE("Stacking") {
       CHECK(0 == strcmp("\u259a", egc)); // quadrant upper left and lower right
       CHECK(0xffffff == ncchannels_fg_rgb(channels));
       CHECK(0xffffff == ncchannels_bg_rgb(channels));
-      ncplane_destroy(top);
+      CHECK(0 == ncplane_destroy(top));
+      CHECK(0 == ncplane_destroy(newn));
     }
   }
 
diff --git a/src/tests/visual.cpp b/src/tests/visual.cpp
index 9c452d0754..88087c0743 100644
--- a/src/tests/visual.cpp
+++ b/src/tests/visual.cpp
@@ -74,18 +74,20 @@ TEST_CASE("Visual") {
     struct ncvisual_options vopts{};
     vopts.x = NCALIGN_CENTER;
     vopts.y = NCALIGN_CENTER;
-    vopts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED;
-    auto p = ncvisual_render(nc_, ncv, &vopts);
+    vopts.n = n_;
+    vopts.flags |= NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_VERALIGNED
+                    | NCVISUAL_OPTION_CHILDPLANE;
+    auto p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
     CHECK(0 == ncvisual_resize(ncv, 20, 20));
-    p = ncvisual_render(nc_, ncv, &vopts);
+    p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
     CHECK(0 == ncvisual_rotate(ncv, M_PI / 2));
-    p = ncvisual_render(nc_, ncv, &vopts);
+    p = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != p);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncplane_destroy(p));
@@ -96,7 +98,7 @@ TEST_CASE("Visual") {
   SUBCASE("VisualAligned") {
     const uint32_t pixels[4] = { htole(0xffff0000), htole(0xff00ff00), htole(0xff0000ff), htole(0xffffffff) };
     ncvisual_options vopts = {
-      .n = nullptr,
+      .n = n_,
       .scaling = NCSCALE_NONE,
       .y = 0,
       .x = NCALIGN_LEFT,
@@ -105,13 +107,13 @@ TEST_CASE("Visual") {
       .leny = 2,
       .lenx = 2,
       .blitter = NCBLIT_1x1,
-      .flags = NCVISUAL_OPTION_HORALIGNED,
+      .flags = NCVISUAL_OPTION_HORALIGNED | NCVISUAL_OPTION_CHILDPLANE,
       .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
     auto ncv = ncvisual_from_rgba(pixels, 2, 2 * sizeof(*pixels), 2);
     REQUIRE(nullptr != ncv);
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     CHECK(0 == notcurses_render(nc_));
     ncvisual_destroy(ncv);
@@ -126,16 +128,17 @@ TEST_CASE("Visual") {
     auto ncv = ncvisual_from_rgba(v.data(), y, sizeof(decltype(v)::value_type) * x, x);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts = {
-      .n = nullptr,
+      .n = n_,
       .scaling = NCSCALE_NONE,
       .y = 0, .x = 0,
       .begy = 0, .begx = 0,
       .leny = 5, .lenx = 8,
       .blitter = NCBLIT_1x1,
-      .flags = 0, .transcolor = 0,
+      .flags = NCVISUAL_OPTION_CHILDPLANE,
+      .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     CHECK(5 == ncplane_dim_y(n));
     CHECK(8 == ncplane_dim_x(n));
@@ -152,16 +155,17 @@ TEST_CASE("Visual") {
     auto ncv = ncvisual_from_rgba(v.data(), 1, sizeof(decltype(v)::value_type), 1);
     REQUIRE(nullptr != ncv);
     struct ncvisual_options vopts = {
-      .n = nullptr,
+      .n = n_,
       .scaling = NCSCALE_STRETCH,
       .y = 0, .x = 0,
       .begy = 0, .begx = 0,
       .leny = 0, .lenx = 0,
       .blitter = NCBLIT_1x1,
-      .flags = 0, .transcolor = 0,
+      .flags = NCVISUAL_OPTION_CHILDPLANE,
+      .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(0 == notcurses_render(nc_));
     REQUIRE(nullptr != n);
     CHECK(dimy == ncplane_dim_y(n));
@@ -198,7 +202,7 @@ TEST_CASE("Visual") {
       .flags = 0, .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
-    auto n = ncvisual_render(nc_, ncv, &vopts);
+    auto n = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != n);
     CHECK(5 == ncplane_dim_y(n));
     CHECK(8 == ncplane_dim_x(n));
@@ -222,7 +226,7 @@ TEST_CASE("Visual") {
       .flags = 0, .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
-    auto newn = ncvisual_render(nc_, ncv, &vopts);
+    auto newn = ncvisual_blit(nc_, ncv, &vopts);
     CHECK(0 == notcurses_render(nc_));
     CHECK(0 == ncvisual_resize_noninterpolative(ncv, ncv->pixy * 3, ncv->pixx * 3));
     CHECK(6 == ncv->pixy);
@@ -244,7 +248,7 @@ TEST_CASE("Visual") {
       }
     }
     REQUIRE(newn);
-    auto enewn = ncvisual_render(nc_, ncv, &vopts);
+    auto enewn = ncvisual_blit(nc_, ncv, &vopts);
     int newy, newx;
     ncplane_dim_yx(enewn, &newy, &newx);
     CHECK(6 == newy);
@@ -265,7 +269,7 @@ TEST_CASE("Visual") {
     struct ncvisual_options opts{};
     opts.blitter = NCBLIT_1x1;
     opts.n = ncp_;
-    CHECK(ncp_ == ncvisual_render(nc_, ncv, &opts));
+    CHECK(ncp_ == ncvisual_blit(nc_, ncv, &opts));
     CHECK(0 == notcurses_render(nc_));
     for(int y = 0 ; y < dimy ; ++y){
       for(int x = 0 ; x < dimx ; ++x){
@@ -293,7 +297,7 @@ TEST_CASE("Visual") {
     struct ncvisual_options opts{};
     opts.blitter = NCBLIT_1x1;
     opts.n = ncp_;
-    CHECK(nullptr != ncvisual_render(nc_, ncv, &opts));
+    CHECK(nullptr != ncvisual_blit(nc_, ncv, &opts));
     CHECK(0 == notcurses_render(nc_));
     for(int y = 0 ; y < dimy ; ++y){
       for(int x = 0 ; x < dimx ; ++x){
@@ -333,7 +337,7 @@ TEST_CASE("Visual") {
       vopts.n = n_;
       vopts.blitter = NCBLIT_2x1;
       vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(n_ == ncvisual_blit(nc_, ncv, &vopts));
       CHECK(0 == notcurses_render(nc_));
       for(int y = 0 ; y < DIMY / 2 ; ++y){
         for(int x = 0 ; x < DIMX ; ++x){
@@ -373,7 +377,7 @@ TEST_CASE("Visual") {
       vopts.n = n_;
       vopts.blitter = NCBLIT_2x2;
       vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(n_ == ncvisual_blit(nc_, ncv, &vopts));
       CHECK(0 == notcurses_render(nc_));
       for(int y = 0 ; y < DIMY / 2 ; ++y){
         for(int x = 0 ; x < DIMX / 2 ; ++x){
@@ -431,7 +435,7 @@ TEST_CASE("Visual") {
       vopts.n = n_;
       vopts.blitter = NCBLIT_2x2;
       vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(n_ == ncvisual_blit(nc_, ncv, &vopts));
       CHECK(0 == notcurses_render(nc_));
       for(int y = 0 ; y < DIMY / 2 ; ++y){
         for(int x = 0 ; x < DIMX / 2 ; ++x){
@@ -471,7 +475,7 @@ TEST_CASE("Visual") {
       auto ncv = ncvisual_from_rgba(pixels, 2, 2 * sizeof(*pixels), 2);
       REQUIRE(nullptr != ncv);
       struct ncvisual_options vopts = {
-        .n = nullptr,
+        .n = n_,
         .scaling = NCSCALE_NONE,
         .y = 0,
         .x = 0,
@@ -480,11 +484,11 @@ TEST_CASE("Visual") {
         .leny = 0,
         .lenx = 0,
         .blitter = NCBLIT_2x2,
-        .flags = 0,
+        .flags = NCVISUAL_OPTION_CHILDPLANE,
         .transcolor = 0,
         .pxoffy = 0, .pxoffx = 0,
       };
-      auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+      auto ncvp = ncvisual_blit(nc_, ncv, &vopts);
       REQUIRE(nullptr != ncvp);
       int dimy, dimx;
       ncplane_dim_yx(ncvp, &dimy, &dimx);
@@ -516,7 +520,7 @@ TEST_CASE("Visual") {
         auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
         REQUIRE(nullptr != ncv);
         struct ncvisual_options vopts = {
-          .n = nullptr,
+          .n = n_,
           .scaling = NCSCALE_NONE,
           .y = 0,
           .x = 0,
@@ -525,11 +529,11 @@ TEST_CASE("Visual") {
           .leny = 0,
           .lenx = 0,
           .blitter = NCBLIT_2x2,
-          .flags = 0,
+          .flags = NCVISUAL_OPTION_CHILDPLANE,
           .transcolor = 0,
           .pxoffy = 0, .pxoffx = 0,
         };
-        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        auto ncvp = ncvisual_blit(nc_, ncv, &vopts);
         REQUIRE(nullptr != ncvp);
         int dimy, dimx;
         ncplane_dim_yx(ncvp, &dimy, &dimx);
@@ -564,7 +568,7 @@ TEST_CASE("Visual") {
         auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
         REQUIRE(nullptr != ncv);
         struct ncvisual_options vopts = {
-          .n = nullptr,
+          .n = n_,
           .scaling = NCSCALE_NONE,
           .y = 0,
           .x = 0,
@@ -573,11 +577,11 @@ TEST_CASE("Visual") {
           .leny = 0,
           .lenx = 0,
           .blitter = NCBLIT_2x2,
-          .flags = 0,
+          .flags = NCVISUAL_OPTION_CHILDPLANE,
           .transcolor = 0,
           .pxoffy = 0, .pxoffx = 0,
         };
-        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        auto ncvp = ncvisual_blit(nc_, ncv, &vopts);
         REQUIRE(nullptr != ncvp);
         int dimy, dimx;
         ncplane_dim_yx(ncvp, &dimy, &dimx);
@@ -617,7 +621,7 @@ TEST_CASE("Visual") {
         auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
         REQUIRE(nullptr != ncv);
         struct ncvisual_options vopts = {
-          .n = nullptr,
+          .n = n_,
           .scaling = NCSCALE_NONE,
           .y = 0,
           .x = 0,
@@ -626,11 +630,11 @@ TEST_CASE("Visual") {
           .leny = 0,
           .lenx = 0,
           .blitter = NCBLIT_2x2,
-          .flags = 0,
+          .flags = NCVISUAL_OPTION_CHILDPLANE,
           .transcolor = 0,
           .pxoffy = 0, .pxoffx = 0,
         };
-        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        auto ncvp = ncvisual_blit(nc_, ncv, &vopts);
         REQUIRE(nullptr != ncvp);
         int dimy, dimx;
         ncplane_dim_yx(ncvp, &dimy, &dimx);
@@ -669,7 +673,7 @@ TEST_CASE("Visual") {
         auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
         REQUIRE(nullptr != ncv);
         struct ncvisual_options vopts = {
-          .n = nullptr,
+          .n = n_,
           .scaling = NCSCALE_NONE,
           .y = 0,
           .x = 0,
@@ -678,11 +682,11 @@ TEST_CASE("Visual") {
           .leny = 0,
           .lenx = 0,
           .blitter = NCBLIT_2x2,
-          .flags = 0,
+          .flags = NCVISUAL_OPTION_CHILDPLANE,
           .transcolor = 0,
           .pxoffy = 0, .pxoffx = 0,
         };
-        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        auto ncvp = ncvisual_blit(nc_, ncv, &vopts);
         REQUIRE(nullptr != ncvp);
         int dimy, dimx;
         ncplane_dim_yx(ncvp, &dimy, &dimx);
@@ -702,7 +706,7 @@ TEST_CASE("Visual") {
     }
   }
 
-  // test NCVISUAL_OPTIONS_CHILDPLANE + stretch + null alignment
+  // test NCVISUAL_OPTION_CHILDPLANE + stretch + null alignment
   SUBCASE("ImageChildScaling") {
     struct ncplane_options opts = {
       .y = 0, .x = 0,
@@ -717,14 +721,14 @@ TEST_CASE("Visual") {
     auto parent = ncplane_create(n_, &opts);
     REQUIRE(parent);
     struct ncvisual_options vopts = {
-      .n = nullptr,
+      .n = n_,
       .scaling = NCSCALE_NONE,
       .y = 0,
       .x = 0,
       .begy = 0, .begx = 0,
       .leny = 0, .lenx = 0,
       .blitter = NCBLIT_1x1,
-      .flags = 0,
+      .flags = NCVISUAL_OPTION_CHILDPLANE,
       .transcolor = 0,
       .pxoffy = 0, .pxoffx = 0,
     };
@@ -736,7 +740,7 @@ TEST_CASE("Visual") {
     };
     auto ncv = ncvisual_from_rgba(pixels, 4, 16, 4);
     REQUIRE(ncv);
-    auto child = ncvisual_render(nc_, ncv, &vopts);
+    auto child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
     CHECK(4 == ncplane_dim_y(child));
     CHECK(4 == ncplane_dim_x(child));
@@ -747,7 +751,7 @@ TEST_CASE("Visual") {
     vopts.n = parent,
     vopts.scaling = NCSCALE_STRETCH,
     vopts.flags = NCVISUAL_OPTION_CHILDPLANE;
-    child = ncvisual_render(nc_, ncv, &vopts);
+    child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
     CHECK(20 == ncplane_dim_y(child));
     CHECK(20 == ncplane_dim_x(child));
@@ -787,7 +791,7 @@ TEST_CASE("Visual") {
     const uint32_t pixels[1] = { htole(0xffffffff) };
     auto ncv = ncvisual_from_rgba(pixels, 1, 4, 1);
     REQUIRE(ncv);
-    auto child = ncvisual_render(nc_, ncv, &vopts);
+    auto child = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(child);
     CHECK(1 == ncplane_dim_y(child));
     CHECK(1 == ncplane_dim_x(child));