diff --git a/src/ipa/simple/algorithms/blc.cpp b/src/ipa/simple/algorithms/blc.cpp
new file mode 100644
index 00000000..08f4345e
--- /dev/null
+++ b/src/ipa/simple/algorithms/blc.cpp
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Black level handling
+ */
+
+#include "blc.h"
+
+#include <numeric>
+
+#include <libcamera/base/log.h>
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+LOG_DEFINE_CATEGORY(IPASoftBL)
+
+BlackLevel::BlackLevel()
+{
+}
+
+int BlackLevel::init(IPAContext &context,
+		     [[maybe_unused]] const YamlObject &tuningData)
+{
+	context.activeState.blc.level = 255;
+	return 0;
+}
+
+void BlackLevel::process(IPAContext &context,
+			 [[maybe_unused]] const uint32_t frame,
+			 [[maybe_unused]] IPAFrameContext &frameContext,
+			 const SwIspStats *stats,
+			 [[maybe_unused]] ControlList &metadata)
+{
+	const SwIspStats::Histogram &histogram = stats->yHistogram;
+
+	/*
+	 * The constant is selected to be "good enough", not overly
+	 * conservative or aggressive. There is no magic about the given value.
+	 */
+	constexpr float ignoredPercentage = 0.02;
+	const unsigned int total =
+		std::accumulate(begin(histogram), end(histogram), 0);
+	const unsigned int pixelThreshold = ignoredPercentage * total;
+	const unsigned int histogramRatio = 256 / SwIspStats::kYHistogramSize;
+	const unsigned int currentBlackIdx =
+		context.activeState.blc.level / histogramRatio;
+
+	for (unsigned int i = 0, seen = 0;
+	     i < currentBlackIdx && i < SwIspStats::kYHistogramSize;
+	     i++) {
+		seen += histogram[i];
+		if (seen >= pixelThreshold) {
+			context.activeState.blc.level = i * histogramRatio;
+			LOG(IPASoftBL, Debug)
+				<< "Auto-set black level: "
+				<< i << "/" << SwIspStats::kYHistogramSize
+				<< " (" << 100 * (seen - histogram[i]) / total << "% below, "
+				<< 100 * seen / total << "% at or below)";
+			break;
+		}
+	};
+}
+
+REGISTER_IPA_ALGORITHM(BlackLevel, "BlackLevel")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/blc.h b/src/ipa/simple/algorithms/blc.h
new file mode 100644
index 00000000..c2140b4b
--- /dev/null
+++ b/src/ipa/simple/algorithms/blc.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * Black level handling
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class BlackLevel : public Algorithm
+{
+public:
+	BlackLevel();
+	~BlackLevel() = default;
+
+	int init(IPAContext &context, const YamlObject &tuningData)
+		override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const SwIspStats *stats,
+		     ControlList &metadata) override;
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
index 31d26e43..1f63c220 100644
--- a/src/ipa/simple/algorithms/meson.build
+++ b/src/ipa/simple/algorithms/meson.build
@@ -1,4 +1,5 @@
 # SPDX-License-Identifier: CC0-1.0
 
 soft_simple_ipa_algorithms = files([
+    'blc.cpp',
 ])
diff --git a/src/ipa/simple/black_level.cpp b/src/ipa/simple/black_level.cpp
deleted file mode 100644
index 37e0109c..00000000
--- a/src/ipa/simple/black_level.cpp
+++ /dev/null
@@ -1,93 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2024, Red Hat Inc.
- *
- * black level handling
- */
-
-#include "black_level.h"
-
-#include <numeric>
-
-#include <libcamera/base/log.h>
-
-namespace libcamera {
-
-LOG_DEFINE_CATEGORY(IPASoftBL)
-
-namespace ipa::soft {
-
-/**
- * \class BlackLevel
- * \brief Object providing black point level for software ISP
- *
- * Black level can be provided in hardware tuning files or, if no tuning file is
- * available for the given hardware, guessed automatically, with less accuracy.
- * As tuning files are not yet implemented for software ISP, BlackLevel
- * currently provides only guessed black levels.
- *
- * This class serves for tracking black level as a property of the underlying
- * hardware, not as means of enhancing a particular scene or image.
- *
- * The class is supposed to be instantiated for the given camera stream.
- * The black level can be retrieved using BlackLevel::get() method. It is
- * initially 0 and may change when updated using BlackLevel::update() method.
- */
-
-BlackLevel::BlackLevel()
-	: blackLevel_(255), blackLevelSet_(false)
-{
-}
-
-/**
- * \brief Return the current black level
- *
- * \return The black level, in the range from 0 (minimum) to 255 (maximum).
- * If the black level couldn't be determined yet, return 0.
- */
-uint8_t BlackLevel::get() const
-{
-	return blackLevelSet_ ? blackLevel_ : 0;
-}
-
-/**
- * \brief Update black level from the provided histogram
- * \param[in] yHistogram The histogram to be used for updating black level
- *
- * The black level is property of the given hardware, not image. It is updated
- * only if it has not been yet set or if it is lower than the lowest value seen
- * so far.
- */
-void BlackLevel::update(SwIspStats::Histogram &yHistogram)
-{
-	/*
-	 * The constant is selected to be "good enough", not overly conservative or
-	 * aggressive. There is no magic about the given value.
-	 */
-	constexpr float ignoredPercentage_ = 0.02;
-	const unsigned int total =
-		std::accumulate(begin(yHistogram), end(yHistogram), 0);
-	const unsigned int pixelThreshold = ignoredPercentage_ * total;
-	const unsigned int histogramRatio = 256 / SwIspStats::kYHistogramSize;
-	const unsigned int currentBlackIdx = blackLevel_ / histogramRatio;
-
-	for (unsigned int i = 0, seen = 0;
-	     i < currentBlackIdx && i < SwIspStats::kYHistogramSize;
-	     i++) {
-		seen += yHistogram[i];
-		if (seen >= pixelThreshold) {
-			blackLevel_ = i * histogramRatio;
-			blackLevelSet_ = true;
-			LOG(IPASoftBL, Debug)
-				<< "Auto-set black level: "
-				<< i << "/" << SwIspStats::kYHistogramSize
-				<< " (" << 100 * (seen - yHistogram[i]) / total << "% below, "
-				<< 100 * seen / total << "% at or below)";
-			break;
-		}
-	};
-}
-
-} /* namespace ipa::soft */
-
-} /* namespace libcamera */
diff --git a/src/ipa/simple/black_level.h b/src/ipa/simple/black_level.h
deleted file mode 100644
index a04230c9..00000000
--- a/src/ipa/simple/black_level.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2024, Red Hat Inc.
- *
- * black level handling
- */
-
-#pragma once
-
-#include <array>
-#include <stdint.h>
-
-#include "libcamera/internal/software_isp/swisp_stats.h"
-
-namespace libcamera {
-
-namespace ipa::soft {
-
-class BlackLevel
-{
-public:
-	BlackLevel();
-	uint8_t get() const;
-	void update(SwIspStats::Histogram &yHistogram);
-
-private:
-	uint8_t blackLevel_;
-	bool blackLevelSet_;
-};
-
-} /* namespace ipa::soft */
-
-} /* namespace libcamera */
diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
index 2cdc39a8..e0d03d96 100644
--- a/src/ipa/simple/data/uncalibrated.yaml
+++ b/src/ipa/simple/data/uncalibrated.yaml
@@ -3,4 +3,5 @@
 ---
 version: 1
 algorithms:
