diff --git a/src/ipa/rkisp1/algorithms/meson.build b/src/ipa/rkisp1/algorithms/meson.build
index 2e42a80cf99d..d329dbfb432d 100644
--- a/src/ipa/rkisp1/algorithms/meson.build
+++ b/src/ipa/rkisp1/algorithms/meson.build
@@ -14,4 +14,5 @@ rkisp1_ipa_algorithms = files([
     'gsl.cpp',
     'lsc.cpp',
     'lux.cpp',
+    'wdr.cpp',
 ])
diff --git a/src/ipa/rkisp1/algorithms/wdr.cpp b/src/ipa/rkisp1/algorithms/wdr.cpp
new file mode 100644
index 000000000000..0c1b261d4373
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/wdr.cpp
@@ -0,0 +1,510 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025, Ideas On Board
+ *
+ * RkISP1 Wide Dynamic Range control
+ */
+
+#include "wdr.h"
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include <libipa/histogram.h>
+#include <libipa/pwl.h>
+
+#include "linux/rkisp1-config.h"
+
+/**
+ * \file wdr.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+/**
+ * \class WideDynamicRange
+ * \brief RkISP1 Wide Dynamic Range algorithm
+ *
+ * This algorithm implements automatic global tone mapping for the RkISP1.
+ * Global tone mapping is done by the GWDR hardware block and applies
+ * a global tone mapping curve to the image to increase the perceived dynamic
+ * range. Imagine an indoor scene with bright outside visible through the
+ * windows. With normal exposure settings, the windows will be completely
+ * saturated and no structure (sky/clouds) will be visible because the AEGC has
+ * to increase overall exposure to reach a certain level of mean brightness. In
+ * WDR mode, the algorithm will artifically reduce the exposure time so that the
+ * texture and colours become visible in the formerly saturated areas. Then the
+ * global tone mapping curve is applied to mitigate the loss of brightness.
+ *
+ * Calculating that tone mapping curve is the most difficult part. This
+ * algorithm implements four tone mapping strategies:
+ * - Linear: The tone mapping curve is a combination of two linear functions
+ * with one kneepoint
+ * - Power: The tone mapping curve follows a power function
+ * - Exponential: The tone mapping curve follows an exponential function
+ * - HistogramEqualization: The tone mapping curve tries to equalize the
+ * histogram
+ *
+ * The overall strategy is the same in all cases: A negative exposure value is
+ * applied to the AEGC regulation until the number of nearly saturated pixels go
+ * below a given threshold (controllable via WdrMaxBrightPixels, default is 2%)
+ * or the MinExposureValue specified in the tuning file is reached.
+ *
+ * The global tone mapping curve is then calculated so that it accounts for the
+ * reduction of brightness due to the negative exposure value. As the result of
+ * tone mapping is very difficult to quantize and is by definition a lossy
+ * process there is not a single "correct" solution.
+ *
+ * The approach taken here is based on a simple linear model. Consider a pixel
+ * that was originally 50% grey. It will have its exposure pushed down by the
+ * WDR's initial exposure compensation. This value then needs to be pushed back
+ * up by the tone mapping curve so that it is 50% grey again. This point serves
+ * as our kneepoint. To get to this kneepoint, this pixel and all darker pixels
+ * (to the left of the kneepoint on the tone mapping curve) will simply have the
+ * exposure compensation undone by pow(2, -WDR_EV). This cancels out the
+ * original exposure compensation, which was pow(2, WDR_EV). The remaining
+ * brigher pixels (to the right of the kneepoint on the tone mapping curve) will
+ * be compressed. The WdrStrength control adjusts the gain of the left part of
+ * the tone mapping curve.
+ *
+ * In the Power and Exponential modes, the curves are calculated so that they
+ * pass through that kneepoint.
+ *
+ * The histogram equalization mode tries to equalize the histogram of the
+ * image and acts independently of the calculated exposure value.
+ *
+ * \code{.unparsed}
+ * algorithms:
+ *   - WideDynamicRange:
+ *       ExposureConstraint:
+ *         MaxBrightPixels: 0.02
+ *         yTarget: 0.95
+ *       MinExposureValue: -4.0
+ * \endcode
+ */
+
+LOG_DEFINE_CATEGORY(RkISP1Wdr)
+
+static constexpr unsigned int kTonecurveXIntervals = RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV;
+
+/*
+ * Increasing interval sizes. The intervals are crafted so that they sum
+ * up to 4096. This results in better fitting curves than the constant intervals
+ * (all entries are 4)
+ */
+static constexpr std::array<int, kTonecurveXIntervals> kLoglikeIntervals = {
+	{ 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4,
+	  4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6 }
+};
+
+WideDynamicRange::WideDynamicRange()
+{
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int WideDynamicRange::init([[maybe_unused]] IPAContext &context,
+			   [[maybe_unused]] const YamlObject &tuningData)
+{
+	if (!(context.hw->supportedBlocks & 1 << RKISP1_EXT_PARAMS_BLOCK_TYPE_WDR)) {
+		LOG(RkISP1Wdr, Error)
+			<< "Wide Dynamic Range not supported by the hardware or kernel.";
+		return -ENOTSUP;
+	}
+
+	toneCurveIntervalValues_ = kLoglikeIntervals;
+
+	/* Calculate a list of normed x values */
+	toneCurveX_[0] = 0.0;
+	int lastValue = 0;
+	for (unsigned int i = 1; i < toneCurveX_.size(); i++) {
+		lastValue += std::pow(2, toneCurveIntervalValues_[i - 1] + 3);
+		lastValue = std::min(lastValue, 4096);
+		toneCurveX_[i] = lastValue / 4096.0;
+	}
+
+	strength_ = 1.0;
+	mode_ = controls::draft::WdrOff;
+	exposureConstraintMaxBrightPixels_ = 0.02;
+	exposureConstraintY_ = 0.95;
+	minExposureValue_ = -4.0;
+
+	const auto &constraint = tuningData["ExposureConstraint"];
+	if (!constraint.isDictionary()) {
+		LOG(RkISP1Wdr, Warning)
+			<< "ExposureConstraint not found in tuning data."
+			   "Using default values MaxBrightPixels: "
+			<< exposureConstraintMaxBrightPixels_
+			<< " yTarget: " << exposureConstraintY_;
+	} else {
+		exposureConstraintMaxBrightPixels_ =
+			constraint["MaxBrightPixels"]
+				.get<double>()
+				.value_or(exposureConstraintMaxBrightPixels_);
+		exposureConstraintY_ =
+			constraint["yTarget"]
+				.get<double>()
+				.value_or(exposureConstraintY_);
+	}
+
+	const auto &minExp = tuningData["MinExposureValue"];
+	minExposureValue_ = minExp.get<double>().value_or(minExposureValue_);
+	if (!minExp) {
+		LOG(RkISP1Wdr, Warning)
+			<< "MinExposureValue not found in tuning data."
+			   " Using default value: "
+			<< minExposureValue_;
+	}
+
+	context.ctrlMap[&controls::draft::WdrMode] =
+		ControlInfo(controls::draft::WdrModeValues, controls::draft::WdrOff);
+	context.ctrlMap[&controls::draft::WdrStrength] =
+		ControlInfo(0.0f, 2.0f, static_cast<float>(strength_));
+	context.ctrlMap[&controls::draft::WdrMaxBrightPixels] =
+		ControlInfo(0.0f, 1.0f, static_cast<float>(exposureConstraintMaxBrightPixels_));
+
+	applyCompensationLinear(1.0, 0.0);
+
+	return 0;
+}
+
+void WideDynamicRange::applyHistogramEqualization(double strength)
+{
+	if (hist_.empty())
+		return;
+
+	strength *= 0.65;
+
+	/*
+	 * In a fully equalized histogram, all bins have the same value. Try
+	 * to equalize the histogram by applying a gain or damping depending on
+	 * the distance of the actual bin value from that norm.
+	 */
+	std::vector<double> gains;
+	gains.resize(hist_.size());
+	double sum = 0;
+	double norm = 1.0 / (gains.size());
+	for (unsigned i = 0; i < hist_.size(); i++) {
+		double diff = 1.0 + strength * (hist_[i] - norm) / norm;
+		gains[i] = diff;
+		sum += diff;
+	}
+	/* Never amplify the last entry. */
+	gains.back() = std::max(gains.back(), 1.0);
+
+	double scale = gains.size() / sum;
+	for (auto &v : gains)
+		v *= scale;
+
+	Pwl pwl;
+	double step = 1.0 / gains.size();
+	double lastX = 0;
+	double lastY = 0;
+
+	pwl.append(lastX, lastY);
+	for (unsigned int i = 0; i < gains.size() - 1; i++) {
+		lastY += gains[i] * step;
+		lastX += step;
+		pwl.append(lastX, lastY);
+	}
+	pwl.append(1.0, 1.0);
+
+	for (unsigned int i = 0; i < toneCurveX_.size(); i++)
+		toneCurveY_[i] = pwl.eval(toneCurveX_[i]);
+}
+
+Vector<double, 2> WideDynamicRange::kneePoint(double gain, double strength)
+{
+	gain = std::pow(gain, strength);
+	double y = 0.5;
+	double x = y / gain;
+
+	return { { x, y } };
+}
+
+void WideDynamicRange::applyCompensationLinear(double gain, double strength)
+{
+	auto kp = kneePoint(gain, strength);
+	double g1 = kp.y() / kp.x();
+	double g2 = (kp.y() - 1) / (kp.x() - 1);
+
+	for (unsigned int i = 0; i < toneCurveX_.size(); i++) {
+		double x = toneCurveX_[i];
+		double y;
+		if (x <= kp.x()) {
+			y = g1 * x;
+		} else {
+			y = g2 * x + 1 - g2;
+		}
+		toneCurveY_[i] = y;
+	}
+}
+
+void WideDynamicRange::applyCompensationPower(double gain, double strength)
+{
+	double e = 1.0;
+	if (strength > 1e-6) {
+		auto kp = kneePoint(gain, strength);
+		/* Calculate an exponent to go through the knee point. */
+		e = log(kp.y()) / log(kp.x());
+	}
+
+	for (unsigned int i = 0; i < toneCurveX_.size(); i++)
+		toneCurveY_[i] = std::pow(toneCurveX_[i], e);
+}
+
+void WideDynamicRange::applyCompensationExponential(double gain, double strength)
+{
+	double k = 0.1;
+	auto kp = kneePoint(gain, strength);
+	double kx = kp.x();
+	double ky = kp.y();
+
+	if (kx > ky) {
+		LOG(RkISP1Wdr, Warning) << "Invalid knee point: " << kp;
+		kx = ky;
+	}
+
+	/*
+	 * The exponential curve is based on the function proposed by Glozman
+	 * et al. in
+	 * S. Glozman, T. Kats, and O. Yadid-Pecht, "Exponent Operator Based
+	 * Tone Mapping Algorithm for Color Wide Dynamic Range Images," 2011.
+	 *
+	 * That function uses a k factor as parameter for the WDR compression
+	 * curve:
+	 * k=0: maximum compression
+	 * k=infinity: linear curve
+	 *
+	 * To calculate a k factor that results in a curve that passes through
+	 * the kneepoint, the equation needs to be solved for k after inserting
+	 * the kneepoint.  This can be formulated as search for a zero point.
+	 * Unfortunately there is no closed solution for that transformation.
+	 * Using newton's method to approximate the value is numerically
+	 * unstable.
+	 *
+	 * Luckily the function only crosses the x axis once and for the set of
+	 * possible kneepoints, a negative and a positive point can be guessed.
+	 * The approximation is then implemented using bisection.
+	 */
+	if (std::abs(kx - ky) < 0.001) {
+		k = 1e8;
+	} else {
+		double kl = 0.0001;
+		double kh = 1000;
+
+		auto fk = [=](double v) {
+			return std::exp(-kx / v) -
+			       ky * std::exp(-1.0 / v) + ky - 1.0;
+		};
+
+		ASSERT(fk(kl) < 0);
+		ASSERT(fk(kh) > 0);
+
+		k = kh / 10.0;
+		while (fk(k) > 0) {
+			kh = k;
+			k /= 10.0;
+		}
+
+		do {
+			k = (kl + kh) / 2;
+			if (fk(k) < 0)
+				kl = k;
+			else
+				kh = k;
+		} while (std::abs(kh - kl) > 1e-3);
+	}
+
+	double a = 1.0 / (1.0 - std::exp(-1.0 / k));
+	for (unsigned int i = 0; i < toneCurveX_.size(); i++)
+		toneCurveY_[i] = a * (1.0 - std::exp(-toneCurveX_[i] / k));
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void WideDynamicRange::queueRequest([[maybe_unused]] IPAContext &context,
+				    [[maybe_unused]] const uint32_t frame,
+				    IPAFrameContext &frameContext,
+				    const ControlList &controls)
+{
+	strength_ = controls.get(controls::draft::WdrStrength).value_or(strength_);
+	exposureConstraintMaxBrightPixels_ =
+		controls.get(controls::draft::WdrMaxBrightPixels)
+			.value_or(exposureConstraintMaxBrightPixels_);
+
+	const auto &mode = controls.get(controls::draft::WdrMode);
+	if (mode) {
+		mode_ = static_cast<controls::draft::WdrModeEnum>(*mode);
+	}
+
+	frameContext.wdr.mode = mode_;
+	frameContext.wdr.strength = strength_;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void WideDynamicRange::prepare(IPAContext &context,
+			       [[maybe_unused]] const uint32_t frame,
+			       IPAFrameContext &frameContext,
+			       RkISP1Params *params)
+{
+	if (!params) {
+		LOG(RkISP1Wdr, Warning) << "Params is null";
+		return;
+	}
+
+	auto config = params->block<BlockType::Wdr>();
+
+	auto mode = frameContext.wdr.mode;
+
+	config.setEnabled(mode != controls::draft::WdrOff);
+
+	double comp = 0;
+
+	/*
+	 * Todo: This overwrites frameContext.agc.exposureValue so that
+	 * in the next call to Agc::process() that exposureValue get's
+	 * applied. In the future it is planned to move the exposure
+	 * calculations from Agc::process() to Agc::prepare(). In this
+	 * case, we need to ensure that this code get's called early enough.
+	 */
+	if (mode != controls::draft::WdrOff) {
+		frameContext.wdr.wdrExposureValue = context.activeState.wdr.exposureValue;
+		frameContext.wdr.agcExposureValue = frameContext.agc.exposureValue;
+
+		/*
+		 * When WDR is enabled, the maxBrightPixels constraint is always
+		 * active. This is problematic when the user sets a positive
+		 * exposureValue. As this would mean that the negative WDR
+		 * exposure value and the user provided positive value should
+		 * cancel each other out. But as the regulation is not absolute
+		 * and only dependent on the measured changes in the histogram,
+		 * reducing the wdr EV would only lead to an even stronger pull
+		 * from regulation. Positive user supplied EVs must therefore be
+		 * handled using the wdr curve.
+		 */
+		if (frameContext.wdr.wdrExposureValue < 0) {
+			frameContext.agc.exposureValue =
+				std::min(frameContext.agc.exposureValue,
+					 frameContext.wdr.wdrExposureValue);
+			comp = frameContext.agc.exposureValue - frameContext.wdr.agcExposureValue;
+		}
+	}
+
+	/* Calculate how much EV we need to compensate with the WDR curve. */
+	double gain = pow(2.0, -comp);
+
+	if (mode == controls::draft::WdrOff) {
+		applyCompensationLinear(1.0, 0.0);
+	} else if (mode == controls::draft::WdrLinear) {
+		applyCompensationLinear(gain, frameContext.wdr.strength);
+	} else if (mode == controls::draft::WdrPower) {
+		applyCompensationPower(gain, frameContext.wdr.strength);
+	} else if (mode == controls::draft::WdrExponential) {
+		applyCompensationExponential(gain, frameContext.wdr.strength);
+	} else if (mode == controls::draft::WdrHistogramEqualization) {
+		applyHistogramEqualization(frameContext.wdr.strength);
+	}
+
+	/* Reset value */
+	config->dmin_strength = 0x10;
+	config->dmin_thresh = 0;
+
+	for (unsigned int i = 0; i < kTonecurveXIntervals; i++) {
+		int v = toneCurveIntervalValues_[i];
+		config->tone_curve.dY[i / 8] |= (v & 0x07) << ((i % 8) * 4);
+	}
+
+	std::vector<Point> debugCurve;
+
+	/*
+	 * Fix the curve to adhere to the hardware constraints. Don't apply a
+	 * constraint on the first element, which is most likely zero anyways.
+	 */
+	int lastY = toneCurveY_[0] * 4096.0;
+	for (unsigned int i = 0; i < toneCurveX_.size(); i++) {
+		int diff = static_cast<int>(toneCurveY_[i] * 4096.0) - lastY;
+		diff = std::clamp(diff, -2048, 2048);
+		lastY = lastY + diff;
+		config->tone_curve.ym[i] = lastY;
+		debugCurve.push_back({ static_cast<int>(toneCurveX_[i] * 4096.0),
+				       lastY });
+	}
+
+	context.debugMetadata.set<Span<const Point>>(controls::debug::WdrCurve, debugCurve);
+}
+
+void WideDynamicRange::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+			       IPAFrameContext &frameContext,
+			       const rkisp1_stat_buffer *stats,
+			       ControlList &metadata)
+{
+	if (!stats || !(stats->meas_type & RKISP1_CIF_ISP_STAT_HIST)) {
+		LOG(RkISP1Wdr, Error) << "No histogram data in statistics";
+		return;
+	}
+
+	if (!started_) {
+		started_ = true;
+		pid_.setStandardParameters(1, 5.0, 3.0);
+		pid_.setOutputLimits(minExposureValue_, 0.0);
+		context.activeState.wdr.exposureValue = 0.0;
+	}
+
+	pid_.setTarget(exposureConstraintY_);
+
+	const rkisp1_cif_isp_stat *params = &stats->params;
+	auto mode = frameContext.wdr.mode;
+
+	metadata.set(controls::draft::WdrMode, mode);
+
+	Histogram cumHist({ params->hist.hist_bins, context.hw->numHistogramBins },
+			  [](uint32_t x) { return x >> 4; });
+
+	std::vector<double> hist;
+	double sum = 0;
+	for (unsigned i = 0; i < context.hw->numHistogramBins; i++) {
+		double v = params->hist.hist_bins[i] >> 4;
+		hist.push_back(v);
+		sum += v;
+	}
+
+	/* Scale so that the entries sum up to 1. */
+	double scale = 1.0 / sum;
+	for (auto &v : hist)
+		v *= scale;
+	hist_.swap(hist);
+
+	double mean = cumHist.quantile(1.0 - exposureConstraintMaxBrightPixels_) /
+		      cumHist.bins();
+	LOG(RkISP1Wdr, Debug) << "Mean y of bright pixels: " << mean;
+
+	if (mode == controls::draft::WdrOff) {
+		metadata.set(controls::draft::WdrExposureValue, 0);
+		return;
+	}
+
+	context.activeState.wdr.exposureValue = pid_.process(mean);
+	LOG(RkISP1Wdr, Debug) << "Active state WDR ev: " << context.activeState.wdr.exposureValue;
+	metadata.set(controls::draft::WdrExposureValue, frameContext.wdr.wdrExposureValue);
+
+	/*
+	 * We overwrote agc exposure value in prepare() to create an
+	 * underexposure for WDR. Report the original exposure value in metadata.
+	 */
+	metadata.set(controls::ExposureValue, frameContext.wdr.agcExposureValue);
+}
+
+REGISTER_IPA_ALGORITHM(WideDynamicRange, "WideDynamicRange")
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/wdr.h b/src/ipa/rkisp1/algorithms/wdr.h
new file mode 100644
index 000000000000..bdea4eb5492a
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/wdr.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021-2022, Ideas On Board
+ *
+ * RkISP1 Wide Dynamic Range control
+ */
+
+#pragma once
+
+#include <libcamera/control_ids.h>
+
+#include "libcamera/internal/pid_controller.h"
+
+#include "linux/rkisp1-config.h"
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+class WideDynamicRange : public Algorithm
+{
+public:
+	WideDynamicRange();
+	~WideDynamicRange() = default;
+
+	int init(IPAContext &context, const YamlObject &tuningData) override;
+
+	void queueRequest(IPAContext &context, const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     RkISP1Params *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const rkisp1_stat_buffer *stats,
+		     ControlList &metadata) override;
+
+private:
+	Vector<double, 2> kneePoint(double gain, double strength);
+	void applyCompensationLinear(double gain, double strength);
+	void applyCompensationPower(double gain, double strength);
+	void applyCompensationExponential(double gain, double strength);
+	void applyHistogramEqualization(double strength);
+
+	double exposureConstraintMaxBrightPixels_;
+	double exposureConstraintY_;
+	double minExposureValue_;
+	double strength_;
+	bool started_ = false;
+
+	controls::draft::WdrModeEnum mode_;
+	std::vector<double> hist_;
+	PidController pid_;
+
+	std::array<int, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV> toneCurveIntervalValues_;
+	std::array<double, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV + 1> toneCurveX_;
+	std::array<double, RKISP1_CIF_ISP_WDR_CURVE_NUM_INTERV + 1> toneCurveY_;
+};
+
+} /* namespace ipa::rkisp1::algorithms */
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
index 60cfab228edf..32f6db30bbdb 100644
--- a/src/ipa/rkisp1/ipa_context.h
+++ b/src/ipa/rkisp1/ipa_context.h
@@ -130,6 +130,10 @@ struct IPAActiveState {
 	struct {
 		double gamma;
 	} goc;
+
+	struct {
+		double exposureValue;
+	} wdr;
 };
 
 struct IPAFrameContext : public FrameContext {
@@ -198,6 +202,13 @@ struct IPAFrameContext : public FrameContext {
 	struct {
 		double lux;
 	} lux;
+
+	struct {
+		controls::draft::WdrModeEnum mode;
+		double wdrExposureValue;
+		double agcExposureValue;
+		double strength;
+	} wdr;
 };
 
 struct IPAContext {
diff --git a/src/ipa/rkisp1/params.cpp b/src/ipa/rkisp1/params.cpp
index 4c0b051ce65d..5edb36c91b87 100644
--- a/src/ipa/rkisp1/params.cpp
+++ b/src/ipa/rkisp1/params.cpp
@@ -74,6 +74,7 @@ const std::map<BlockType, BlockTypeInfo> kBlockTypeInfo = {
 	RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandBls, COMPAND_BLS, compand_bls),
 	RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandExpand, COMPAND_EXPAND, compand_curve),
 	RKISP1_BLOCK_TYPE_ENTRY_EXT(CompandCompress, COMPAND_COMPRESS, compand_curve),
+	RKISP1_BLOCK_TYPE_ENTRY_EXT(Wdr, WDR, wdr),
 };
 
 } /* namespace */
