diff --git a/DEPENDS b/DEPENDS
index 7d43690b3c115ea60f482031806a6dc36a6eef3b..b18c86bacb67525e328f47a25aab32e930d0ac6d 100644
--- a/DEPENDS
+++ b/DEPENDS
@@ -1,3 +1,3 @@
 TrainMatcher, 2.3.2-2.16.2
-calngDeps, 1.0.0-2.16.2
+calngDeps, 1.0.1-2.16.2
 calibrationClient, 11.0.0
diff --git a/setup.py b/setup.py
index e600454731c8032d3f474a5b7bd67150e04d062f..2f94de91bc5b5eb48fbdd96f23e00ce2ce6de65b 100644
--- a/setup.py
+++ b/setup.py
@@ -31,10 +31,12 @@ setup(name='calng',
               'Gotthard2Correction = calng.corrections.Gotthard2Correction:Gotthard2Correction',
               'JungfrauCorrection = calng.corrections.JungfrauCorrection:JungfrauCorrection',
               'LpdCorrection = calng.corrections.LpdCorrection:LpdCorrection',
+              'LpdminiCorrection = calng.corrections.LpdminiCorrection:LpdminiCorrection',
               'ShmemToZMQ = calng.ShmemToZMQ:ShmemToZMQ',
               'ShmemTrainMatcher = calng.ShmemTrainMatcher:ShmemTrainMatcher',
               'DetectorAssembler = calng.DetectorAssembler:DetectorAssembler',
               'Gotthard2Assembler = calng.Gotthard2Assembler:Gotthard2Assembler',
+              'LpdminiSplitter = calng.LpdminiSplitter:LpdminiSplitter',
           ],
 
           'karabo.middlelayer_device': [
@@ -47,6 +49,7 @@ setup(name='calng',
               'Dssc1MGeometry = calng.geometries:Dssc1MGeometry.Dssc1MGeometry',
               'Epix100Geometry = calng.geometries:Epix100Geometry.Epix100Geometry',
               'Lpd1MGeometry = calng.geometries:Lpd1MGeometry.Lpd1MGeometry',
+              'LpdminiGeometry = calng.geometries:LpdminiGeometry.LpdminiGeometry',
               'JungfrauGeometry = calng.geometries:JungfrauGeometry.JungfrauGeometry',
               'RoiTool = calng.RoiTool:RoiTool',
           ],
@@ -78,6 +81,12 @@ setup(name='calng',
                   extra_compile_args=['-O3', '-march=native', '-fopenmp'],
                   extra_link_args=['-fopenmp'],
               ),
+              Extension(
+                  'calng.kernels.lpd_cython',
+                  ['src/calng/kernels/lpd_cpu.pyx'],
+                  extra_compile_args=['-O3', '-march=native', '-fopenmp'],
+                  extra_link_args=['-fopenmp'],
+              ),
           ],
           compiler_directives={
               'language_level': 3
diff --git a/src/calng/LpdminiSplitter.py b/src/calng/LpdminiSplitter.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed4c78a6086f1370716e9a0c2f402b1f3429a2fd
--- /dev/null
+++ b/src/calng/LpdminiSplitter.py
@@ -0,0 +1,242 @@
+from timeit import default_timer
+
+import numpy as np
+from karabo.bound import (
+    DOUBLE_ELEMENT,
+    INPUT_CHANNEL,
+    KARABO_CLASSINFO,
+    NODE_ELEMENT,
+    OUTPUT_CHANNEL,
+    OVERWRITE_ELEMENT,
+    UINT64_ELEMENT,
+    VECTOR_UINT32_ELEMENT,
+    VECTOR_STRING_ELEMENT,
+    ChannelMetaData,
+    Hash,
+    MetricPrefix,
+    PythonDevice,
+    State,
+    Timestamp,
+    Unit,
+)
+from karabo.common.api import KARABO_SCHEMA_DISPLAY_TYPE_SCENES as DT_SCENES
+
+from . import scenes, schemas, shmem_utils, utils
+from .base_correction import WarningLampType
+from ._version import version as deviceVersion
+
+# plan:
+# - split data into 8 modules
+# - copy each into shared memory and send on distinct channel
+
+PROCESSING_STATE_TIMEOUT = 10
+
+
+@KARABO_CLASSINFO("LpdminiSplitter", deviceVersion)
+class LpdminiSplitter(PythonDevice):
+    # up to 8 minis, using 1-indexing for naming
+    _output_channel_names = [f"output-{i+1}" for i in range(8)]
+
+    @staticmethod
+    def expectedParameters(expected):
+        (
+            OVERWRITE_ELEMENT(expected)
+            .key("state")
+            .setNewDefaultValue(State.INIT)
+            .commit(),
+
+            INPUT_CHANNEL(expected)
+            .key("input")
+            .commit(),
+
+            VECTOR_UINT32_ELEMENT(expected)
+            .key("outputDataShape")
+            .displayedName("Output data shape")
+            .readOnly()
+            .initialValue([512, 32, 256])
+            .commit(),
+
+            UINT64_ELEMENT(expected)
+            .key("trainId")
+            .displayedName("Train ID")
+            .description("ID of latest train processed by this device.")
+            .readOnly()
+            .initialValue(0)
+            .commit(),
+
+            NODE_ELEMENT(expected)
+            .key("performance")
+            .commit(),
+
+            DOUBLE_ELEMENT(expected)
+            .key("performance.rate")
+            .displayedName("Rate")
+            .description(
+                "Actual rate with which this device gets, processes, and sends trains. "
+                "This is a simple windowed moving average."
+            )
+            .unit(Unit.HERTZ)
+            .readOnly()
+            .initialValue(0)
+            .commit(),
+
+            DOUBLE_ELEMENT(expected)
+            .key("performance.processingTime")
+            .displayedName("Processing time")
+            .unit(Unit.SECOND)
+            .metricPrefix(MetricPrefix.MILLI)
+            .readOnly()
+            .initialValue(0)
+            .warnHigh(100)
+            .info("Processing too slow to reach 10 Hz")
+            .needsAcknowledging(False)
+            .commit(),
+
+            VECTOR_STRING_ELEMENT(expected)
+            .key("availableScenes")
+            .setSpecialDisplayType(DT_SCENES)
+            .readOnly()
+            .initialValue(["overview"])
+            .commit(),
+        )
+
+        for channel_name in LpdminiSplitter._output_channel_names:
+            (
+                OUTPUT_CHANNEL(expected)
+                .key(channel_name)
+                .dataSchema(schemas.xtdf_output_schema())
+                .commit(),
+            )
+
+    def __init__(self, config):
+        super().__init__(config)
+        self.registerInitialFunction(self._initialization)
+        self._output_image_shape = (512, 32, 256)  # will be updated based on frames
+        self.KARABO_SLOT(self.requestScene)
+        self._latest_log_message = None
+        self._latest_warn_type = None
+
+    def _initialization(self):
+        self.KARABO_ON_DATA("input", self.input_handler)
+        self._shmem_buffers = [
+            shmem_utils.ShmemCircularBuffer(
+                1 * 2**30,
+                (512, 32, 256),
+                np.uint16,
+                f"{self.getInstanceId()}:{channel_name}"
+            )
+            for channel_name in self._output_channel_names
+        ]
+
+        # performance measures and such
+        self._last_processing_started = 0  # used for processing time and timeout
+        self._buffered_status_update = Hash()
+        self._processing_time_tracker = utils.ExponentialMovingAverage(alpha=0.3)
+        self._rate_tracker = utils.WindowRateTracker()
+        self._input_delay_tracker = utils.ExponentialMovingAverage(alpha=0.3)
+        self._performance_measure_update_timer = utils.RepeatingTimer(
+            interval=1,
+            callback=self._update_performance_measures,
+        )
+        self.updateState(State.ON)
+
+    def input_handler(self, data_hash, metadata):
+        state = self.get("state")
+        if state is State.INIT:
+            return
+
+        # TODO: handle empty DAQ hash
+        if not data_hash.has("image.data"):
+            self.log_status_info("Input hash had no image data node")
+            if state is State.PROCESSING:
+                self.updateState(State.IGNORING)
+            return
+        try:
+            image_data = np.asarray(
+                data_hash.get("image.data")
+            )
+        except RuntimeError as err:
+            self.log_status_info(
+                f"Failed to load image data; probably empty hash from DAQ: {err}",
+                WarningLampType.EMPTY_HASH,
+            )
+            if state is State.PROCESSING:
+                self.updateState(State.IGNORING)
+            return
+
+        if state is not State.PROCESSING:
+            self.log_status_info("Processing data")
+            self.updateState(State.PROCESSING)
+
+        self._last_processing_started = default_timer()
+        timestamp = Timestamp.fromHashAttributes(metadata.getAttributes("timestamp"))
+        self._buffered_status_update.set("trainId", timestamp.getTrainId())
+        num_frames = data_hash["image.cellId"].size
+        image_data = image_data.reshape((num_frames, 256, 256))
+        # note: reshape is equivalent to overrideInputAxisOrder in BaseCorrection
+        if num_frames != self._output_image_shape:
+            self._output_image_shape = (num_frames, 32, 256)
+            for shmem_buffer in self._shmem_buffers:
+                shmem_buffer.change_shape(self._output_image_shape)
+            self.set("outputDataShape", list(self._output_image_shape))
+        # reshape to drop singleton axis + optionally fix DAQ shape shenanigans
+        receiver_source_name = metadata.get("source")
+        for i, (channel_name, shmem_buffer) in enumerate(
+            zip(self._output_channel_names, self._shmem_buffers)
+        ):
+            this_slice = image_data[:, i * 32 : (i + 1) * 32]
+            buffer_handle, buffer_array = shmem_buffer.next_slot()
+            buffer_array[:] = this_slice
+            data_hash.set("image.data", buffer_handle)
+            data_hash.set("calngShmemPaths", ["image.data"])
+            # TODO: consider source name; virtual indices like in offline?
+            channel = self.signalSlotable.getOutputChannel(channel_name)
+            channel.write(
+                data_hash,
+                ChannelMetaData(f"{receiver_source_name}-{i+1}", timestamp),
+                # adding 1-indexed suffix index to soruce name forwarded
+                copyAllData=False,
+            )
+            channel.update()
+        self._processing_time_tracker.update(
+            default_timer() - self._last_processing_started
+        )
+        self._rate_tracker.update()
+
+    def _update_performance_measures(self):
+        if self.get("state") in {State.PROCESSING, State.IGNORING}:
+            self._buffered_status_update.set(
+                "performance.rate", self._rate_tracker.get()
+            )
+            self._buffered_status_update.set(
+                "performance.processingTime", self._processing_time_tracker.get() * 1000
+            )
+            self.set(self._buffered_status_update)
+            if (
+                default_timer() - self._last_processing_started
+                > PROCESSING_STATE_TIMEOUT
+            ):
+                self.updateState(State.ON)
+
+    def requestScene(self, params):
+        payload = Hash()
+        payload["name"] = "overview"
+        payload["data"] = scenes.lpdmini_splitter_overview(
+            device_id=self.getInstanceId(),
+            schema=self.getFullSchema(),
+        )
+        payload["success"] = True
+        response = Hash()
+        response["type"] = "deviceScene"
+        response["origin"] = self.getInstanceId()
+        response["payload"] = payload
+        self.reply(response)
+
+    def log_status_info(self, msg, warn_type=None):
+        if (msg != self._latest_log_message) and (
+            warn_type is None or warn_type != self._latest_warn_type
+        ):
+            self.log.INFO(msg)
+            self.set("status", msg)
+            self._latest_log_message = msg
+            self._latest_warn_type = warn_type
diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index 2bbc38e42319fce73f234a7aee18ad7bd3d0aae9..57a62cee7ef626e90abaa31558b41a86da8259de 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -723,16 +723,7 @@ class BaseCorrection(PythonDevice):
         self._update_correction_flags()
         self._update_frame_filter()
 
-        self._buffered_status_update = Hash(
-            "trainId",
-            0,
-            "performance.rate",
-            0,
-            "performance.processingTime",
-            0,
-            "performance.ratioOfRecentTrainsReceived",
-            0,
-        )
+        self._buffered_status_update = Hash()
         self._processing_time_tracker = utils.ExponentialMovingAverage(alpha=0.3)
         self._rate_tracker = utils.WindowRateTracker()
         self._input_delay_tracker = utils.ExponentialMovingAverage(alpha=0.3)
@@ -1005,6 +996,7 @@ class BaseCorrection(PythonDevice):
                 self.output_data_dtype,
                 shmem_buffer_name,
             )
