diff --git a/include/libcamera/internal/formats.h b/include/libcamera/internal/formats.h
index f59ac8f..ebd256c 100644
--- a/include/libcamera/internal/formats.h
+++ b/include/libcamera/internal/formats.h
@@ -32,6 +32,12 @@ private:
 	std::map<unsigned int, std::vector<SizeRange>> data_;
 };
 
+struct PixelFormatPlane
+{
+	unsigned int bytesPerGroup;
+	unsigned int verticalSubSampling;
+};
+
 class PixelFormatInfo
 {
 public:
@@ -45,13 +51,16 @@ public:
 
 	static const PixelFormatInfo &info(const PixelFormat &format);
 
-	/* \todo Add support for non-contiguous memory planes */
 	const char *name;
 	PixelFormat format;
 	V4L2PixelFormat v4l2Format;
 	unsigned int bitsPerPixel;
 	enum ColourEncoding colourEncoding;
 	bool packed;
+
+	unsigned int pixelsPerGroup;
+
+	std::array<PixelFormatPlane, 3> planes;
 };
 
 } /* namespace libcamera */
diff --git a/src/libcamera/formats.cpp b/src/libcamera/formats.cpp
index d3b722c..6bdd28d 100644
--- a/src/libcamera/formats.cpp
+++ b/src/libcamera/formats.cpp
@@ -110,6 +110,22 @@ const std::map<unsigned int, std::vector<SizeRange>> &ImageFormats::data() const
 	return data_;
 }
 
+/**
+ * \class PixelFormatPlane
+ * \brief Information about a single plane of a pixel format
+ *
+ * \var PixelFormatPlane::bytesPerGroup
+ * \brief The number of bytes that a pixel group consumes
+ *
+ * \sa PixelFormatInfo::pixelsPerGroup
+ *
+ * \var PixelFormatPlane::verticalSubSampling
+ * \brief Vertical subsampling multiplier
+ *
+ * This value is the ratio between the number of rows of pixels in the frame
+ * to the number of rows of pixels in the plane.
+ */
+
 /**
  * \class PixelFormatInfo
  * \brief Information about pixel formats
@@ -152,6 +168,26 @@ const std::map<unsigned int, std::vector<SizeRange>> &ImageFormats::data() const
  * bytes. For instance, 12-bit Bayer data with two pixels stored in three bytes
  * is packed, while the same data stored with 4 bits of padding in two bytes
  * per pixel is not packed.
+ *
+ * \var PixelFormatInfo::pixelsPerGroup
+ * \brief The number of pixels in a pixel group
+ *
+ * The minimum number of pixels (including padding) necessary in a row
+ * when the frame has only one column of effective pixels
+ *
+ * A pixel group is defined as the minimum number of pixels (including padding)
+ * necessary in a row when the image has only one column of effective pixels.
+ * pixelsPerGroup refers to this value. bytesPerGroup, then, refers to the
+ * number of bytes that a pixel group consumes. This definition of a pixel
+ * group allows simple calculation of stride, as
+ * ceil(width / pixelsPerGroup) * bytesPerGroup. These values are determined
+ * only in terms of a row, and include bytes that are used in all planes (for
+ * multiplanar formats).
+ *
+ * \var PixelFormatInfo::planes
+ * \brief Information about pixels for each plane
+ *
+ * \sa PixelFormatPlane
  */
 
 /**
@@ -179,6 +215,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 24,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::RGB888, {
 		.name = "RGB888",
@@ -187,6 +225,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 24,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::ABGR8888, {
 		.name = "ABGR8888",
@@ -195,6 +235,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 32,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::ARGB8888, {
 		.name = "ARGB8888",
@@ -203,6 +245,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 32,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::BGRA8888, {
 		.name = "BGRA8888",
@@ -211,6 +255,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 32,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::RGBA8888, {
 		.name = "RGBA8888",
@@ -219,6 +265,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 32,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 
 	/* YUV packed formats. */
