diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp
index 2f6bb672f7bb..89fc34f86e46 100644
--- a/src/ipa/ipu3/ipu3.cpp
+++ b/src/ipa/ipu3/ipu3.cpp
@@ -176,6 +176,9 @@ private:
 
 	/* Local parameter storage */
 	struct IPAContext context_;
+
+	/* Control retention to maintain mode states */
+	ControlList retainedControls_;
 };
 
 /**
@@ -456,6 +459,8 @@ int IPAIPU3::configure(const IPAConfigInfo &configInfo,
 
 	/* Clean IPAActiveState at each reconfiguration. */
 	context_.activeState = {};
+	retainedControls_.clear();
+
 	IPAFrameContext initFrameContext;
 	context_.frameContexts.fill(initFrameContext);
 
@@ -617,8 +622,24 @@ void IPAIPU3::processStatsBuffer(const uint32_t frame,
  */
 void IPAIPU3::queueRequest(const uint32_t frame, const ControlList &controls)
 {
+	/*
+	 * Controls are retained, to ensure any mode updates are persistant.
+	 * We merge them into our class control storage before assigning to
+	 * the frame context.
+	 *
+	 * \todo This will not support trigger controls and may need rework if
+	 *	 we add any to prevent continually retriggering.
+	 *
+	 * \todo We may wish to store both the full merged control list, as well
+	 *	 as the delta (\a controls) to facilitate algorithms identifying
+	 *	 when things have changed.
+	 */
+	ControlList mergeControls = controls;
+	mergeControls.merge(retainedControls_);
+	retainedControls_ = mergeControls;
+
 	/* \todo Start processing for 'frame' based on 'controls'. */
-	context_.frameContexts[frame % kMaxFrameContexts] = { frame, controls };
+	context_.frameContexts[frame % kMaxFrameContexts] = { frame, std::move(mergeControls) };
 }
 
 /**