+            self._shmem_receiver = shmem_utils.ShmemCircularBufferReceiver()
             if self._cuda_pin_buffers:
                 self.log.INFO("Trying to pin the shmem buffer memory")
                 self._shmem_buffer.cuda_pin()
@@ -1092,6 +1084,7 @@ class BaseCorrection(PythonDevice):
                 WarningLampType.EMPTY_HASH,
                 only_print_once=True,
             ) as warn:
+                self._shmem_receiver.dereference_shmem_handles(data_hash)
                 try:
                     image_data = np.asarray(data_hash.get(self._image_data_path))
                     cell_table = (
diff --git a/src/calng/base_geometry.py b/src/calng/base_geometry.py
index e300162bed53a32e7521b0ec03ef1205ceb9c3d9..5ed201da7e05255c2106068f45059237a4a3a7ab 100644
--- a/src/calng/base_geometry.py
+++ b/src/calng/base_geometry.py
@@ -445,7 +445,7 @@ class ManualQuadrantsGeometryBase(ManualGeometryBase):
         self.quadrantCorners.offset.y = 0
 
 
-class ModuleListItem(Configurable):
+class OrientableModuleListItem(Configurable):
     posX = Double(
         assignment=Assignment.OPTIONAL,
         defaultValue=0,
@@ -458,20 +458,32 @@ class ModuleListItem(Configurable):
     orientY = Int32(assignment=Assignment.OPTIONAL, defaultValue=1)
 
 
-def make_manual_module_list_node(defaults):
-    class ManualModuleListNode(BaseManualGeometryConfigNode):
+class RotatableModuleListItem(Configurable):
+    posX = Double(
+        assignment=Assignment.OPTIONAL,
+        defaultValue=0,
+    )
+    posY = Double(
+        assignment=Assignment.OPTIONAL,
+        defaultValue=0,
+    )
+    rotate = Int32(assignment=Assignment.OPTIONAL, defaultValue=1)
+
+
+def make_manual_orientable_module_list_node(defaults):
+    class ManualOrientableModuleListNode(BaseManualGeometryConfigNode):
         modules = VectorHash(
             displayedName="Modules",
-            rows=ModuleListItem,
+            rows=OrientableModuleListItem,
             defaultValue=defaults,
             accessMode=AccessMode.RECONFIGURABLE,
             assignment=Assignment.OPTIONAL,
         )
 
-    return Node(ManualModuleListNode)
+    return Node(ManualOrientableModuleListNode)
 
 
-class ManualModuleListGeometryBase(ManualGeometryBase):
+class ManualOrientableModuleListGeometryBase(ManualGeometryBase):
     moduleList = None  # subclass must define (with nice defaults)
 
     @slot
@@ -499,3 +511,46 @@ class ManualModuleListGeometryBase(ManualGeometryBase):
             ).offset((self.moduleList.offset.x.value, self.moduleList.offset.y.value))
             await self._set_geometry(geometry)
             self.tweakGeometry._reset()
+
+
+def make_manual_rotatable_module_list_node(defaults):
+    class ManualRotatableModuleListNode(BaseManualGeometryConfigNode):
+        modules = VectorHash(
+            displayedName="Modules",
+            rows=RotatableModuleListItem,
+            defaultValue=defaults,
+            accessMode=AccessMode.RECONFIGURABLE,
+            assignment=Assignment.OPTIONAL,
+        )
+
+    return Node(ManualRotatableModuleListNode)
+
+
+class ManualRotatableModuleListGeometryBase(ManualGeometryBase):
+    moduleList = None  # subclass must define (with nice defaults)
+
+    @slot
+    def requestScene(self, params):
+        name = params.get("name", default="overview")
+        if name == "overview":
+            # Assumes there are correction devices known to manager
+            scene_data = scenes.modules_geometry_overview(
+                self.deviceId,
+                self.getDeviceSchema(),
+            )
+            payload = Hash("success", True, "name", name, "data", scene_data)
+
+        return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload)
+
+    async def _set_from_manual_config(self):
+        self._set_status("Updating geometry from manual configuration")
+        with self.push_state(State.CHANGING):
+            geometry = self.geometry_class.from_module_positions(
+                [(x, y) for (x, y, _) in self.moduleList.modules.value],
+                [
+                    rotation
+                    for (_, _, rotation) in self.moduleList.modules.value
+                ],
+            ).offset((self.moduleList.offset.x.value, self.moduleList.offset.y.value))
+            await self._set_geometry(geometry)
+            self.tweakGeometry._reset()
diff --git a/src/calng/corrections/LpdCorrection.py b/src/calng/corrections/LpdCorrection.py
index ed70abd5b7b020fadda1834687e6fa2e58bfbbb2..62a9e42f1637f1341514ce4df3aa21df157b91a3 100644
--- a/src/calng/corrections/LpdCorrection.py
+++ b/src/calng/corrections/LpdCorrection.py
@@ -48,7 +48,7 @@ class CorrectionFlags(enum.IntFlag):
 
 class LpdGpuRunner(base_kernel_runner.BaseKernelRunner):
     _gpu_based = True