diff --git a/src/ipa/rkisp1/params.h b/src/ipa/rkisp1/params.h
index 40450e34497a..2e60528d102e 100644
--- a/src/ipa/rkisp1/params.h
+++ b/src/ipa/rkisp1/params.h
@@ -40,6 +40,7 @@ enum class BlockType {
 	CompandBls,
 	CompandExpand,
 	CompandCompress,
+	Wdr,
 };
 
 namespace details {
@@ -74,6 +75,7 @@ RKISP1_DEFINE_BLOCK_TYPE(Afc, afc)
 RKISP1_DEFINE_BLOCK_TYPE(CompandBls, compand_bls)
 RKISP1_DEFINE_BLOCK_TYPE(CompandExpand, compand_curve)
 RKISP1_DEFINE_BLOCK_TYPE(CompandCompress, compand_curve)
+RKISP1_DEFINE_BLOCK_TYPE(Wdr, wdr)
 
 } /* namespace details */
 
diff --git a/src/libcamera/control_ids_debug.yaml b/src/libcamera/control_ids_debug.yaml
index 797532712099..9489e677402e 100644
--- a/src/libcamera/control_ids_debug.yaml
+++ b/src/libcamera/control_ids_debug.yaml
@@ -3,4 +3,9 @@
 %YAML 1.1
 ---
 vendor: debug