@@ -229,6 +277,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::YVYU, {
 		.name = "YVYU",
@@ -237,6 +287,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::UYVY, {
 		.name = "UYVY",
@@ -245,6 +297,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::VYUY, {
 		.name = "VYUY",
@@ -253,6 +307,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 
 	/* YUV planar formats. */
@@ -263,6 +319,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 2, 2 }, { 0, 0 } }},
 	} },
 	{ formats::NV21, {
 		.name = "NV21",
@@ -271,6 +329,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 2, 2 }, { 0, 0 } }},
 	} },
 	{ formats::NV16, {
 		.name = "NV16",
@@ -279,6 +339,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 2, 1 }, { 0, 0 } }},
 	} },
 	{ formats::NV61, {
 		.name = "NV61",
@@ -287,6 +349,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 2, 1 }, { 0, 0 } }},
 	} },
 	{ formats::NV24, {
 		.name = "NV24",
@@ -295,6 +359,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 24,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 1, 1 }, { 2, 1 }, { 0, 0 } }},
 	} },
 	{ formats::NV42, {
 		.name = "NV42",
@@ -303,6 +369,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 24,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 1, 1 }, { 2, 1 }, { 0, 0 } }},
 	} },
 	{ formats::YUV420, {
 		.name = "YUV420",
@@ -311,6 +379,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 1, 2 }, { 1, 2 } }},
 	} },
 	{ formats::YUV422, {
 		.name = "YUV422",
@@ -319,6 +389,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 1, 1 }, { 1, 1 } }},
 	} },
 
 	/* Greyscale formats. */
@@ -329,6 +401,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 8,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 1, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 
 	/* Bayer formats. */
@@ -339,6 +413,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 8,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGBRG8, {
 		.name = "SGBRG8",
@@ -347,6 +423,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 8,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGRBG8, {
 		.name = "SGRBG8",
@@ -355,6 +433,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 8,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SRGGB8, {
 		.name = "SRGGB8",
@@ -363,6 +443,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 8,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SBGGR10, {
 		.name = "SBGGR10",
@@ -371,6 +453,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGBRG10, {
 		.name = "SGBRG10",
@@ -379,6 +463,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGRBG10, {
 		.name = "SGRBG10",
@@ -387,6 +473,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SRGGB10, {
 		.name = "SRGGB10",
@@ -395,6 +483,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SBGGR10_CSI2P, {
 		.name = "SBGGR10_CSI2P",
@@ -403,6 +493,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 4,
+		.planes = {{ { 5, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGBRG10_CSI2P, {
 		.name = "SGBRG10_CSI2P",
@@ -411,6 +503,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 4,
+		.planes = {{ { 5, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGRBG10_CSI2P, {
 		.name = "SGRBG10_CSI2P",
@@ -419,6 +513,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 4,
+		.planes = {{ { 5, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SRGGB10_CSI2P, {
 		.name = "SRGGB10_CSI2P",
@@ -427,6 +523,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 4,
+		.planes = {{ { 5, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SBGGR12, {
 		.name = "SBGGR12",
@@ -435,6 +533,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGBRG12, {
 		.name = "SGBRG12",
@@ -443,6 +543,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGRBG12, {
 		.name = "SGRBG12",
@@ -451,6 +553,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SRGGB12, {
 		.name = "SRGGB12",
@@ -459,6 +563,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = false,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SBGGR12_CSI2P, {
 		.name = "SBGGR12_CSI2P",
@@ -467,6 +573,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGBRG12_CSI2P, {
 		.name = "SGBRG12_CSI2P",
@@ -475,6 +583,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGRBG12_CSI2P, {
 		.name = "SGRBG12_CSI2P",
@@ -483,6 +593,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SRGGB12_CSI2P, {
 		.name = "SRGGB12_CSI2P",
@@ -491,6 +603,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 12,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SBGGR10_IPU3, {
 		.name = "SBGGR10_IPU3",
@@ -499,6 +613,9 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		/* \todo remember to double this in the ipu3 pipeline handler */
+		.pixelsPerGroup = 25,
+		.planes = {{ { 32, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGBRG10_IPU3, {
 		.name = "SGBRG10_IPU3",
@@ -507,6 +624,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 25,
+		.planes = {{ { 32, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SGRBG10_IPU3, {
 		.name = "SGRBG10_IPU3",
@@ -515,6 +634,8 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 25,
+		.planes = {{ { 32, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 	{ formats::SRGGB10_IPU3, {
 		.name = "SRGGB10_IPU3",
@@ -523,16 +644,21 @@ const std::map<PixelFormat, PixelFormatInfo> pixelFormatInfo{
 		.bitsPerPixel = 10,
 		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
 		.packed = true,
+		.pixelsPerGroup = 25,
+		.planes = {{ { 32, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 
 	/* Compressed formats. */
+	/* \todo remember to fill this in from the pipeline handler. */
 	{ formats::MJPEG, {
 		.name = "MJPEG",
 		.format = formats::MJPEG,
 		.v4l2Format = V4L2PixelFormat(V4L2_PIX_FMT_MJPEG),
-		.bitsPerPixel = 0,
+		.bitsPerPixel = 16,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 1, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 };
 
