diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
index 5c566a82fb..578ee12a51 100644
--- a/src/ipa/libipa/agc.cpp
+++ b/src/ipa/libipa/agc.cpp
@@ -237,13 +237,10 @@ namespace {
  */
 static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
 
-int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+int parseRelativeLuminanceTarget(AgcMeanLuminance::Configuration &config, const ValueNode &target)
 {
-	auto &target = tuningData["relativeLuminanceTarget"];
-	if (!target) {
-		config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
+	if (!target)
 		return 0;
-	}
 
 	auto pwl = target.get<Pwl>();
 	if (!pwl) {
@@ -293,7 +290,7 @@ int parseConstraint(AgcMeanLuminance::Configuration &config, const ValueNode &mo
 	return 0;
 }
 
-int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNode &constraintModes)
 {
 	static constexpr std::tuple<std::string_view, std::string_view, controls::AeConstraintModeEnum> compat[] = {
 		{ "ConstraintNormal", "Normal", controls::AeConstraintModeNormal },
@@ -302,67 +299,50 @@ int parseConstraintModes(AgcMeanLuminance::Configuration &config, const ValueNod
 		{ "ConstraintCustom", "Custom", controls::AeConstraintModeCustom },
 	};
 
-	const ValueNode &constraintModes = tuningData[controls::AeConstraintMode.name()];
-	if (constraintModes.isDictionary()) {
-		for (const auto &[modeName, modeDict] : constraintModes.asDict()) {
-			std::optional<controls::AeConstraintModeEnum> modeValue;
-
-			auto it = controls::AeConstraintModeNameValueMap.find(modeName);
-			if (it == controls::AeConstraintModeNameValueMap.end()) {
-				for (const auto &[oldName, newName, v] : compat) {
-					if (modeName != oldName)
-						continue;
-
-					modeValue = v;
-
-					LOG(Agc, Warning)
-						<< "Constraint mode '" << oldName << "' has been renamed to '"
-						<< newName << "', please fix the tuning file";
-					break;
-				}
-			} else {
-				modeValue = static_cast<controls::AeConstraintModeEnum>(it->second);
-			}
+	if (!constraintModes.isDictionary())
+		return 0;
 
-			if (!modeValue) {
-				LOG(Agc, Warning)
-					<< "Skipping unknown constraint mode '" << modeName << "'";
-				continue;
-			}
+	for (const auto &[modeName, modeDict] : constraintModes.asDict()) {
+		std::optional<controls::AeConstraintModeEnum> modeValue;
+
+		auto it = controls::AeConstraintModeNameValueMap.find(modeName);
+		if (it == controls::AeConstraintModeNameValueMap.end()) {
+			for (const auto &[oldName, newName, v] : compat) {
+				if (modeName != oldName)
+					continue;
 
-			if (!modeDict.isDictionary()) {
-				LOG(Agc, Error)
-					<< "Invalid constraint mode '" << modeName << "'";
-				return -EINVAL;
+				modeValue = v;
+
+				LOG(Agc, Warning)
+					<< "Constraint mode '" << oldName << "' has been renamed to '"
+					<< newName << "', please fix the tuning file";
+				break;
 			}
+		} else {
+			modeValue = static_cast<controls::AeConstraintModeEnum>(it->second);
+		}
 
-			int ret = parseConstraint(config, modeDict, *modeValue);
-			if (ret)
-				return ret;
+		if (!modeValue) {
+			LOG(Agc, Warning)
+				<< "Skipping unknown constraint mode '" << modeName << "'";
+			continue;
 		}
-	}
 
-	/*
-	 * If the tuning data file contains no constraints then we use the
-	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
-	 * to anyway before centralisation; this constraint forces the top 2% of
-	 * the histogram to be at least 0.5.
-	 */
-	if (config.constraintModes.empty()) {
-		AgcMeanLuminance::AgcConstraint constraint = {
-			AgcMeanLuminance::AgcConstraint::Bound::Lower,
-			0.98,
-			1.0,
-			Pwl({ { { 0.0, 0.5 } } }),
-		};
+		if (!modeDict.isDictionary()) {
+			LOG(Agc, Error)
+				<< "Invalid constraint mode '" << modeName << "'";
+			return -EINVAL;
+		}
 
-		config.constraintModes[controls::AeConstraintModeNormal].push_back(std::move(constraint));
+		int ret = parseConstraint(config, modeDict, *modeValue);
+		if (ret)
+			return ret;
 	}
 
 	return 0;
 }
 
-int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &tuningData)
+int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode &exposureModes)
 {
 	static constexpr std::tuple<std::string_view, std::string_view, controls::AeExposureModeEnum> compat[] = {
 		{ "ExposureNormal", "Normal", controls::AeExposureModeNormal },
@@ -371,79 +351,68 @@ int parseExposureModes(AgcMeanLuminance::Configuration &config, const ValueNode
 		{ "ExposureCustom", "Custom", controls::AeExposureModeCustom },
 	};
 
-	const ValueNode &exposureModes = tuningData[controls::AeExposureMode.name()];
-	if (exposureModes.isDictionary()) {
-		for (const auto &[modeName, modeValues] : exposureModes.asDict()) {
-			std::optional<controls::AeExposureModeEnum> modeValue;
-
-			auto it = controls::AeExposureModeNameValueMap.find(modeName);
-			if (it == controls::AeExposureModeNameValueMap.end()) {
-				for (const auto &[oldName, newName, v] : compat) {
-					if (modeName != oldName)
-						continue;
-
-					modeValue = v;
-
-					LOG(Agc, Warning)
-						<< "Exposure mode '" << oldName << "' has been renamed to '"
-						<< newName << "', please fix the tuning file";
-					break;
-				}
-			} else {
-				modeValue = static_cast<controls::AeExposureModeEnum>(it->second);
-			}
+	if (!exposureModes.isDictionary())
+		return 0;
+
+	for (const auto &[modeName, modeValues] : exposureModes.asDict()) {
+		std::optional<controls::AeExposureModeEnum> modeValue;
+
+		auto it = controls::AeExposureModeNameValueMap.find(modeName);
+		if (it == controls::AeExposureModeNameValueMap.end()) {
+			for (const auto &[oldName, newName, v] : compat) {
+				if (modeName != oldName)
+					continue;
+
+				modeValue = v;
 
-			if (!modeValue) {
 				LOG(Agc, Warning)
-					<< "Skipping unknown exposure mode '" << modeName << "'";
-				continue;
+					<< "Exposure mode '" << oldName << "' has been renamed to '"
+					<< newName << "', please fix the tuning file";
+				break;
 			}
+		} else {
+			modeValue = static_cast<controls::AeExposureModeEnum>(it->second);
+		}
 
-			if (!modeValues.isDictionary()) {
-				LOG(Agc, Error)
-					<< "Invalid exposure mode '" << modeName << "'";
-				return -EINVAL;
-			}
+		if (!modeValue) {
+			LOG(Agc, Warning)
+				<< "Skipping unknown exposure mode '" << modeName << "'";
+			continue;
+		}
 
-			std::vector<uint32_t> exposureTimes =
-				modeValues["exposureTime"].get<std::vector<uint32_t>>().value_or(utils::defopt);
-			std::vector<double> gains =
-				modeValues["gain"].get<std::vector<double>>().value_or(utils::defopt);
+		if (!modeValues.isDictionary()) {
+			LOG(Agc, Error)
+				<< "Invalid exposure mode '" << modeName << "'";
+			return -EINVAL;
+		}
 
-			if (exposureTimes.size() != gains.size()) {
-				LOG(Agc, Error)
-					<< "Exposure time and gain array sizes unequal";
-				return -EINVAL;
-			}
+		std::vector<uint32_t> exposureTimes =
+			modeValues["exposureTime"].get<std::vector<uint32_t>>().value_or(utils::defopt);
+		std::vector<double> gains =
+			modeValues["gain"].get<std::vector<double>>().value_or(utils::defopt);
 
-			if (exposureTimes.empty()) {
-				LOG(Agc, Error)
-					<< "Exposure time and gain arrays are empty";
-				return -EINVAL;
-			}
+		if (exposureTimes.size() != gains.size()) {
+			LOG(Agc, Error)
+				<< "Exposure time and gain array sizes unequal";
+			return -EINVAL;
+		}
 
-			std::vector<ExposureModeHelper::Stage> stages;
-			for (size_t i = 0; i < exposureTimes.size(); i++) {
-				stages.push_back({
-					.exposureTime = std::chrono::microseconds(exposureTimes[i]),
-					.gain = gains[i],
-				});
-			}
+		if (exposureTimes.empty()) {
+			LOG(Agc, Error)
+				<< "Exposure time and gain arrays are empty";
+			return -EINVAL;
+		}
 
-			config.exposureModeHelpers.try_emplace(*modeValue, std::move(stages));
+		std::vector<ExposureModeHelper::Stage> stages;
+		for (size_t i = 0; i < exposureTimes.size(); i++) {
+			stages.push_back({
+				.exposureTime = std::chrono::microseconds(exposureTimes[i]),
+				.gain = gains[i],
+			});
 		}
-	}
 
-	/*
-	 * If we don't have any exposure modes in the tuning data we create an
-	 * ExposureModeHelper using an empty vector of stages. This will result
-	 * in the ExposureModeHelper simply driving the exposure time as high as
-	 * possible before touching gain.
-	 */
-	if (config.exposureModeHelpers.empty())
-		config.exposureModeHelpers.try_emplace(
-			controls::AeExposureModeNormal,
-			std::vector<ExposureModeHelper::Stage>{});
+		config.exposureModeHelpers.try_emplace(*modeValue, std::move(stages));
+	}
 
 	return 0;
 }
@@ -452,15 +421,49 @@ int parseTuningData(AgcMeanLuminance::Configuration &config, const ValueNode &tu
 {
 	int ret;
 
-	ret = parseRelativeLuminanceTarget(config, tuningData);
+	ret = parseRelativeLuminanceTarget(config, tuningData["relativeLuminanceTarget"]);
 	if (ret)
 		return ret;
 
-	ret = parseConstraintModes(config, tuningData);
+	ret = parseConstraintModes(config, tuningData[controls::AeConstraintMode.name()]);
 	if (ret)
 		return ret;
 
-	return parseExposureModes(config, tuningData);
+	return parseExposureModes(config, tuningData[controls::AeExposureMode.name()]);
+}
+
+void addFallbacks(AgcMeanLuminance::Configuration &config)
+{
+	if (config.relativeLuminanceTarget.empty())
+		config.relativeLuminanceTarget = { { { { 0.0, kDefaultRelativeLuminanceTarget } } } };
+
+	/*
+	 * If the tuning data file contains no constraints then we use the
+	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
+	 * to anyway before centralisation; this constraint forces the top 2% of
+	 * the histogram to be at least 0.5.
+	 */
+	if (config.constraintModes.empty()) {
+		AgcMeanLuminance::AgcConstraint constraint = {
+			AgcMeanLuminance::AgcConstraint::Bound::Lower,
+			0.98,
+			1.0,
+			Pwl({ { { 0.0, 0.5 } } }),
+		};
+
+		config.constraintModes[controls::AeConstraintModeNormal].push_back(std::move(constraint));
+	}
+
+	/*
+	 * If we don't have any exposure modes in the tuning data we create an
+	 * ExposureModeHelper using an empty vector of stages. This will result
+	 * in the ExposureModeHelper simply driving the exposure time as high as
+	 * possible before touching gain.
+	 */
+	if (config.exposureModeHelpers.empty())
+		config.exposureModeHelpers.try_emplace(
+			controls::AeExposureModeNormal,
+			std::vector<ExposureModeHelper::Stage>{});
 }
 
 } /* namespace */
@@ -608,6 +611,8 @@ int AgcAlgorithm::init(const ValueNode &tuningData, CameraSensorHelper *sensor,
 		if (ret)
 			return ret;
 
+		addFallbacks(agcConfig);
+
 		impl_.emplace<AgcMeanLuminance>(std::move(agcConfig));
 	} else {
 		impl_.emplace<AgcMSV>();
