diff --git a/include/libcamera/ipa/raspberrypi.fbs b/include/libcamera/ipa/raspberrypi.fbs
new file mode 100644
index 0000000..dcc1918
--- /dev/null
+++ b/include/libcamera/ipa/raspberrypi.fbs
@@ -0,0 +1,107 @@
+enum RPiConfigParameters:uint16 {
+	NONE = 0,
+	RPI_IPA_CONFIG_LS_TABLE,
+	RPI_IPA_CONFIG_STAGGERED_WRITE,
+	RPI_IPA_CONFIG_SENSOR,
+	RPI_IPA_CONFIG_SEND_FD,
+}
+
+enum RPiEvents:uint16 {
+	NONE = 0,
+	RPI_IPA_EVENT_SIGNAL_STAT_READY,
+	RPI_IPA_EVENT_SIGNAL_ISP_PREPARE,
+	RPI_IPA_EVENT_QUEUE_REQUEST,
+	RPI_IPA_EVENT_SEND_FD,
+}
+
+enum RPiActions:uint16 {
+	NONE = 0,
+	RPI_IPA_ACTION_V4L2_SET_STAGGERED,
+	RPI_IPA_ACTION_V4L2_SET_ISP,
+	RPI_IPA_ACTION_STATS_METADATA_COMPLETE,
+	RPI_IPA_ACTION_RUN_ISP,
+	RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME,
+	RPI_IPA_ACTION_EMBEDDED_COMPLETE,
+}
+
+// TODO define this for all primitive types, and make global
+table UnsignedInt {
+	value:uint32;
+}
+
+table SignedInt {
+	value:int32;
+}
+
+// TODO make this global
+// TODO rename this
+table ControlList {
+	list:[uint8];
+}
+
+// Data containers
+
+table RPiStaggeredWritePayload {
+	gainDelay:uint32;
+	exposureDelay:uint32;
+	sensorMetadata:uint32;
+}
+
+table RPiIspPreparePayload {
+	embeddedbufferId:uint32;
+	bayerbufferId:uint32;
+}
+
+table RPiStatsCompletePayload {
+	bufferId:uint32;
+	controls:ControlList;
+}
+
+
+// Payload unions
+
+union RPiConfigureUnion {
+	lsTableHandle:UnsignedInt,
+	staggeredWriteResult:RPiStaggeredWritePayload,
+	controls:ControlList,
+	bufferFd:SignedInt,
+}
+
+table RPiConfigurePayload {
+        op:RPiConfigParameters;
+	payload:RPiConfigureUnion;
+}
+
+union RPiEventPayload {
+	bufferId:UnsignedInt,
+	ispPrepare:RPiIspPreparePayload,
+	controls:ControlList,
+	bufferFd:SignedInt,
+}
+
+union RPiActionPayload {
+	bufferId:UnsignedInt,
+	statsComplete:RPiStatsCompletePayload,
+	controls:ControlList,
+}
+
+
+// IPA function parameters
+
+table RPiConfigParams {
+	params:[RPiConfigurePayload];
+}
+
+table RPiEventParams {
+	ev:RPiEvents;
+	payload:RPiEventPayload;
+}
+
+table RPiActionParams {
+	op:RPiActions;
+	payload:RPiActionPayload;
+}
+
+root_type RPiConfigureParams;
+root_type RPiEventParams;
+root_type RPiActionParams;
diff --git a/include/libcamera/ipa/raspberrypi.h b/include/libcamera/ipa/raspberrypi.h
index a493776..69b3808 100644
--- a/include/libcamera/ipa/raspberrypi.h
+++ b/include/libcamera/ipa/raspberrypi.h
@@ -10,24 +10,6 @@
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 
-enum RPiConfigParameters {
-	RPI_IPA_CONFIG_LS_TABLE = (1 << 0),
-	RPI_IPA_CONFIG_STAGGERED_WRITE = (1 << 1),
-	RPI_IPA_CONFIG_SENSOR = (1 << 2),
-};
-
-enum RPiOperations {
-	RPI_IPA_ACTION_V4L2_SET_STAGGERED = 1,
-	RPI_IPA_ACTION_V4L2_SET_ISP,
-	RPI_IPA_ACTION_STATS_METADATA_COMPLETE,
-	RPI_IPA_ACTION_RUN_ISP,
-	RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME,
-	RPI_IPA_ACTION_EMBEDDED_COMPLETE,
-	RPI_IPA_EVENT_SIGNAL_STAT_READY,
-	RPI_IPA_EVENT_SIGNAL_ISP_PREPARE,
-	RPI_IPA_EVENT_QUEUE_REQUEST,
-};
-
 enum RPiIpaMask {
 	ID		= 0x0ffff,
 	STATS		= 0x10000,
diff --git a/include/libcamera/ipa/raspberrypi.libcamera.decl b/include/libcamera/ipa/raspberrypi.libcamera.decl
new file mode 100644
index 0000000..851ae6c
--- /dev/null
+++ b/include/libcamera/ipa/raspberrypi.libcamera.decl
@@ -0,0 +1,83 @@
+namespace RPi;
+
+// this is necessary for de/serializeControlList()
+// define in include/libcamera/ipa/raspberrypi.h
+ControlInfoMap RPiControls;
+
+// opcodes
+// these must be enums, and must be named as shown
+
+// required
+// must specify which union option each value corresponds to
+enum ConfigOps {
+	RPI_IPA_CONFIG_LS_TABLE:lsTableHandle,
+	RPI_IPA_CONFIG_STAGGERED_WRITE:staggeredWriteResult,
+	RPI_IPA_CONFIG_SENSOR:controls,
+	RPI_IPA_CONFIG_SEND_FD:bufferFd,
+}
+
+// required
+// must specify which union option each value corresponds to
+enum EventOps {
+	RPI_IPA_EVENT_SIGNAL_STAT_READY:bufferId,
+	RPI_IPA_EVENT_SIGNAL_ISP_PREPARE:ispPrepare,
+	RPI_IPA_EVENT_QUEUE_REQUEST:controls,
+	RPI_IPA_EVENT_SEND_FD:bufferFd,
+}
+
+// required
+// must specify which union option each value corresponds to
+enum ActionOps {
+	RPI_IPA_ACTION_V4L2_SET_STAGGERED:controls,
+	RPI_IPA_ACTION_V4L2_SET_ISP:controls,
+	RPI_IPA_ACTION_STATS_METADATA_COMPLETE:statsComplete,
+	RPI_IPA_ACTION_RUN_ISP:bufferId,
+	RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME:bufferId,
+	RPI_IPA_ACTION_EMBEDDED_COMPLETE:bufferId,
+}
+
+// Custom Data containers
+
+table RPiStaggeredWritePayload {
+	gainDelay:uint32;
+	exposureDelay:uint32;
+	sensorMetadata:uint32;
+}
+
+table RPiIspPreparePayload {
+	embeddedbufferId:uint32;
+	bayerbufferId:uint32;
+}
+
+table RPiStatsCompletePayload {
+	bufferId:uint32;
+	controls:ControlList;
+}
+
+
+// First level payload unions
+// these must be unions, and must be named as shown
+
+// required
+// this one is actually in a vector of payload objects (op-union pair)
+union ConfigPayload {
+	lsTableHandle:uint32,
+	staggeredWriteResult:RPiStaggeredWritePayload,
+	controls:ControlList,
+	bufferFd:int32,
+}
+
+// required
+union EventPayload {
+	bufferId:uint32,
+	ispPrepare:RPiIspPreparePayload,
+	controls:ControlList,
+	bufferFd:int32,
+}
+
+// required
+union ActionPayload {
+	bufferId:uint32,
+	statsComplete:RPiStatsCompletePayload,
+	controls:ControlList,
+}
diff --git a/include/libcamera/ipa/raspberrypi_wrapper.h b/include/libcamera/ipa/raspberrypi_wrapper.h
new file mode 100644
index 0000000..eab51d3
--- /dev/null
+++ b/include/libcamera/ipa/raspberrypi_wrapper.h
@@ -0,0 +1,658 @@
+// will be automatically generated by custom compiler
+
+#ifndef __LIBCAMERA_IPA_INTERFACE_RASPBERRYPI_FB_H__
+#define __LIBCAMERA_IPA_INTERFACE_RASPBERRYPI_FB_H__
+
+#include "raspberrypi_generated.h"
+
+namespace libcamera {
+
+// TODO this (and deserialize) should go global
+inline flatbuffers::Offset<ControlList> serializeControlList(flatbuffers::FlatBufferBuilder &builder, ControlList &list)
+{
+	ControlSerializer serializer;
+
+	// TODO need a way to get the info map (param? #defined param?)
+	size_t size = serializer.binarySize(RPiControls);
+	std::vector<uint8_t> infoData(size);
+	ByteStreamBuffer buffer(infoData.data(), infoData.size());
+	serializer.serialize(RPiControls, buffer);
+
+	size = serializer.binarySize(list);
+	std::vector<uint8_t> listData(size);
+	buffer = ByteStreamBuffer(listData.data(), listData.size());
+	serializer.serialize(list, buffer);
+
+	// don't need to error check; just write empty vector
+	return CreateControlListDirect(builder, listData);
+}
+
+// TODO fix the input type for this (like ControlListFB or something?)
+inline ControlList deserializeControlList(ControlList *obj)
+{
+	ControlSerializer deserializer;
+	std::vector<uint8_t> buf = std::vector(obj->begin(), obj->end());
+
+	std::vector<uint8_t> infoData(size);
+	BytesStreamBuffer buffer(const_cast<const uint8_t *>(infoData.data()), infoData.size());
+	ControlInfoMap infoMap = deserializer.deserialize<ControlInfoMap>(buffer);
+	// TODO what to do for error checking?
+
+	std::vector<uint8_t> listData(size);
+	buffer = ByteStreamBuffer(const_cast<const uint8_t *>(listData.data()), listData.size());
+	ControlList list = deserializer.deserialize<ControlList>(buffer);
+
+	return list;
+}
+
+
+enum RPiConfigParametersWrapper {
+	RPI_IPA_CONFIG_LS_TABLE = 1,
+	RPI_IPA_CONFIG_STAGGERED_WRITE = 2,
+	RPI_IPA_CONFIG_SENSOR = 3,
+	RPI_IPA_CONFIG_SEND_FD = 4,
+};
+
+enum RPiEventsWrapper {
+  RPI_IPA_EVENT_SIGNAL_STAT_READY = 1,
+  RPI_IPA_EVENT_SIGNAL_ISP_PREPARE = 2,
+  RPI_IPA_EVENT_QUEUE_REQUEST = 3,
+  RPI_IPA_EVENT_SEND_FD = 4,
+};
+
+enum RPiActionsWrapper {
+	RPI_IPA_ACTION_V4L2_SET_STAGGERED,
+	RPI_IPA_ACTION_V4L2_SET_ISP,
+	RPI_IPA_ACTION_STATS_METADATA_COMPLETE,
+	RPI_IPA_ACTION_RUN_ISP,
+	RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME,
+	RPI_IPA_ACTION_EMBEDDED_COMPLETE,
+};
+
+struct RPiStaggeredWritePayloadWrapper : Serializable
+{
+	RPiStaggeredWritePayloadWrapper() {}
+
+	RPiStaggeredWritePayloadWrapper(uint32_t gainDelay, uint32_t exposureDelay, uint32_t sensorMetadata) : gainDelay_(gainDelay), exposureDelay_(exposureDelay), sensorMetadata_(sensorMetadata) {}
+
+	RPiStaggeredWritePayloadWrapper(const RPiStaggeredWritePayload *buf)
+	{
+		const RPiStaggeredWritePayload *obj = flatbuffers::GetRoot<RPiStaggeredWritePayload>(buf);
+
+		gainDelay_ = obj->gainDelay();
+		exposureDelay_ = obj->exposureDelay();
+		sensorMetadata_ = obj->sensorMetadata();
+	}
+
+	flatbuffers::Offset<RPiStaggeredWritePayload> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// nop for gainDelay_
+
+		// nop for exposureDelay_
+
+		// nop for sensorMetadata_
+
+		return CreateRPiStaggeredWritePayload(builder, gainDelay_, exposureDelay_, sensorMetadata_);
+	}
+
+	std::deque<int> extractFds()
+	{
+		// contains no fd and no compound children, so return empty
+		return {};
+	}
+
+	void injectFds(std::deque<int> &fds)
+	{
+		// nop
+	}
+
+	uint32_t gainDelay_;
+	uint32_t exposureDelay_;
+	uint32_t sensorMetadata_;
+}
+
+struct RPiIspPreparePayloadWrapper : Serializable
+{
+	RPiIspPreparePayloadWrapper() {}
+
+	RPiIspPreparePayloadWrapper(uint32_t embeddedbufferId, uint32_t bayerbufferId) : embeddedbufferId_(embeddedbufferId), bayerbufferId_(bayerbufferId) {}
+
+	RPiIspPreparePayloadWrapper(const RPiIspPreparePayload *buf)
+	{
+		const RPiIspPreparePayload *obj = flatbuffers::GetRoot<RPiIspPreparePayload>(buf);
+
+		embeddedbufferId_ = obj->embeddedbufferId();
+		bayerbufferId_ = obj->bayerbufferId();
+	}
+
+	flatbuffers::Offset<RPiIspPreparePayload> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// nop for embeddedbufferId_
+
+		// nop for bayerbufferId_
+
+		return CreateRPiIspPreparePayload(builder, embeddedbufferId_, bayerbufferId_);
+	}
+
+	// these could be generated for every struct and not just those that
+	// fds in their tree. that'll save parsing the dependency graph in the
+	// compiler
+	std::deque<int> extractFds()
+	{
+		return {};
+	}
+
+	void injectFds(std::deque<int> &fds)
+	{
+		// nop
+	}
+
+	uint32_t embeddedbufferId_;
+	uint32_t bayerbufferId_;
+}
+
+struct RPiStatsCompletePayloadWrapper : Serializable
+{
+	RPiStatsCompletePayloadWrapper() {}
+
+	RPiStatsCompletePayloadWrapper(uint32_t bufferId, ControlList &controls) : bufferId_(bufferId), controls_(controls) {}
+
+	// deserialize
+	RPiStatsCompletePayloadWrapper(const RPiStatsCompletePayload *buf)
+	{
+		const RPiStatsCompletePayload *obj = flatbuffers::GetRoot<RPiStatsCompletePayload>(buf);
+
+		bufferId_ = obj->bufferId();
+		controls_ = deserializeControlList(obj->controls());
+	}
+
+	flatbuffers::Offset<RPiStatsCompletePayloadWrapper> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// nop for bufferId_
+
+		// serialize controls_
+		flatbuffers::Offset<ControlList> controls = serializeControlList(builder, controls_);
+
+		return CreateRPiStatsCompletePayload(builder, bufferId_, controls);
+	}
+
+	uint32_t bufferId_;
+	ControlList controls_;
+}
+
+union RPiConfigureUnionWrapper
+{
+	uint32_t lsTableHandle_;
+	RPiStaggeredWritePayload staggeredWriteResult_;
+	ControlList contols_;
+	int32_t bufferFd_;
+}
+
+struct RPiConfigurePayloadWrapper : Serializable
+{
+	RPiConfigurePayloadWrapper() {}
+
+	RPiConfigurePayloadWrapper(enum RPiConfigParametersWrapper op, RPiConfigureUnionWrapper payload) : op_(op), payload_(payload) {}
+
+	// deserialize
+	RPiConfigurePayloadWrapper(const RPiConfigurePayload *buf)
+	{
+		// yeah fill this in if you want
+	}
+
+	// deserialize
+	// TODO need this if is a member of vector
+	RPiConfigurePayloadWrapper(flatbuffers::Offset<RPiConfigurePayload> &p)
+	{
+		// nop for op
+		op_ = p.op();
+
+		// start of union block for payload_
+		switch (op_) {
+		case RPI_IPA_CONFIG_LS_TABLE:
+			payload_.lsTableHandle_ = p.payload_as_lsTableHandle();
+			break;
+		case RPI_IPA_CONFIG_STAGGERED_WRITE:
+			payload_.staggeredWriteResult_ = RPiStaggeredWritePayloadWrapper(p.payload_as_staggeredWriteResult);
+			break;
+		case RPI_IPA_CONFIG_SENSOR:
+			payload_.controls_ = deserializeControlList(p.payload_as_controls());
+			break;
+		case RPI_IPA_CONFIG_SEND_FD:
+			payload_.bufferFd_ = p.payload_as_bufferFd();
+			break;
+		}
+		// end of union block for payload_
+	}
+
+	flatbuffers::Offset<RPiConfigurePayload> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// nop for op_
+		RPiConfigureUnion payloadType;
+		flatbuffers::Offset<void> payload;
+
+		// start of union block for payload_
+		switch (op_) {
+		case RPI_IPA_CONFIG_LS_TABLE:
+			payloadType = RPiConfigureUnion_lsTableHandle;
+			payload = CreateUnsignedInt(builder, payload_.lsTableHandle_);
+			break;
+		case RPI_IPA_CONFIG_STAGGERED_WRITE:
+			payloadType = RPiConfigureUnion_staggeredWriteResult;
+			payload = payload_.staggeredWriteResult_.serialize(builder);
+			break;
+		case RPI_IPA_CONFIG_SENSOR:
+			// controls
+			payloadType = RPiConfigureUnion_controls;
+			payload = serializeControlList(builder, payload_.controls_);
+			break;
+		case RPI_IPA_CONFIG_SEND_FD:
+			payloadType = RPiConfigureUnion_bufferFd;
+			payload = CreateSignedInt(builder, payload_.bufferFd_);
+			break;
+		}
+		// end of union block for payload_
+
+		return CreateRPiConfigurePayload(builder, op_, payloadType, payload);
+	}
+
+	std::deque<int> extractFds()
+	{
+		std::deque<int> fds;
+
+		// nop for op_, since it has no children, and is not fd
+
+		// start of union block for payload_
+		switch (op_) {
+		case RPI_IPA_CONFIG_LS_TABLE:
+			// value is not fd
+			break;
+		case RPI_IPA_CONFIG_STAGGERED_WRITE:
+			std::deque<int> pfds = payload_.staggeredWriteResult_.extractFds();
+			fds.insert(fds.end(), pfds,begin(), pfds.end());
+			break;
+		case RPI_IPA_CONFIG_SENSOR:
+			// value is not fd
+			break;
+		case RPI_IPA_CONFIG_SEND_FD:
+			fds.push_back(payload.bufferFd_);
+			break;
+		}
+		// end of union block for payload_
+
+		return fds;
+	}
+
+	void injectFds(std::deque<int> &fds)
+	{
+		// nop for op_
+
+		// start of union block for payload_
+		switch (op_) {
+		case RPI_IPA_CONFIG_LS_TABLE:
+			break;
+		case RPI_IPA_CONFIG_STAGGERED_WRITE:
+			payload_.staggeredWriteResult_.injectFds(fds);
+			break;
+		case RPI_IPA_CONFIG_SENSOR:
+			break;
+		case RPI_IPA_CONFIG_SEND_FD:
+			payload_.bufferFd_ = fds.at(0);
+			fds.pop_front();
+			break;
+		}
+		// end of union block for payload_
+	}
+
+	enum RPiConfigParametersWrapper op_;
+	RPiConfigureUnionWrapper payload_;
+}
+
+union RPiEventPayloadWrapper
+{
+	uint32_t bufferId_;
+	RPiIspPreparePayloadWrapper ispPrepare_;
+	ControlList controls_;
+	int32_t bufferFd_;
+}
+
+union RPiActionPayloadWrapper
+{
+	uint32_t bufferId_;
+	RPiStatsCompletePayloadWrapper statsComplete_;
+	ControlList controls_;
+}
+
+struct RPiConfigureParamsWrapper : Serializable
+{
+	RPiConfigureParamsWrapper() {}
+
+	RPiConfigureParamsWrapper(std::vector<RPiConfigurePayloadWrapper> payload) : payload_(payload) {}
+
+	// deserialize
+	// only top-level gets fds
+	RPiConfigureParamsWrapper(const uint8_t *buf, std::dequeue<int> fds)
+	{
+		const RPiConfigureParams *obj = flatbuffers::GetRoot<RPiConfigureParams>(buf);
+
+		// start of vector block for payload_
+		flatbuffers::Vector<flatbuffers::Offset<RPiConfigurePayload>> *payload = obj->params();
+		std::transform(payload->begin(), payload->end(), std::back_inserter(payload_),
+				[](flatbuffers::Offset<RPiConfigurePayload> &p) -> RPiConfigurePayloadWrapper {
+					return RPiConfigurePayloadWrapper(p);
+				});
+		// end of vector block for payload_
+
+
+		// after constructing all the children, we need to inject the fds
+		injecetFds(fds);
+	}
+
+	flatbuffers::Offset<RPiConfigureParams> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// start of vector block for payload_
+		std::vector<flatbuffers::Offset<RPiConfigurePayload>> payload;
+		std::transform(payload_.begin(), payload_.end(), std::back_inserter(payload),
+				[](RPiConfigurePayloadWrapper &p) -> flatbuffers::Offset<RPiConfigurePayload> { return p.serialize(builder); });
+		// end of vector block for payload_
+
+		// use direct if there's vector as a member
+		return CreateRPiConfigureParamsDirect(builder, payload);
+	}
+
+	// this is only for root_type
+	std::deque<int> extractFds()
+	{
+		std::deque<int> fds;
+
+		// start of vector block for payload_
+		for (RPiConfigurePayloadWrapper &payload : payload_)  {
+			std::deque<int> &pfds = payload_.extractFds();
+			fds.insert(fds.end(), pfds.begin(), pfds.end());
+		}
+		// end of vector block for payload_
+
+		return fds;
+	}
+
+	void injectFds(std::deque<int> &fds)
+	{
+		// start of vector block for payload_
+		for (RPiConfigurePayloadWrapper &payload : payload_) 
+			payload_.injectFds(fds);
+		// end of vector block for payload_
+	}
+
+	// this is only for root_type
+	// returns buffer pointer, buffer size (in bytes), and vector of fds
+	// the fds are matched by index; the index maps to full path in object
+	// tree, generated by the special compiler
+	std::tuple<uint8_t *, size_t, std::deque<int>> serialize()
+	{
+		flatbuffers::FlatBufferBuilder builder(1024);
+		builder.Finish(this.serialize(builder));
+
+		uint8_t *buf = builder.GetBufferPointer();
+		size_t size = builder.GetSize();
+		return {buf, size, extractFds()};
+	}
+
+	std::vector<RPiConfigurePayloadWrapper> payload_;
+}
+
+struct RPiEventParamsWrapper : Serializable
+{
+	RPiEventParamsWrapper() {}
+
+	RPiEventParamsWrapper(enum RPiEventsWrapper ev, RPiEventPayloadWrapper payload) : ev_(ev), payload_(payload) {}
+
+	// deserialize
+	// root_type needs fds
+	RPiEventParamsWrapper(const uint8_t *buf, std::deque<int> fds)
+	{
+		const RPiEventParams *obj = flatbuffers::GetRoot<RPiEventParams>(buf);
+
+		ev_ = obj->ev();
+
+		// start of union block for payload_
+		switch (ev_) {
+		case RPI_IPA_EVENT_SIGNAL_STAT_READY:
+			payload_.bufferId_ = obj->payload_as_bufferId()->value();
+			break;
+		case RPI_IPA_EVENT_SIGNAL_ISP_PREPARE:
+			payload_.ispPrepare_ = RPiIspPreparePayloadWrapper(obj->payload_as_ispPrepare());
+			break;
+		case RPI_IPA_EVENT_QUEUE_REQUEST:
+			payload_.controls_ = deserializeControlList(obj->payload_as_controls());
+			break;
+		case RPI_IPA_EVENT_SEND_FD:
+			payload_.bufferFd_ = obj->payload_as_bufferFd()->value();
+			break;
+		}
+		// end of union block for payload_
+
+		injectFds(fds);
+	}
+
+	flatbuffers::Offset<RPiEventParams> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// nop for ev_
+
+		// start of union block for payload_
+		RPiEventPayload payloadType;
+		flatbuffers::Offset<void> payload;
+
+		switch (ev_) {
+		case RPI_IPA_EVENT_SIGNAL_STAT_READY:
+			payloadType = RPiEventPayload_bufferId;
+			payload = CreateUnsignedInt(builder, payload_.bufferId_);
+			break;
+		case RPI_IPA_EVENT_SIGNAL_ISP_PREPARE:
+			payloadType = RPiEventPayload_ispPrepare;
+			payload = payload_.ispPrepare_.serialize(builder);
+			break;
+		case RPI_IPA_EVENT_QUEUE_REQUEST:
+			payloadType = RPiEventPayload_controls;
+			payload = serializeControlList(builder, payload_.controls_);
+			break;
+		case RPI_IPA_EVENT_SEND_FD:
+			payloadType = RPiEventPayload_bufferFd;
+			payload = CreateSignedInt(builder, payload_.bufferFd_);
+			break;
+		}
+		// end of union block
+
+		return CreateRPiEventParams(builder, ev_, payloadType, payload);
+	}
+
+	std::deque<int> extractFds()
+	{
+		std::deque<int> fds;
+
+		// nop for ev_
+
+		// start of union block for payload_
+		switch (ev_) {
+		case RPI_IPA_EVENT_SIGNAL_STAT_READY:
+			// non-fd scalar
+			break;
+		case RPI_IPA_EVENT_SIGNAL_ISP_PREPARE:
+			std::deque<int> &pfds = payload_.ispPrepare_.extractFds();
+			fds.insert(fds.end(), pfds.begin(), pfds.end());
+			break;
+		case RPI_IPA_EVENT_QUEUE_REQUEST:
+			// non-fd controls
+			break;
+		case RPI_IPA_EVENT_SEND_FD:
+			fds.push_back(payload_.bufferFd_);
+			break;
+		}
+		// end of union block for payload_
+
+		return fds;
+	}
+
+	void injectFds(std::deque<int> &fds)
+	{
+		// nop for ev_
+
+		// start of union block for payload_
+		switch (ev_) {
+		case RPI_IPA_EVENT_SIGNAL_STAT_READY:
+			break;
+		case RPI_IPA_EVENT_SIGNAL_ISP_PREPARE:
+			payload_.ispPrepare_.injectFds(fds);
+			break;
+		case RPI_IPA_EVENT_QUEUE_REQUEST:
+			break;
+		case RPI_IPA_EVENT_SEND_FD:
+			payload_.bufferFd_ = fds.at(0);
+			fds.pop_front();
+			break;
+		}
+		// end of union block for payload_
+	}
+
+	// this is only for root_type
+	// returns buffer pointer, buffer size (in bytes), and vector of fds
+	// the fds are matched by index; the index maps to full path in object
+	// tree, generated by the special compiler
+	std::tuple<uint8_t *, size_t, std::deque<int>> serialize()
+	{
+		flatbuffers::FlatBufferBuilder builder(1024);
+		builder.Finish(this.serialize(builder));
+
+		uint8_t *buf = builder.GetBufferPointer();
+		size_t size = builder.GetSize();
+		return {buf, size, extractFds()};
+	}
+
+	enum RPiEventsWrapper ev_;
+	RPiEventPayloadWrapper payload_;
+}
+
+struct RPiActionParamsWrapper : Serializable
+{
+	RPiActionParamsWrapper() {}
+
+	RPiActionParamsWrapper(enum RPiActionsWrapper op, RPiActionPayloadWrapper payload) : op_(op), payload_(payload) {}
+
+	// deserialize
+	// root_type needs fds
+	RPiActionParamsWrapper(const uint8_t *buf, std::deque<int> &fds)
+	{
+		const RPiActionParams *obj = flatbuffers::GetRoot<RPiActionParams>(buf);
+
+		op_ = obj->op();
+
+		// start of union block for payload_
+		switch (op_) {
+		case RPI_IPA_ACTION_V4L2_SET_STAGGERED:
+		case RPI_IPA_ACTION_V4L2_SET_ISP:
+			payload_.controls_ = deserializeControlList(obj->payload_as_controls());
+			break;
+		case RPI_IPA_ACTION_STATS_METADATA_COMPLETE:
+			payload_.statsComplete_ = RPiStatsCompletePayloadWrapper(obj->payload_as_statsComplete());
+			break;
+		case RPI_IPA_ACTION_RUN_ISP:
+		case RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME:
+		case RPI_IPA_ACTION_EMBEDDED_COMPLETE:
+			// UnsignedInt needs ->value()
+			payload_.bufferId_ = obj->payload_as_bufferId()->value();
+			break;
+		}
+		// end of union block for payload_
+
+		injectFds(fds);
+	}
+
+	flatbuffers::Offset<RPiEventParams> serialize(flatbuffers::FlatBufferBuilder &builder)
+	{
+		// - construct compound children depth-first
+		// - get simple data
+		// - feed to Create
+
+		// nop for op_
+
+		// start of union block for payload_
+		RPiActionPayload payloadType;
+		flatbuffers::Offset<void> payload;
+
+		switch (op_) {
+		case RPI_IPA_ACTION_V4L2_SET_STAGGERED:
+		case RPI_IPA_ACTION_V4L2_SET_ISP:
+			payloadType = RPiActionPayload_controls;
+			payload = serializeControlList(builder, payload_.controls_);
+			break;
+		case RPI_IPA_ACTION_STATS_METADATA_COMPLETE:
+			payloadType = RPiActionPayload_statsComplete;
+			payload = payload_.statsComplete_.serialize(builder);
+			break;
+		case RPI_IPA_ACTION_RUN_ISP:
+		case RPI_IPA_ACTION_RUN_ISP_AND_DROP_FRAME:
+		case RPI_IPA_ACTION_EMBEDDED_COMPLETE:
+			payloadType = RPiActionPayload_bufferId;
+			payload = CreateUnsignedInt(builder, payload_.bufferId_);
+			break;
+		}
+		// end of union block for payload_
+
+		return CreateRPiActionParams(builder, op_, payloadType, payload);
+	}
+
+	// root_type needs it even if it's empty, because of top-level serialize()
+	std::deque<int> extractFds()
+	{
+		return {};
+	}
+
+	std::deque<int> injectFds()
+	{
+		// nop
+	}
+
+	// this is only for root_type
+	// returns buffer pointer, buffer size (in bytes), and vector of fds
+	// the fds are matched by index; the index maps to full path in object
+	// tree, generated by the special compiler
+	std::tuple<uint8_t *, size_t, std::deque<int>> serialize()
+	{
+		flatbuffers::FlatBufferBuilder builder(1024);
+		builder.Finish(this.serialize(builder));
+
+		uint8_t *buf = builder.GetBufferPointer();
+		size_t size = builder.GetSize();
+		return {buf, size, extractFds()};
+	}
+
+	enum RPiActionsWrapper op_;
+	RPiActionPayloadWrapper payload_;
+}
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_IPA_INTERFACE_RASPBERRYPI_FB_H__ */
