diff --git a/include/libcamera/geometry.h b/include/libcamera/geometry.h
index d7fdbe70..6aef1f39 100644
--- a/include/libcamera/geometry.h
+++ b/include/libcamera/geometry.h
@@ -8,6 +8,7 @@
 #pragma once
 
 #include <algorithm>
+#include <cmath>
 #include <ostream>
 #include <string>
 
@@ -49,6 +50,70 @@ static inline bool operator!=(const Point &lhs, const Point &rhs)
 
 std::ostream &operator<<(std::ostream &out, const Point &p);
 
+struct PointF {
+	constexpr PointF()
+		: x(0), y(0)
+	{
+	}
+
+	constexpr PointF(double _x, double _y)
+		: x(_x), y(_y)
+	{
+	}
+
+	constexpr PointF operator-() const
+	{
+		return PointF{ -x, -y };
+	}
+
+	constexpr PointF operator-(const PointF &p) const
+	{
+		return PointF(x - p.x, y - p.y);
+	}
+
+	constexpr PointF operator+(const PointF &p) const
+	{
+		return PointF(x + p.x, y + p.y);
+	}
+
+	constexpr double operator%(const PointF &p) const
+	{
+		return x * p.x + y * p.y;
+	}
+
+	constexpr PointF operator*(double f) const
+	{
+		return PointF(x * f, y * f);
+	}
+
+	constexpr PointF operator/(double f) const
+	{
+		return PointF(x / f, y / f);
+	}
+
+	constexpr double len2() const
+	{
+		return x * x + y * y;
+	}
+
+	constexpr double len() const
+	{
+		return std::sqrt(len2());
+	}
+
+	const std::string toString() const;
+
+	double x, y;
+};
+
+bool operator==(const PointF &lhs, const PointF &rhs);
+static inline bool operator!=(const PointF &lhs, const PointF &rhs)
+{
+	return !(lhs == rhs);
+}
+
+std::ostream &operator<<(std::ostream &out, const PointF &p);
+
 class Size
 {
 public:
diff --git a/src/libcamera/geometry.cpp b/src/libcamera/geometry.cpp
index 8d85b758..90b81282 100644
--- a/src/libcamera/geometry.cpp
+++ b/src/libcamera/geometry.cpp
@@ -21,7 +21,7 @@ namespace libcamera {
 
 /**
  * \class Point
- * \brief Describe a point in two-dimensional space
+ * \brief Describe a point in two-dimensional integer space
  *
  * The Point structure defines a point in two-dimensional space with integer
  * precision. The coordinates of a Point may be negative as well as positive.
@@ -94,6 +94,127 @@ std::ostream &operator<<(std::ostream &out, const Point &p)
 	return out;
 }
 
+/**
+ * \class PointF
+ * \brief Describe a point in two-dimensional real space
+ *
+ * The Point structure defines a point in two-dimensional space with double
+ * precision. The coordinates of a Point may be negative as well as positive.
+ *
+ * This class exists separately from Point to not require all users of the
+ * Point class to have to use template parameters to specify a type.
+ */
+
+/**
+ * \fn PointF::PointF()
+ * \copydoc libcamera::Point::Point
+ */
+
+/**
+ * \fn PointF::PointF(double _x, double _y)
+ * \brief Construct a PointF at given \a _x and \a _y values
+ * \param[in] _x The x-coordinate
+ * \param[in] _y The y-coordinate
+ */
+
+/**
+ * \var PointF::x
+ * \copydoc libcamera::Point::x
+ */
+
+/**
+ * \var PointF::y
+ * \copydoc libcamera::Point::y
+ */
+
+/**
+ * \fn constexpr PointF PointF::operator-() const
+ * \copydoc libcamera::Point::operator-
+ */
+
+/**
+ * \fn constexpr PointF PointF::operator-(PointF const &p) const
+ * \brief Subtract one point from another, as if they were vectors
+ * \param[in] p The other point
+ * \return The difference of p from this point
+ */
+
+/**
+ * \fn PointF::operator+()
+ * \brief Add two points together, as if they were vectors
+ * \param[in] p The other point
+ * \return The sum of the two points
+ */
+
+/**
+ * \fn PointF::operator%()
+ * \brief Compute the dot product, treating the points as vectors
+ * \param[in] p The other point
+ * \return The dot product of the two points
+ */
+
+/**
+ * \fn PointF::operator*()
+ * \brief Scale up the point, as if it were a vector
+ * \param[in] f The factor
+ * \return The scaled point
+ */
+
+/**
+ * \fn PointF::operator/()
+ * \brief Scale down the point, as if it were a vector
+ * \param[in] f The factor
+ * \return The scaled point
+ */
+
+/**
+ * \fn PointF::len2()
+ * \brief Get the squared length of the point, as if it were a vector
+ * \return The squared length of the point
+ */
+
+/**
+ * \fn PointF::len()
+ * \brief Get the length of the point, as if it were a vector
+ * \return The length of the point
+ */
+
+/**
+ * \copydoc Point::toString()
+ */
+const std::string PointF::toString() const
+{
+	std::stringstream ss;
+	ss << *this;
+
+	return ss.str();
+}
+
+/**
+ * \copydoc operator==(const Point &lhs, const Point &rhs)
+ */
+bool operator==(const PointF &lhs, const PointF &rhs)
+{
+	return lhs.x == rhs.x && lhs.y == rhs.y;
+}
+
+/**
+ * \fn bool operator!=(const Point &lhs, const Point &rhs)
+ * \copydoc libcamera::Point::operator!=
+ */
+
+/**
+ * \brief Insert a text representation of a PointF into an output stream
+ * \param[in] out The output stream
+ * \param[in] p The point
+ * \return The output stream \a out
+ */
+std::ostream &operator<<(std::ostream &out, const PointF &p)
+{
+	out << "(" << p.x << ", " << p.y << ")";
+	return out;
+}
+
 /**
  * \class Size
  * \brief Describe a two-dimensional size
diff --git a/test/geometry.cpp b/test/geometry.cpp
index 008d51ea..7f1f5b1a 100644
--- a/test/geometry.cpp
+++ b/test/geometry.cpp
@@ -33,6 +33,53 @@ protected:
 		return true;
 	}
 
+	template<typename T, typename U, typename V>
+	bool compareF(const T &lhs, const U &rhs,
+		      V (PointF::*op)(const U &) const,
+		      const char *opName, V expect)
+	{
+		V result = (lhs.*op)(rhs);
+
+		if (result != expect) {
+			cout << lhs << opName << " " << rhs
+			     << "test failed" << std::endl;
+			return false;
+		}
+
+		return true;
+	}
+
+	template<typename T, typename U, typename V>
+	bool compareFScale(const T &lhs, const U &rhs,
+			   V (PointF::*op)(U) const,
+			   const char *opName, V expect)
+	{
+		V result = (lhs.*op)(rhs);
+
+		if (result != expect) {
+			cout << lhs << opName << " " << rhs
+			     << "test failed" << std::endl;
+			return false;
+		}
+
+		return true;
+	}
+
+	template<typename T>
+	bool compareFLen(const T &lhs, double (PointF::*op)() const,
+			 const char *opName, double expect)
+	{
+		double result = (lhs.*op)();
+
+		if (result != expect) {
+			cout << lhs << opName
+			     << "test failed" << std::endl;
+			return false;
+		}
+
+		return true;
+	}
+
 	int run()
 	{
 		/*
@@ -88,6 +135,314 @@ protected:
 			return TestFail;
 		}
 
+		/*
+		 * PointF tests
+		 */
+
+		/* Equality */
+		if (!compare(PointF(50.1, 100.1), PointF(50.1, 100.1), &operator==, "==", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, 100.1), PointF(-50.1, 100.1), &operator==, "==", true))
+			return TestFail;
+
+		if (!compare(PointF(50.1, -100.1), PointF(50.1, -100.1), &operator==, "==", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, -100.1), PointF(-50.1, -100.1), &operator==, "==", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, -100.0), PointF(-50.1, -100), &operator==, "==", true))
+			return TestFail;
+
+		/* Inequality */
+		if (!compare(PointF(50.1, 100.1), PointF(50.1, 100.2), &operator!=, "!=", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, 100.1), PointF(-50.1, 100.01), &operator!=, "!=", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, 100.1), PointF(-50.1, 100.1), &operator!=, "!=", false))
+			return TestFail;
+
+		if (!compare(PointF(50.1, -100.1), PointF(50.2, -100.1), &operator!=, "!=", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, -100.1), PointF(-50.01, -100.0), &operator!=, "!=", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, 100.1), PointF(50.1, 100.1), &operator!=, "!=", true))
+			return TestFail;
+
+		if (!compare(PointF(50.1, -100.1), PointF(50.1, 100.1), &operator!=, "!=", true))
+			return TestFail;
+
+		if (!compare(PointF(-50.1, -100.1), PointF(-50.1, -100.1), &operator!=, "!=", false))
+			return TestFail;
+
+		/* Negation */
+		if (PointF(50.1, 100.1) != -PointF(-50.1, -100.1) ||
+		    PointF(50.1, 100.1) == -PointF(50.1, -100.1) ||
+		    PointF(50.1, 100.1) == -PointF(-50.1, 100.1)) {
+			cout << "PointF negation test failed" << endl;
+			return TestFail;
+		}
+
+		typedef PointF (PointF::*pointfOp)(const PointF &) const;
+		typedef double (PointF::*pointfDotProd)(const PointF &) const;
+		typedef PointF (PointF::*pointfScale)(double) const;
+		typedef double (PointF::*pointfLen)() const;
+
+		/* Subtraction */
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(0, 0)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(-50.1, 100.1), PointF(-50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(0, 0)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, -100.1), PointF(50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(0, 0)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(-50.1, -100.1), PointF(-50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(0, 0)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(-50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(100.2, 0)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(0, 200.2)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(-50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(100.2, 200.2)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(-50.1, 100.1), PointF(50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator-), "-",
+			      PointF(-100.2, 0)))
+			return TestFail;
+
+		/* Addition */
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(100.2, 200.2)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(-50.1, 100.1), PointF(-50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(-100.2, 200.2)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, -100.1), PointF(50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(100.2, -200.2)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(-50.1, -100), PointF(-50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(-100.2, -200.1)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(-50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(0, 200.2)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.0), PointF(50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(100.2, -0.09999999999999432)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(50.1, 100.1), PointF(-50.1, -100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(0, 0)))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, PointF>(PointF(-50.1, 100.1), PointF(50.1, 100.1),
+			      static_cast<pointfOp>(&PointF::operator+), "+",
+			      PointF(0, 200.2)))
+			return TestFail;
+
+		/* Dot product */
+		if (!compareF<PointF, PointF, double>(PointF(50.1, 100.1), PointF(50.1, 100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      12530.019999999999))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(50.1, 100.1), PointF(50.1, -100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      -7509.999999999998))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(50.1, 100.1), PointF(-50.1, 100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      7509.999999999998))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(50.1, 100.1), PointF(-50.1, -100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      -12530.019999999999))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(-50.1, 100.1), PointF(-50.1, 100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      12530.019999999999))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(50.1, -100.1), PointF(50.1, -100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      12530.019999999999))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(-50.1, -100.1), PointF(-50.1, -100.1),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      12530.019999999999))
+			return TestFail;
+
+		if (!compareF<PointF, PointF, double>(PointF(0, 0), PointF(0, 0),
+			      static_cast<pointfDotProd>(&PointF::operator%), "%",
+			      0))
+			return TestFail;
+
+
+		/* Scaling up */
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 0,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(0, 0)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 1,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(10.5, -100.1)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 1.0,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(10.5, -100.1)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), -4.2,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(-44.1, 420.42)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 4.2,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(44.1, -420.42)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(0, -100.1), 4.2,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(0, -420.42)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(-50.1, -100.1), 4.2,
+				   static_cast<pointfScale>(&PointF::operator*), "*",
+				   PointF(-210.42000000000002, -420.42)))
+			return TestFail;
+
+		/* Scaling down */
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 1,
+				   static_cast<pointfScale>(&PointF::operator/), "/",
+				   PointF(10.5, -100.1)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 1.0,
+				   static_cast<pointfScale>(&PointF::operator/), "/",
+				   PointF(10.5, -100.1)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), -4.2,
+				   static_cast<pointfScale>(&PointF::operator/), "/",
+				   PointF(-2.5, 23.833333333333332)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(10.5, -100.1), 4.2,
+				   static_cast<pointfScale>(&PointF::operator/), "/",
+				   PointF(2.5, -23.833333333333332)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(0, -100.1), 4.2,
+				   static_cast<pointfScale>(&PointF::operator/), "/",
+				   PointF(0, -23.833333333333332)))
+			return TestFail;
+
+		if (!compareFScale<PointF, double, PointF>(PointF(-50.1, -100.1), 4.2,
+				   static_cast<pointfScale>(&PointF::operator/), "/",
+				   PointF(-11.928571428571429, -23.833333333333332)))
+			return TestFail;
+
+
+		/* Squared length */
+		if (!compareFLen<PointF>(PointF(0, 0),
+					 static_cast<pointfLen>(&PointF::len2), "len2",
+					 0))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(10.4, 0),
+					 static_cast<pointfLen>(&PointF::len2), "len2",
+					 108.16000000000001))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(10.4, 50.1),
+					 static_cast<pointfLen>(&PointF::len2), "len2",
+					 2618.17))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(-10.4, 50.1),
+					 static_cast<pointfLen>(&PointF::len2), "len2",
+					 2618.17))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(-10.4, -50.1),
+					 static_cast<pointfLen>(&PointF::len2), "len2",
+					 2618.17))
+			return TestFail;
+
+		/* Length */
+		if (!compareFLen<PointF>(PointF(0, 0),
+					 static_cast<pointfLen>(&PointF::len), "len",
+					 0))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(10.4, 0),
+					 static_cast<pointfLen>(&PointF::len), "len",
+					 10.4))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(10.4, 50.1),
+					 static_cast<pointfLen>(&PointF::len), "len",
+					 51.16805644149483))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(-10.4, 50.1),
+					 static_cast<pointfLen>(&PointF::len), "len",
+					 51.16805644149483))
+			return TestFail;
+
+		if (!compareFLen<PointF>(PointF(-10.4, -50.1),
+					 static_cast<pointfLen>(&PointF::len), "len",
+					 51.16805644149483))
+			return TestFail;
+
+		/* Default constructor */
+		if (PointF() != PointF(0, 0)) {
+			cout << "Default constructor test failed" << endl;
+			return TestFail;
+		}
+
 		/*
 		 * Size tests
 		 */