-    _corrected_axis_order = "fxy"
+    _corrected_axis_order = "fyx"
 
     @property
     def input_shape(self):
@@ -81,7 +81,7 @@ class LpdGpuRunner(base_kernel_runner.BaseKernelRunner):
         self.processed_data = cp.empty(self.processed_shape, dtype=output_data_dtype)
         self.cell_table = cp.empty(frames, dtype=np.uint16)
 
-        self.map_shape = (constant_memory_cells, pixels_x, pixels_y, 3)
+        self.map_shape = (constant_memory_cells, pixels_y, pixels_x, 3)
         self.offset_map = cp.empty(self.map_shape, dtype=np.float32)
         self.gain_amp_map = cp.empty(self.map_shape, dtype=np.float32)
         self.rel_gain_slopes_map = cp.empty(self.map_shape, dtype=np.float32)
@@ -396,8 +396,8 @@ class LpdCorrection(BaseCorrection):
         return (
             self.unsafe_get("dataFormat.frames"),
             1,
-            self.unsafe_get("dataFormat.pixelsX"),
             self.unsafe_get("dataFormat.pixelsY"),
+            self.unsafe_get("dataFormat.pixelsX"),
         )
 
     def __init__(self, config):
diff --git a/src/calng/corrections/LpdminiCorrection.py b/src/calng/corrections/LpdminiCorrection.py
new file mode 100644
index 0000000000000000000000000000000000000000..688b174b23474f35346014e91bd3b9074879fb4a
--- /dev/null
+++ b/src/calng/corrections/LpdminiCorrection.py
@@ -0,0 +1,103 @@
+import numpy as np
+from karabo.bound import (
+    DOUBLE_ELEMENT,
+    KARABO_CLASSINFO,
+    OVERWRITE_ELEMENT,
+)
+
+from .._version import version as deviceVersion
+from ..base_correction import add_correction_step_schema
+from . import LpdCorrection
+
+
+class LpdminiGpuRunner(LpdCorrection.LpdGpuRunner):
+    def load_constant(self, constant_type, constant_data):
+        print(f"Given: {constant_type} with shape {constant_data.shape}")
+        # constant type → transpose order
+        constant_buffer_map = {
+            LpdCorrection.Constants.Offset: self.offset_map,
+            LpdCorrection.Constants.GainAmpMap: self.gain_amp_map,
+            LpdCorrection.Constants.FFMap: self.flatfield_map,
+            LpdCorrection.Constants.RelativeGain: self.rel_gain_slopes_map,
+        }
+        if constant_type in {
+            LpdCorrection.Constants.BadPixelsDark,
+            LpdCorrection.Constants.BadPixelsFF,
+        }:
+            self.bad_pixel_map |= self._xp.asarray(
+                constant_data,
+                dtype=np.uint32,
+            )[: self.constant_memory_cells]
+        else:
+            constant_buffer_map[constant_type].set(
+                constant_data.astype(np.float32)[: self.constant_memory_cells]
+            )
+
+
+class LpdminiCalcatFriend(LpdCorrection.LpdCalcatFriend):
+    @staticmethod
+    def add_schema(
+        schema,
+        managed_keys,
+    ):
+        super(LpdminiCalcatFriend, LpdminiCalcatFriend).add_schema(schema, managed_keys)
+        (
+            OVERWRITE_ELEMENT(schema)
+            .key("constantParameters.biasVoltage")
+            .setNewDisplayedName("Bias voltage (odd)")
+            .setNewDescription("Bias voltage apbplied to minis 1, 3, 5, and 7.")
+            .commit(),
+
+            DOUBLE_ELEMENT(schema)
+            .key("constantParameters.biasVoltage2")
+            .displayedName("Bias voltage (even)")
+            .description("Separate bias voltage used for minis 2, 4, 6, and 8.")
+            .assignmentOptional()
+            .defaultValue(300)
+            .reconfigurable()
+            .commit(),
+
+            OVERWRITE_ELEMENT(schema)
+            .key("constantParameters.pixelsY")
+            .setNewDefaultValue(32)
+            .commit(),
+        )
+        managed_keys.add("constantParameters.biasVoltage2")
+
+    def basic_condition(self):
+        res = super().basic_condition()
+        if int(self.device.get("fastSources")[0][-1]) % 2 == 0:
+            res["Sensor Bias Voltage"] = self._get_param("biasVoltage2")
+        if "category" in res:
+            del res["category"]
+        return res
+
+
+@KARABO_CLASSINFO("LpdminiCorrection", deviceVersion)
+class LpdminiCorrection(LpdCorrection.LpdCorrection):
+    _calcat_friend_class = LpdminiCalcatFriend
+    _kernel_runner_class = LpdminiGpuRunner
+    _managed_keys = LpdCorrection.LpdCorrection._managed_keys.copy()
+
+    @classmethod
+    def expectedParameters(cls, expected):
+        (
+            OVERWRITE_ELEMENT(expected)
+            .key("dataFormat.pixelsY")
+            .setNewDefaultValue(32)
+            .commit(),
+
+            OVERWRITE_ELEMENT(expected)
+            .key("dataFormat.frames")
+            .setNewDefaultValue(512)
+            .commit(),
+        )
+        cls._calcat_friend_class.add_schema(expected, cls._managed_keys)
+        # warning: this is redundant, but needed for now to get managed keys working
+        add_correction_step_schema(expected, cls._managed_keys, cls._correction_steps)
+        (
+            OVERWRITE_ELEMENT(expected)
+            .key("managedKeys")
+            .setNewDefaultValue(list(cls._managed_keys))
+            .commit()
+        )
diff --git a/src/calng/geometries/JungfrauGeometry.py b/src/calng/geometries/JungfrauGeometry.py
index c447accc7dec7857a5c858e3cfac62e001d2af29..e0c0703cb394a9b3f51fa2f163a62cc119c6a15a 100644
--- a/src/calng/geometries/JungfrauGeometry.py
+++ b/src/calng/geometries/JungfrauGeometry.py
@@ -2,12 +2,15 @@ import extra_geom
 import numpy as np
 from karabo.middlelayer import Hash
 
