diff --git a/libavutil/Makefile b/libavutil/Makefile
index d0632f16a6..59b8d52e04 100644
--- a/libavutil/Makefile
+++ b/libavutil/Makefile
@@ -207,6 +207,7 @@ TESTPROGS = adler32                                                     \
             hash                                                        \
             hmac                                                        \
             hwdevice                                                    \
+            hwframes                                                    \
             integer                                                     \
             imgutils                                                    \
             lfg                                                         \
diff --git a/libavutil/tests/hwframes.c b/libavutil/tests/hwframes.c
new file mode 100644
index 0000000000..32e3cdbdf9
--- /dev/null
+++ b/libavutil/tests/hwframes.c
@@ -0,0 +1,404 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+
+#include "libavutil/hwcontext.h"
+#include "libavutil/imgutils.h"
+#include "libavutil/pixdesc.h"
+
+static int test_frames_transfer(AVBufferRef *frames_ref)
+{
+    AVHWFramesContext *frames = (AVHWFramesContext*)frames_ref->data;
+    const AVPixFmtDescriptor *desc;
+    AVFrame *src, *hw, *dst;
+    int planes, line_width[4], plane_height[4];
+    int c, p, x, y, err;
+    uint8_t val;
+
+    desc = av_pix_fmt_desc_get(frames->sw_format);
+    if (!desc)
+        return -1;
+
+    err = av_image_fill_linesizes(line_width,
+                                  frames->sw_format, frames->width);
+    if (err < 0) {
+        fprintf(stderr, "Failed to determine line widths for format %s.\n",
+                desc->name);
+        return -1;
+    }
+    planes = 0;
+    for (c = 0; c < desc->nb_components; c++)
+        planes = FFMAX(planes, desc->comp[c].plane + 1);
+    for (p = 0; p < planes; p++) {
+        plane_height[p] = AV_CEIL_RSHIFT(frames->height,
+                                         desc->log2_chroma_h);
+    }
+
+    src = av_frame_alloc();
+    hw  = av_frame_alloc();
+    dst = av_frame_alloc();
+    if (!src || !hw || !dst) {
+        fprintf(stderr, "Failed to allocate frames.\n");
+        err = -1;
+        goto fail;
+    }
+
+    src->format = dst->format = frames->sw_format;
+    src->width  = dst->width  = frames->width;
+    src->height = dst->height = frames->height;
+
+    err = av_frame_get_buffer(src, 0);
+    if (err < 0) {
+        fprintf(stderr, "Failed to allocate source frame data.\n");
+        err = -1;
+        goto fail;
+    }
+    err = av_hwframe_get_buffer(frames_ref, hw, 0);
+    if (err < 0) {
+        fprintf(stderr, "Failed to allocate hardware frame.\n");
+        err = -1;
+        goto fail;
+    }
+    err = av_frame_get_buffer(dst, 0);
+    if (err < 0) {
+        fprintf(stderr, "Failed to allocate destination frame data.\n");
+        err = -1;
+        goto fail;
+    }
+
+    val = 0;
+    for (p = 0; p < planes; p++) {
+        for (y = 0; y < plane_height[p]; y++) {
+            for (x = 0; x < line_width[p]; x++) {
+                src->data[p][y * src->linesize[p] + x] = val;
+                val += 127;
+            }
+        }
+    }
+
+    err = av_hwframe_transfer_data(hw, src, 0);
+    if (err < 0) {
+        fprintf(stderr, "Failed to transfer data to hardware frame: %d.\n",
+                err);
+        err = -1;
+        goto fail;
+    }
+
+    err = av_hwframe_transfer_data(dst, hw, 0);
+    if (err < 0) {
+        fprintf(stderr, "Failed to transfer data to hardware frame: %d.\n",
+                err);
+        err = -1;
+        goto fail;
+    }
+
+    for (p = 0; p < planes; p++) {
+        for (y = 0; y < plane_height[p]; y++) {
+            for (x = 0; x < line_width[p]; x++) {
+                if (src->data[p][y * src->linesize[p] + x] !=
+                    dst->data[p][y * dst->linesize[p] + x]) {
+                    fprintf(stderr, "Downloaded frame does not match "
+                            "uploaded frame: differs at plane %d line %d "
+                            "byte %d: wrote %d read %d.\n", p, y, x,
+                            src->data[p][y * src->linesize[p] + x],
+                            dst->data[p][y * dst->linesize[p] + x]);
+                    err = -1;
+                    goto fail;
+                }
+            }
+        }
+    }
+
+    fprintf(stderr, "Transfer successful for %s %dx%d.\n",
+            desc->name, frames->width, frames->height);
+
+fail:
+    av_frame_free(&src);
+    av_frame_free(&hw);
+    av_frame_free(&dst);
+    return err;
+}
+
+static void test_free_function(AVHWFramesContext *frames)
+{
+    int *free_check = frames->user_opaque;
+
+    *free_check = 1;
+}
+
+static int test_format(AVBufferRef *device_ref, enum AVPixelFormat hw_format,
+                       enum AVPixelFormat sw_format, int expect_failure)
+{
+    AVBufferRef *frames_ref;
+    AVHWFramesContext *frames;
+    const AVPixFmtDescriptor *hw_desc, *sw_desc;
+    int err, free_check;
+
+    hw_desc = av_pix_fmt_desc_get(hw_format);
+    sw_desc = av_pix_fmt_desc_get(sw_format);
+    if (!hw_desc || !sw_desc) {
+        fprintf(stderr, "Frame format %d / %d is invalid.\n",
+                hw_format, sw_format);
+        return -1;
+    }
+
+    frames_ref = av_hwframe_ctx_alloc(device_ref);
+    if (!frames_ref) {
+        fprintf(stderr, "Failed to allocate frames context.\n");
+        return -1;
+    }
+
+    frames = (AVHWFramesContext*)frames_ref->data;
+
+    frames->format    = hw_format;
+    frames->sw_format = sw_format;
+
+    frames->width  = 640;
+    frames->height = 480;
+
+    free_check = 0;
+    frames->free = &test_free_function;
+    frames->user_opaque = &free_check;
+
+    err = av_hwframe_ctx_init(frames_ref);
+    if (err < 0) {
+        av_buffer_unref(&frames_ref);
+        if (!expect_failure) {
+            fprintf(stderr, "Failed to create frames with format %s / %s.\n",
+                    hw_desc->name, sw_desc->name);
+            return -1;
+        } else {
+            return 0;
+        }
+    }
+    if (expect_failure) {
+        fprintf(stderr, "Successfully created frames with format %s / %s, "
+                "but expected to fail.\n", hw_desc->name, sw_desc->name);
+        // Continue anyway.
+    }
+
+    fprintf(stderr, "Frames created with format %s / %s.\n",
+            hw_desc->name, sw_desc->name);
+
+    err = test_frames_transfer(frames_ref);
+
+    if (free_check != 0) {
+        fprintf(stderr, "Free check value incorrect before freeing: %d.\n",
+                free_check);
+        err = -1;
+    }
+    av_buffer_unref(&frames_ref);
+    if (free_check != 1) {
+        fprintf(stderr, "Free check value incorrect after freeing: %d.\n",
+                free_check);
+        err = -1;
+    }
+
+    return err;
+}
+
+static enum AVPixelFormat test_format_list[] = {
+    // YUV 4:2:0 formats;
+    AV_PIX_FMT_YUV420P,
+    AV_PIX_FMT_NV12,
+    AV_PIX_FMT_YUV420P10,
+    AV_PIX_FMT_P010,
+    AV_PIX_FMT_YUV420P16,
+    AV_PIX_FMT_P016,
+    // YUV 4:2:2 formats.
+    AV_PIX_FMT_YUYV422,
+    AV_PIX_FMT_UYVY422,
+    AV_PIX_FMT_YUV422P,
+    AV_PIX_FMT_YUV422P10,
+    AV_PIX_FMT_YUV422P16,
+    // YUV 4:4:4 formats.
+    AV_PIX_FMT_YUV444P,
+    AV_PIX_FMT_YUV444P10,
+    AV_PIX_FMT_YUV444P16,
+    // Packed RGB / RGBA.
+    AV_PIX_FMT_RGBA,
+    AV_PIX_FMT_BGRA,
+    AV_PIX_FMT_RGB0,
+    AV_PIX_FMT_BGR0,
+    // Planar RGB,
+    AV_PIX_FMT_GBRP,
+    AV_PIX_FMT_GBRP10,
+    AV_PIX_FMT_GBRP16,
+    // A valid format which won't be supported in hardware.
+    AV_PIX_FMT_PAL8,
+};
+
+static int test_device(enum AVHWDeviceType type, const char *name,
+                       const char *device, AVDictionary *opts, int flags)
+{
+    AVBufferRef *ref;
+    AVHWFramesConstraints *constraints = NULL;
+    enum AVPixelFormat hw_format, sw_format;
+    int i, j, k, failures, err;
+
+    err = av_hwdevice_ctx_create(&ref, type, device, opts, flags);
+    if (err < 0) {
+        fprintf(stderr, "Failed to create %s device: %d.\n", name, err);
+        return 1;
+    }
+
+    constraints = av_hwdevice_get_hwframe_constraints(ref, NULL);
+    if (!constraints || !constraints->valid_hw_formats) {
+        fprintf(stderr, "Device does not implement constraints.\n");
+        err = 1;
+        goto done;
+    }
+
+    failures = 0;
+    for (i = 0; constraints->valid_hw_formats[i] != AV_PIX_FMT_NONE; i++) {
+        hw_format = constraints->valid_hw_formats[i];
+
+        if (constraints->valid_sw_formats) {
+            for (j = 0; constraints->valid_sw_formats[j] !=
+                        AV_PIX_FMT_NONE; j++) {
+                sw_format = constraints->valid_sw_formats[j];
+
+                err = test_format(ref, hw_format, sw_format, 0);
+                if (err < 0)
+                    ++failures;
+            }
+        }
+        for (j = 0; j < FF_ARRAY_ELEMS(test_format_list); j++) {
+            sw_format = test_format_list[j];
+            for (k = 0; constraints->valid_sw_formats[k] !=
+                        AV_PIX_FMT_NONE; k++) {
+                if (constraints->valid_sw_formats[k] == sw_format)
+                    sw_format = AV_PIX_FMT_NONE;
+            }
+            if (sw_format == AV_PIX_FMT_NONE)
+                continue;
+
+            err = test_format(ref, hw_format, sw_format, 1);
+            if (err < 0)
+                ++failures;
+        }
+    }
+
+    err = failures ? -1 : 0;
+done:
+    av_hwframe_constraints_free(&constraints);
+    av_buffer_unref(&ref);
+    return err;
+}
+
+static const struct {
+    enum AVHWDeviceType type;
+    const char *possible_devices[5];
+} test_devices[] = {
+    { AV_HWDEVICE_TYPE_CUDA,
+      { "0", "1", "2" } },
+    { AV_HWDEVICE_TYPE_DRM,
+      { "/dev/dri/card0", "/dev/dri/card1",
+        "/dev/dri/renderD128", "/dev/dri/renderD129" } },
+    { AV_HWDEVICE_TYPE_DXVA2,
+      { "0", "1", "2" } },
+    { AV_HWDEVICE_TYPE_D3D11VA,
+      { "0", "1", "2" } },
+    { AV_HWDEVICE_TYPE_OPENCL,
+      { "0.0", "0.1", "1.0", "1.1" } },
+    { AV_HWDEVICE_TYPE_VAAPI,
+      { "/dev/dri/renderD128", "/dev/dri/renderD129", ":0" } },
+};
+
+static int test_device_type(enum AVHWDeviceType type)
+{
+    enum AVHWDeviceType check;
+    const char *name;
+    int i, j, found, err;
+
+    name = av_hwdevice_get_type_name(type);
+    if (!name) {
+        fprintf(stderr, "No name available for device type %d.\n", type);
+        return -1;
+    }
+
+    check = av_hwdevice_find_type_by_name(name);
+    if (check != type) {
+        fprintf(stderr, "Type %d maps to name %s maps to type %d.\n",
+               type, name, check);
+        return -1;
+    }
+
+    found = 0;
+
+    err = test_device(type, name, NULL, NULL, 0);
+    if (err < 0) {
+        fprintf(stderr, "Test failed for %s with default options.\n", name);
+        return -1;
+    }
+    if (err == 0) {
+        fprintf(stderr, "Test passed for %s with default options.\n", name);
+        ++found;
+    }
+
+    for (i = 0; i < FF_ARRAY_ELEMS(test_devices); i++) {
+        if (test_devices[i].type != type)
+            continue;
+
+        for (j = 0; test_devices[i].possible_devices[j]; j++) {
+            err = test_device(type, name,
+                              test_devices[i].possible_devices[j],
+                              NULL, 0);
+            if (err < 0) {
+                fprintf(stderr, "Test failed for %s with device %s.\n",
+                       name, test_devices[i].possible_devices[j]);
+                return -1;
+            }
+            if (err == 0) {
+                fprintf(stderr, "Test passed for %s with device %s.\n",
+                        name, test_devices[i].possible_devices[j]);
+                ++found;
+            }
+        }
+    }
+
+    return !found;
+}
+
+int main(void)
+{
+    enum AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;
+    int pass, fail, skip, err;
+
+    pass = fail = skip = 0;
+    while (1) {
+        type = av_hwdevice_iterate_types(type);
+        if (type == AV_HWDEVICE_TYPE_NONE)
+            break;
+
+        err = test_device_type(type);
+        if (err == 0)
+            ++pass;
+        else if (err < 0)
+            ++fail;
+        else
+            ++skip;
+    }
+
+    fprintf(stderr, "Attempted to test %d device types: "
+            "%d passed, %d failed, %d skipped.\n",
+            pass + fail + skip, pass, fail, skip);
+
+    return fail > 0;
+}
diff --git a/tests/fate/hw.mak b/tests/fate/hw.mak
index d606cdeab6..a46b289b67 100644
--- a/tests/fate/hw.mak
+++ b/tests/fate/hw.mak
@@ -3,4 +3,9 @@ fate-hwdevice: libavutil/tests/hwdevice$(EXESUF)
 fate-hwdevice: CMD = run libavutil/tests/hwdevice
 fate-hwdevice: CMP = null
 
+FATE_HWCONTEXT += fate-hwframes
+fate-hwframes: libavutil/tests/hwframes$(EXESUF)
+fate-hwframes: CMD = run libavutil/tests/hwframes
+fate-hwframes: CMP = null
+
 FATE_HW-$(CONFIG_AVUTIL) += $(FATE_HWCONTEXT)