+  - BlackLevel:
 ...
diff --git a/src/ipa/simple/ipa_context.cpp b/src/ipa/simple/ipa_context.cpp
index 3c1c7262..268cff32 100644
--- a/src/ipa/simple/ipa_context.cpp
+++ b/src/ipa/simple/ipa_context.cpp
@@ -50,4 +50,12 @@ namespace libcamera::ipa::soft {
  * \brief The current state of IPA algorithms
  */
 
+/**
+ * \var IPAActiveState::black
+ * \brief Context for the Black Level algorithm
+ *
+ * \var IPAActiveState::black.level
+ * \brief Current determined black level
+ */
+
 } /* namespace libcamera::ipa::soft */
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index e7d8b8df..ac2a59d7 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -7,6 +7,8 @@
 
 #pragma once
 
+#include <stdint.h>
+
 #include <libipa/fc_queue.h>
 
 namespace libcamera {
@@ -17,6 +19,9 @@ struct IPASessionConfiguration {
 };
 
 struct IPAActiveState {
+	struct {
+		uint8_t level;
+	} blc;
 };
 
 struct IPAFrameContext : public FrameContext {
diff --git a/src/ipa/simple/meson.build b/src/ipa/simple/meson.build
index dcd7c70a..2f9f15f4 100644
--- a/src/ipa/simple/meson.build
+++ b/src/ipa/simple/meson.build
@@ -8,7 +8,6 @@ ipa_name = 'ipa_soft_simple'
 soft_simple_sources = files([
     'ipa_context.cpp',
     'soft_simple.cpp',
-    'black_level.cpp',
 ])
 
 soft_simple_sources += soft_simple_ipa_algorithms
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index 5beec45a..3332e2b1 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -29,7 +29,6 @@
 
 #include "libipa/camera_sensor_helper.h"
 
-#include "black_level.h"
 #include "module.h"
 
 namespace libcamera {
@@ -61,7 +60,7 @@ class IPASoftSimple : public ipa::soft::IPASoftInterface, public Module
 {
 public:
 	IPASoftSimple()
-		: params_(nullptr), stats_(nullptr), blackLevel_(BlackLevel()),
+		: params_(nullptr), stats_(nullptr),
 		  context_({ {}, {}, { kMaxFrameContexts } }),
 		  ignoreUpdates_(0)
 	{
@@ -93,7 +92,6 @@ private:
 	SwIspStats *stats_;
 	std::unique_ptr<CameraSensorHelper> camHelper_;
 	ControlInfoMap sensorInfoMap_;
-	BlackLevel blackLevel_;
 
 	static constexpr unsigned int kGammaLookupSize = 1024;
 	std::array<uint8_t, kGammaLookupSize> gammaTable_;
@@ -303,9 +301,7 @@ void IPASoftSimple::processStats(const uint32_t frame,
 		algo->process(context_, frame, frameContext, stats_, metadata);
 
 	SwIspStats::Histogram histogram = stats_->yHistogram;
-	if (ignoreUpdates_ > 0)
-		blackLevel_.update(histogram);
-	const uint8_t blackLevel = blackLevel_.get();
+	const uint8_t blackLevel = context_.activeState.blc.level;
 
 	/*
 	 * Black level must be subtracted to get the correct AWB ratios, they