-from ..base_geometry import ManualModuleListGeometryBase, make_manual_module_list_node
+from ..base_geometry import (
+    ManualOrientableModuleListGeometryBase,
+    make_manual_orientable_module_list_node,
+)
 
 
-class JungfrauGeometry(ManualModuleListGeometryBase):
+class JungfrauGeometry(ManualOrientableModuleListGeometryBase):
     geometry_class = extra_geom.JUNGFRAUGeometry
-    moduleList = make_manual_module_list_node(
+    moduleList = make_manual_orientable_module_list_node(
         [
             Hash("posX", x, "posY", y, "orientX", ox, "orientY", oy)
             for (x, y, ox, oy) in [
diff --git a/src/calng/geometries/LpdminiGeometry.py b/src/calng/geometries/LpdminiGeometry.py
new file mode 100644
index 0000000000000000000000000000000000000000..31846b953ccc949564686e221b631bf0bd8c866e
--- /dev/null
+++ b/src/calng/geometries/LpdminiGeometry.py
@@ -0,0 +1,24 @@
+import extra_geom
+from karabo.middlelayer import Hash
+
+from ..base_geometry import (
+    ManualRotatableModuleListGeometryBase,
+    make_manual_rotatable_module_list_node,
+)
+
+
+class LpdminiGeometry(ManualRotatableModuleListGeometryBase):
+    geometry_class = extra_geom.LPD_MiniGeometry
+
+    moduleList = make_manual_rotatable_module_list_node(
+        [
+            Hash("posX", x, "posY", y, "rotate", r)
+            for (x, y, r) in [
+                (0, 0, 0),
+                # TODO: appropriate defaults
+            ]
+        ]
+    )
+
+    def _update_manual_from_current(self):
+        raise NotImplementedError()
diff --git a/src/calng/geometries/__init__.py b/src/calng/geometries/__init__.py
index 1526d0b903f59ff4810264dfa0f470258e1633bf..a13f829173ab32062f9192111e9fb87349eab331 100644
--- a/src/calng/geometries/__init__.py
+++ b/src/calng/geometries/__init__.py
@@ -4,5 +4,6 @@ from . import (
     Dssc1MGeometry,
     Epix100Geometry,
     Lpd1MGeometry,
+    LpdminiGeometry,
     JungfrauGeometry,
 )
diff --git a/src/calng/kernels/lpd_cpu.pyx b/src/calng/kernels/lpd_cpu.pyx
new file mode 100644
index 0000000000000000000000000000000000000000..0c3eb82d50c3e23d1414515fd967347ffe4a0876
--- /dev/null
+++ b/src/calng/kernels/lpd_cpu.pyx
@@ -0,0 +1,55 @@
+# cython: boundscheck=False
+# cython: cdivision=True
+# cython: wrapararound=False
+
+cdef unsigned char NONE = 0
+cdef unsigned char OFFSET = 1
+cdef unsigned char GAIN_AMP = 2
+cdef unsigned char REL_GAIN = 4
+cdef unsigned char FF_CORR = 8
+cdef unsigned char BPMASK = 16
+
+from cython.parallel import prange
+from libc.math cimport isinf, isnan
+
+def correct(
+    unsigned short[:, :, :, :] image_data,
+    unsigned short[:] cell_table,
+    unsigned char flags,
+    float[:, :, :, :] offset_map,
+    float[:, :, :, :] gain_amp_map,
+    float[:, :, :, :] rel_gain_slopes_map,
+    float[:, :, :, :] flatfield_map,
+    unsigned[:, :, :, :] bad_pixel_map,
+    float bad_pixel_mask_value,
+    # TODO: support spitting out gain map for preview purposes
+    float[:, :, :] output
+):
+    cdef int frame, map_cell, ss, fs
+    cdef unsigned char gain_stage
+    cdef float res
+    cdef unsigned short raw_data_value
+    for frame in prange(image_data.shape[0], nogil=True):
+        map_cell = cell_table[frame]
+        if map_cell >= offset_map.shape[0]:
+            for ss in range(image_data.shape[1]):
+                for fs in range(image_data.shape[2]):
+                    output[frame, ss, fs] = <float>image_data[frame, 0, ss, fs]
+            continue
+        for ss in range(image_data.shape[1]):
+            for fs in range(image_data.shape[3]):
+                raw_data_value = image_data[frame, 0, ss, fs]
+        gain_stage = (raw_data_value >> 12) & 0x0003
+        res = <float>(raw_data_value & 0x0fff)
+        if gain_stage > 2 or (flags & BPMASK and bad_pixel_map[map_cell, ss, fs, gain_stage] != 0):
+            res = bad_pixel_mask_value
+        else:
+            if flags & OFFSET:
+                res = res - offset_map[map_cell, ss, fs, gain_stage]
+            if flags & GAIN_AMP:
+                res = res * gain_amp_map[map_cell, ss, fs, gain_stage]
+            if flags & FF_CORR:
+                res = res * rel_gain_slopes_map[map_cell, ss, fs, gain_stage]
+            if res < 1e-7 or res > 1e7 or isnan(res) or isinf(res):
+                res = bad_pixel_mask_value
+        output[frame, ss, fs] = res
diff --git a/src/calng/scenes.py b/src/calng/scenes.py
index 2a6c16c0751e26071b4c5e142cf51b5af8effb7c..3849d6829f59d62b0f4b0b76dbef43ec3b17807f 100644
--- a/src/calng/scenes.py
+++ b/src/calng/scenes.py
@@ -9,6 +9,7 @@ from karabo.common.scenemodel.api import (
     DetectorGraphModel,
     DisplayCommandModel,
     DisplayLabelModel,
+    DisplayListModel,
     DisplayStateColorModel,
     DisplayTextLogModel,
     DoubleLineEditModel,
@@ -46,7 +47,13 @@ def DisplayRoundedFloat(*args, decimals=2, **kwargs):
     return EvaluatorModel(*args, expression=f"f'{{x:.{decimals}f}}'", **kwargs)
 
 
-_type_to_display_model = {"BOOL": CheckBoxModel, "FLOAT": DisplayRoundedFloat}
+_type_to_display_model = {
+    "BOOL": CheckBoxModel,
+    "FLOAT": DisplayRoundedFloat,
+    "STRING": DisplayLabelModel,
+    "UINT32": DisplayLabelModel,
+    "VECTOR_UINT32": DisplayListModel,
+}
 _type_to_line_editable = {
     "BOOL": (CheckBoxModel, {"klass": "EditableCheckBox"}),
     "DOUBLE": (DoubleLineEditModel, {}),
@@ -301,8 +308,13 @@ class DisplayAndEditableRow(HorizontalLayout):
         )
 
         if self.include_display(key_attr):
+            if value_type in _type_to_display_model:
+                model = _type_to_display_model[value_type]
+            else:
+                model = DisplayLabelModel
+                print(f"Scene generator would like to know more about {value_type}")
             self.children.append(
-                _type_to_display_model.get(value_type, DisplayLabelModel)(
+                model(
                     keys=[f"{device_id}.{key_path}"],
                     width=display_width * size_scale,
                     height=height,
@@ -551,7 +563,7 @@ class ManagerDeviceStatus(VerticalLayout):
 
 @titled("Device status", width=6 * NARROW_INC)
 @boxed
-class CorrectionDeviceStatus(VerticalLayout):
+class ProcessingDeviceStatus(VerticalLayout):
     def __init__(self, device_id, schema_hash):
         super().__init__(padding=0)
         name = DisplayLabelModel(
@@ -600,28 +612,29 @@ class CorrectionDeviceStatus(VerticalLayout):
                 ),
             ]
         )
-        self.children.append(
-            VerticalLayout(
-                children=[
-                    HorizontalLayout(
-                        LampModel(
-                            keys=[f"{device_id}.{warning_lamp}"],
-                            width=BASE_INC,
-                            height=BASE_INC,
-                        ),
-                        LabelModel(
-                            text=warning_lamp,
-                            width=8 * BASE_INC,
-                            height=BASE_INC,
-                        ),
-                    )
-                    for warning_lamp in schema_hash.getAttribute(
-                        "warningLamps", "defaultValue"
-                    )
-                ],
-                padding=0,
+        if schema_hash.has("warningLamps"):
+            self.children.append(
+                VerticalLayout(
+                    children=[
+                        HorizontalLayout(
+                            LampModel(
+                                keys=[f"{device_id}.{warning_lamp}"],
+                                width=BASE_INC,
+                                height=BASE_INC,
+                            ),
+                            LabelModel(
+                                text=warning_lamp,
+                                width=8 * BASE_INC,
+                                height=BASE_INC,
+                            ),
+                        )
+                        for warning_lamp in schema_hash.getAttribute(
+                            "warningLamps", "defaultValue"
+                        )
+                    ],
+                    padding=0,
+                )
             )
-        )
         self.children.append(status_log)
 
 
@@ -1241,7 +1254,7 @@ def scene_generator(fun):
 def correction_device_overview(device_id, schema):
     schema_hash = schema_to_hash(schema)
     main_overview = HorizontalLayout(
-        CorrectionDeviceStatus(device_id, schema_hash),
+        ProcessingDeviceStatus(device_id, schema_hash),
         VerticalLayout(
             recursive_editable(
                 device_id,
@@ -1287,6 +1300,15 @@ def correction_device_overview(device_id, schema):
     )
 
 
+@scene_generator
+def lpdmini_splitter_overview(device_id, schema):
+    schema_hash = schema_to_hash(schema)
+    return VerticalLayout(
+        ProcessingDeviceStatus(device_id, schema_hash),
+        DisplayRow(device_id, schema_hash, "outputDataShape", 6, 8),
+    )
+
+
 @scene_generator
 def correction_device_preview(device_id, schema, preview_channel):
     schema_hash = schema_to_hash(schema)
@@ -1694,6 +1716,7 @@ def gotthard2_assembler_overview(device_id, schema):
         ),
     )
 
+
 @scene_generator
 def quadrant_geometry_overview(device_id, schema):
     schema_hash = schema_to_hash(schema)