-controls: []
+controls:
+- WdrCurve:
+    type: Point
+    direction: out
+    description: Debug control WdrCurve found in src/ipa/rkisp1/algorithms/wdr.cpp
+    size: '[n]'
diff --git a/src/libcamera/control_ids_draft.yaml b/src/libcamera/control_ids_draft.yaml
index 03309eeac34f..0c0af7a19968 100644
--- a/src/libcamera/control_ids_draft.yaml
+++ b/src/libcamera/control_ids_draft.yaml
@@ -293,5 +293,75 @@ controls:
 
         Currently identical to ANDROID_STATISTICS_FACE_IDS.
       size: [n]
+  - WdrMode:
+      type: int32_t
+      direction: inout
+      description: |
+        Set the WDR mode.
+
+        The WDR mode is used to select the algorithm used for global tone
+        mapping. It will automatically reduce the exposure time of the sensor
+        so that there are only a small number of saturated pixels in the image.
+        The algorithm then compensates for the loss of brightness by applying a
+        global tone mapping curve to the image.
+      enum:
+        - name: WdrOff
+          value: 0
+          description: Wdr is disabled.
+        - name: WdrLinear
+          value: 1
+          description:
+            Apply a linear global tone mapping curve.
+
+            The curve with two linear sections is applied. This produces good
+            results at the expense of a slightly artificial look.
+        - name: WdrPower
+          value: 2
+          description: |
+            Apply a power global tone mapping curve.
+
+            This curve has high gain values on the dark areas of an image and
+            high compression values on the bright area. It therefore tends to
+            produce noticeable noise artifacts.
+        - name: WdrExponential
+          value: 3
+          description: |
+            Apply a exponential global tone mapping curve.
+
+            This curve has lower gain values in dark areas compared to the power
+            curve but produces a more natural look compared to the linear curve.
+            It is therefore the best choice for most scenes.
+        - name: WdrHistogramEqualization
+          value: 4
+          description: |
+            Apply histogram equalization.
+
+            This curve preserves most of the information of the image at the
+            expense of a very artificial look. It is therefore best suited for
+            technical analysis.
+  - WdrStrength:
+      type: float
+      direction: in
+      description: |
+        Specify the strength of the wdr algorithm. The exact meaning of this
+        value is specific to the algorithm in use. Usually a value of 0 means no
+        global tone mapping is applied. A values of 1 is the default value and
+        the correct value for most scenes. A value above 1 increases the global
+        tone mapping effect and can lead to unrealistic image effects.
+  - WdrMaxBrightPixels:
+      type: float
+      direction: in
+      description: |
+        Percentage of allowed (nearly) saturated pixels. The WDR algorithm
+        reduces the WdrExposureValue until the amount of pixels that are close
+        to saturation is lower than this value.
+  - WdrExposureValue:
+      type: float
+      direction: out
+      description: |
+        Reports the Exposure Value that was applied to the AEGC regulation so
+        that the WdrMaxBrightPixels limit is reached.
+
+        \sa ExposureValue
 
 ...
