diff --git a/demos/sdl2.c b/demos/sdl2.c
index ac97c5ee080fac4978c1a7ed74bd60c0ab54bafb..8039b2e0e20c9c3c4835fa59af95a62586ac8c4c 100644
--- a/demos/sdl2.c
+++ b/demos/sdl2.c
@@ -115,10 +115,12 @@ static void init_sdl() {
         exit(1);
     }
 
+    uint32_t window_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE |
+                            SDL_WINDOW_VULKAN;
+
     window = SDL_CreateWindow("libplacebo demo",
                               SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
-                              WINDOW_WIDTH, WINDOW_HEIGHT,
-                              SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN);
+                              WINDOW_WIDTH, WINDOW_HEIGHT, window_flags);
 
     if (!window) {
         fprintf(stderr, "Failed creating window: %s\n", SDL_GetError());
@@ -269,8 +271,7 @@ static void render_frame(const struct pl_swapchain_frame *frame)
         .planes     = { img_plane },
         .repr       = pl_color_repr_unknown,
         .color      = pl_color_space_unknown,
-        .width      = img->params.w,
-        .height     = img->params.h,
+        .src_rect   = {0, 0, img->params.w, img->params.h},
     };
 
     // This seems to be the case for SDL2_image
@@ -283,6 +284,8 @@ static void render_frame(const struct pl_swapchain_frame *frame)
         .len = icc_profile.size,
     };
 
+    pl_rect2d_aspect_copy(&target.dst_rect, &image.src_rect, 0.0);
+
     const struct pl_tex *osd = osd_plane.texture;
     struct pl_overlay target_ol;
     if (osd) {
@@ -297,6 +300,9 @@ static void render_frame(const struct pl_swapchain_frame *frame)
         target.num_overlays = 1;
     }
 
+    if (pl_render_target_partial(&target))
+        pl_tex_clear(vk->gpu, target.fbo, (float[4]) {0} );
+
     // Use the heaviest preset purely for demonstration/testing purposes
     if (!pl_render_image(renderer, &image, &target, &pl_render_high_quality_params)) {
         fprintf(stderr, "Failed rendering frame!\n");
diff --git a/meson.build b/meson.build
index e7222ced5dfc8250cde9e231fb367134333b74f4..46cf6d058820236d8ff29c98706be5760b44ec6d 100644
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@ project('libplacebo', ['c', 'cpp'],
   license: 'LGPL2.1+',
   default_options: ['c_std=c99', 'cpp_std=c++11', 'warning_level=2'],
   meson_version: '>=0.49',
-  version: '2.63.0',
+  version: '2.64.0',
 )
 
 # Version number
diff --git a/src/common.c b/src/common.c
index 57b3cf08b676a64dd42023c5d37cf3f12fc88052..5cbabfc20e35991716862fd21f4b65784ee033b1 100644
--- a/src/common.c
+++ b/src/common.c
@@ -15,6 +15,8 @@
  * License along with libplacebo. If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <math.h>
+
 #include "common.h"
 
 void pl_rect2d_normalize(struct pl_rect2d *rc)
@@ -39,6 +41,28 @@ void pl_rect3d_normalize(struct pl_rect3d *rc)
     };
 }
 
+struct pl_rect2d pl_rect2df_round(const struct pl_rect2df *rc)
+{
+    return (struct pl_rect2d) {
+        .x0 = roundf(rc->x0),
+        .x1 = roundf(rc->x1),
+        .y0 = roundf(rc->y0),
+        .y1 = roundf(rc->y1),
+    };
+}
+
+struct pl_rect3d pl_rect3df_round(const struct pl_rect3df *rc)
+{
+    return (struct pl_rect3d) {
+        .x0 = roundf(rc->x0),
+        .x1 = roundf(rc->x1),
+        .y0 = roundf(rc->y0),
+        .y1 = roundf(rc->y1),
+        .z0 = roundf(rc->z0),
+        .z1 = roundf(rc->z1),
+    };
+}
+
 const struct pl_matrix3x3 pl_matrix3x3_identity = {{
     { 1, 0, 0 },
     { 0, 1, 0 },
@@ -222,3 +246,119 @@ void pl_transform2x2_apply_rc(const struct pl_transform2x2 *t, struct pl_rect2df
     rc->y0 += t->c[1];
     rc->y1 += t->c[1];
 }
+
+float pl_rect2df_aspect(const struct pl_rect2df *rc)
+{
+    float w = fabs(pl_rect_w(*rc)), h = fabs(pl_rect_h(*rc));
+    return h ? (w / h) : 0.0;
+}
+
+void pl_rect2df_aspect_set(struct pl_rect2df *rc, float aspect, float panscan)
+{
+    pl_assert(aspect >= 0);
+    float orig_aspect = pl_rect2df_aspect(rc);
+    if (!aspect || !orig_aspect)
+        return;
+
+    float scale_x, scale_y;
+    if (aspect > orig_aspect) {
+        // New aspect is wider than the original, so we need to either grow in
+        // scale_x (panscan=1) or shrink in scale_y (panscan=0)
+        scale_x = powf(aspect / orig_aspect, panscan);
+        scale_y = powf(aspect / orig_aspect, panscan - 1.0);
+    } else if (aspect < orig_aspect) {
+        // New aspect is taller, so either grow in scale_y (panscan=1) or
+        // shrink in scale_x (panscan=0)
+        scale_x = powf(orig_aspect / aspect, panscan - 1.0);
+        scale_y = powf(orig_aspect / aspect, panscan);
+    } else {
+        return; // No change in aspect
+    }
+
+    pl_rect2df_stretch(rc, scale_x, scale_y);
+}
+
+void pl_rect2df_aspect_fit(struct pl_rect2df *rc, const struct pl_rect2df *src,
+                           float panscan)
+{
+    float orig_w = fabs(pl_rect_w(*rc)),
+          orig_h = fabs(pl_rect_h(*rc));
+    if (!orig_w || !orig_h)
+        return;
+
+    // If either one of these is larger than 1, then we need to shrink to fit,
+    // otherwise we can just directly stretch the rect.
+    float scale_x = fabs(pl_rect_w(*src)) / orig_w,
+          scale_y = fabs(pl_rect_h(*src)) / orig_h;
+
+    if (scale_x > 1.0 || scale_y > 1.0) {
+        pl_rect2df_aspect_copy(rc, src, panscan);
+    } else {
+        pl_rect2df_stretch(rc, scale_x, scale_y);
+    }
+}
+
+void pl_rect2df_stretch(struct pl_rect2df *rc, float stretch_x, float stretch_y)
+{
+    float midx = (rc->x0 + rc->x1) / 2.0,
+          midy = (rc->y0 + rc->y1) / 2.0;
+
+    rc->x0 = rc->x0 * stretch_x + midx * (1.0 - stretch_x);
+    rc->x1 = rc->x1 * stretch_x + midx * (1.0 - stretch_x);
+    rc->y0 = rc->y0 * stretch_y + midy * (1.0 - stretch_y);
+    rc->y1 = rc->y1 * stretch_y + midy * (1.0 - stretch_y);
+}
+
+void pl_rect2df_offset(struct pl_rect2df *rc, float offset_x, float offset_y)
+{
+    if (rc->x1 < rc->x0)
+        offset_x = -offset_x;
+    if (rc->y1 < rc->y0)
+        offset_y = -offset_y;
+
+    rc->x0 += offset_x;
+    rc->x1 += offset_x;
+    rc->y0 += offset_y;
+    rc->y1 += offset_y;
+}
+
+float pl_rect2d_aspect(const struct pl_rect2d *rc)
+{
+    float w = abs(pl_rect_w(*rc)), h = abs(pl_rect_h(*rc));
+    return h ? (w / h) : 0.0;
+}
+
+void pl_rect2d_aspect_set(struct pl_rect2d *rc, float aspect, float panscan)
+{
+    struct pl_rect2df frc = { rc->x0, rc->y0, rc->x1, rc->y1 };
+    pl_rect2df_aspect_set(&frc, aspect, panscan);
+    *rc = pl_rect2df_round(&frc);
+}
+
+void pl_rect2d_aspect_fit(struct pl_rect2d *rc, const struct pl_rect2df *src,
+                          float panscan)
+{
+    struct pl_rect2df frc = { rc->x0, rc->y0, rc->x1, rc->y1 };
+    pl_rect2df_aspect_fit(&frc, src, panscan);
+    *rc = pl_rect2df_round(&frc);
+}
+
+void pl_rect2d_stretch(struct pl_rect2d *rc, float stretch_x, float stretch_y)
+{
+    struct pl_rect2df frc = { rc->x0, rc->y0, rc->x1, rc->y1 };
+    pl_rect2df_stretch(&frc, stretch_x, stretch_y);
+    *rc = pl_rect2df_round(&frc);
+}
+
+void pl_rect2d_offset(struct pl_rect2d *rc, int offset_x, int offset_y)
+{
+    if (rc->x1 < rc->x0)
+        offset_x = -offset_x;
+    if (rc->y1 < rc->y0)
+        offset_y = -offset_y;
+
+    rc->x0 += offset_x;
+    rc->x1 += offset_x;
+    rc->y0 += offset_y;
+    rc->y1 += offset_y;
+}
diff --git a/src/include/libplacebo/common.h b/src/include/libplacebo/common.h
index 8b5cb4ae3d785c612596636b7d318706f231c9f0..8d8a79c44c10f90bce287450ded78b44d89aead6 100644
--- a/src/include/libplacebo/common.h
+++ b/src/include/libplacebo/common.h
@@ -62,6 +62,10 @@ struct pl_rect3df {
 void pl_rect2d_normalize(struct pl_rect2d *rc);
 void pl_rect3d_normalize(struct pl_rect3d *rc);
 
+// Return the rounded form of a rect.
+struct pl_rect2d pl_rect2df_round(const struct pl_rect2df *rc);
+struct pl_rect3d pl_rect3df_round(const struct pl_rect3df *rc);
+
 // Represents a row-major matrix, i.e. the following matrix
 //     [ a11 a12 a13 ]
 //     [ a21 a22 a23 ]
@@ -136,4 +140,49 @@ extern const struct pl_transform2x2 pl_transform2x2_identity;
 void pl_transform2x2_apply(const struct pl_transform2x2 *t, float vec[2]);
 void pl_transform2x2_apply_rc(const struct pl_transform2x2 *t, struct pl_rect2df *rc);
 
+// Helper functions for dealing with aspect ratios and stretched/scaled rects.
+
+// Return the (absolute) aspect ratio (width/height) of a given pl_rect2df.
+// This will always be a positive number, even if `rc` is flipped.
+float pl_rect2df_aspect(const struct pl_rect2df *rc);
+
+// Set the aspect of a `rc` to a given aspect ratio with an extra 'panscan'
+// factor choosing the balance between shrinking and growing the `rc` to meet
+// this aspect ratio. If `panscan` is 0.0, this function will only ever shrink
+// the rc . If `panscan` is 1.0, this function will only ever grow the `rc`.
+void pl_rect2df_aspect_set(struct pl_rect2df *rc, float aspect, float panscan);
+
+// Set one rect's aspect to that of another
+#define pl_rect2df_aspect_copy(rc, src, panscan) \
+    pl_rect2df_aspect_set((rc), pl_rect2df_aspect(src), (panscan))
+
+// 'Fit' one rect inside another. `rc` will be set to the same size and aspect
+// ratio as `src`, but with the size limited to fit inside the original `rc`.
+// Like `pl_rect2df_aspect_set`, `panscan` controls the pan&scan factor.
+void pl_rect2df_aspect_fit(struct pl_rect2df *rc, const struct pl_rect2df *src,
+                           float panscan);
+
+// Scale rect in each direction while keeping it centered.
+void pl_rect2df_stretch(struct pl_rect2df *rc, float stretch_x, float stretch_y);
+
+// Offset rect by an arbitrary offset factor. If the corresponding dimension
+// of a rect is flipped, so too is the applied offset.
+void pl_rect2df_offset(struct pl_rect2df *rc, float offset_x, float offset_y);
+
+// Scale a rect uniformly in both dimensions.
+#define pl_rect2df_zoom(rc, zoom) pl_rect2df_stretch((rc), (zoom), (zoom))
+
+// Variants of the functions above that operate directly on rounded rects.
+// Note: Applying multiple of these operations compounds rounding error in each
+// step. Consider doing the calculations on pl_rect2df and rounding at the end.
+float pl_rect2d_aspect(const struct pl_rect2d *rc);
+void pl_rect2d_aspect_set(struct pl_rect2d *rc, float aspect, float panscan);
+#define pl_rect2d_aspect_copy(rc, src, panscan) \
+    pl_rect2d_aspect_set((rc), pl_rect2df_aspect(src), (panscan))
+void pl_rect2d_aspect_fit(struct pl_rect2d *rc, const struct pl_rect2df *src,
+                          float panscan);
+void pl_rect2d_stretch(struct pl_rect2d *rc, float stretch_x, float stretch_y);
+void pl_rect2d_offset(struct pl_rect2d *rc, int offset_x, int offset_y);
+#define pl_rect2d_zoom(rc, zoom) pl_rect2d_stretch((rc), (zoom), (zoom))
+
 #endif // LIBPLACEBO_COMMON_H_
diff --git a/src/include/libplacebo/renderer.h b/src/include/libplacebo/renderer.h
index 0914136e99d0865d53c14b70649b70a997c4da10..11d7012494dbd6d6370d02fcd0425c82f044f7da 100644
--- a/src/include/libplacebo/renderer.h
+++ b/src/include/libplacebo/renderer.h
@@ -410,6 +410,11 @@ struct pl_render_target {
 void pl_render_target_from_swapchain(struct pl_render_target *out_target,
                                      const struct pl_swapchain_frame *frame);
 
+// Helper function to determine if the `target` covers the entire FBO or not.
+// If this returns true, users may want to `pl_tex_clear` the `target.fbo`
+// before calling `pl_render_image`.
+bool pl_render_target_partial(const struct pl_render_target *target);
+
 // Render a single image to a target using the given parameters. This is
 // fully dynamic, i.e. the params can change at any time. libplacebo will
 // internally detect and flush whatever caches are invalidated as a result of
diff --git a/src/renderer.c b/src/renderer.c
index 58d9749ae036fb90a17acabcc6fb32a14c726a15..0fc212940543db0574c73274acf1a0de8e401edd 100644
--- a/src/renderer.c
+++ b/src/renderer.c
@@ -1652,3 +1652,20 @@ void pl_render_target_from_swapchain(struct pl_render_target *out_target,
     if (frame->flipped)
         PL_SWAP(out_target->dst_rect.y0, out_target->dst_rect.y1);
 }
+
+bool pl_render_target_partial(const struct pl_render_target *target)
+{
+    int x0 = PL_MIN(target->dst_rect.x0, target->dst_rect.x1),
+        y0 = PL_MIN(target->dst_rect.y0, target->dst_rect.y1),
+        x1 = PL_MAX(target->dst_rect.x0, target->dst_rect.x1),
+        y1 = PL_MAX(target->dst_rect.y0, target->dst_rect.y1),
+        fbo_w = target->fbo->params.w,
+        fbo_h = target->fbo->params.h;
+
+    if (!x0 && !x1)
+        x1 = fbo_w;
+    if (!y0 && !y1)
+        y1 = fbo_h;
+
+    return x0 > 0 || y0 > 0 || x1 < fbo_w || y1 < fbo_h;
+}
diff --git a/src/tests/context.c b/src/tests/context.c
index 4921988f20e88fced6ac92d03cc47c9b864de19b..970e176d5d992c3319f84588371a512a2d84db38 100644
--- a/src/tests/context.c
+++ b/src/tests/context.c
@@ -50,8 +50,46 @@ int main()
     for (int i = 0; i < 3; i++) {
         for (int j = 0; j < 3; j++) {
             printf("%f %f\n", tr.mat.m[i][j], tr2.mat.m[i][j]);
-            REQUIRE(fabs(tr.mat.m[i][j] - tr2.mat.m[i][j]) < 1e-4);
+            REQUIRE(feq(tr.mat.m[i][j], tr2.mat.m[i][j], 1e-4));
         }
-        REQUIRE(fabs(tr.c[i] - tr2.c[i]) < 1e-4);
+        REQUIRE(feq(tr.c[i], tr2.c[i], 1e-4));
     }
+
+    // Test aspect ratio code
+    const struct pl_rect2df rc1080p = {0, 0, 1920, 1080};
+    const struct pl_rect2df rc43 = {0, 0, 1024, 768};
+    struct pl_rect2df rc;
+
+    REQUIRE(feq(pl_rect2df_aspect(&rc1080p), 16.0/9.0, 1e-8));
+    REQUIRE(feq(pl_rect2df_aspect(&rc43), 4.0/3.0, 1e-8));
+
+#define pl_rect2df_midx(rc) (((rc).x0 + (rc).x1) / 2.0)
+#define pl_rect2df_midy(rc) (((rc).y0 + (rc).y1) / 2.0)
+
+    for (float aspect = 0.2; aspect < 3.0; aspect += 0.4) {
+        for (float scan = 0.0; scan <= 1.0; scan += 0.5) {
+            rc = rc1080p;
+            pl_rect2df_aspect_set(&rc, aspect, scan);
+            printf("aspect %.2f, panscan %.1f: {%f %f} -> {%f %f}\n",
+                   aspect, scan, rc.x0, rc.y0, rc.x1, rc.y1);
+            REQUIRE(feq(pl_rect2df_aspect(&rc), aspect, 1e-6));
+            REQUIRE(feq(pl_rect2df_midx(rc), pl_rect2df_midx(rc1080p), 1e-6));
+            REQUIRE(feq(pl_rect2df_midy(rc), pl_rect2df_midy(rc1080p), 1e-6));
+        }
+    }
+
+    rc = rc1080p;
+    pl_rect2df_aspect_fit(&rc, &rc43, 0.0);
+    REQUIRE(feq(pl_rect2df_aspect(&rc), pl_rect2df_aspect(&rc43), 1e-6));
+    REQUIRE(feq(pl_rect2df_midx(rc), pl_rect2df_midx(rc1080p), 1e-6));
+    REQUIRE(feq(pl_rect2df_midy(rc), pl_rect2df_midy(rc1080p), 1e-6));
+    REQUIRE(feq(pl_rect_w(rc), pl_rect_w(rc43), 1e-6));
+    REQUIRE(feq(pl_rect_h(rc), pl_rect_h(rc43), 1e-6));
+
+    rc = rc43;
+    pl_rect2df_aspect_fit(&rc, &rc1080p, 0.0);
+    REQUIRE(feq(pl_rect2df_aspect(&rc), pl_rect2df_aspect(&rc1080p), 1e-6));
+    REQUIRE(feq(pl_rect2df_midx(rc), pl_rect2df_midx(rc43), 1e-6));
+    REQUIRE(feq(pl_rect2df_midy(rc), pl_rect2df_midy(rc43), 1e-6));
+    REQUIRE(feq(pl_rect_w(rc), pl_rect_w(rc43), 1e-6));
 }