diff --git a/src/calng/AgipdCorrection.py b/src/calng/AgipdCorrection.py
index a9c5ecc623a31c6fda3dddaa3bd5f453d7a7e84a..09810d4df3ca670d0d04078ec6d79fcec432d0b6 100644
--- a/src/calng/AgipdCorrection.py
+++ b/src/calng/AgipdCorrection.py
@@ -3,19 +3,17 @@ from karabo.bound import (
     BOOL_ELEMENT,
     FLOAT_ELEMENT,
     KARABO_CLASSINFO,
-    NDARRAY_ELEMENT,
     NODE_ELEMENT,
     OUTPUT_CHANNEL,
     STRING_ELEMENT,
     VECTOR_STRING_ELEMENT,
-    Schema,
 )
 from karabo.common.states import State
 
 from . import shmem_utils, utils
 from ._version import version as deviceVersion
 from .agipd_gpu import AgipdGainMode, AgipdGpuRunner, BadPixelValues, CorrectionFlags
-from .base_correction import BaseCorrection, add_correction_step_schema
+from .base_correction import BaseCorrection, add_correction_step_schema, preview_schema
 from .calcat_utils import AgipdCalcatFriend, AgipdConstants
 
 
@@ -55,14 +53,7 @@ class AgipdCorrection(BaseCorrection):
             .defaultValue("ADAPTIVE_GAIN")
             .options("ADAPTIVE_GAIN,FIXED_HIGH_GAIN,FIXED_MEDIUM_GAIN,FIXED_LOW_GAIN")
             .commit(),
-            BOOL_ELEMENT(expected)
-            .key("sendGainMap")
-            .displayedName("Send gain map on dataOutput")
-            .assignmentOptional()
-            .defaultValue(False)
-            .commit(),
         )
-        # TODO: make sendGainMap reconfigurable
 
         (
             STRING_ELEMENT(expected)
@@ -72,14 +63,14 @@ class AgipdCorrection(BaseCorrection):
             .defaultValue("")
             .commit()
         )
-        preview_schema = Schema()
-        (
-            NODE_ELEMENT(preview_schema).key("data").commit(),
-            NDARRAY_ELEMENT(preview_schema).key("data.adc").dtype("FLOAT").commit(),
-        )
+
         (
             OUTPUT_CHANNEL(expected)
-            .key("preview.outputGain")
+            .key("preview.outputRawGain")
+            .dataSchema(preview_schema)
+            .commit(),
+            OUTPUT_CHANNEL(expected)
+            .key("preview.outputGainMap")
             .dataSchema(preview_schema)
             .commit(),
         )
@@ -207,7 +198,6 @@ class AgipdCorrection(BaseCorrection):
             "gain_mode": self.gain_mode,
             "bad_pixel_mask_value": self.bad_pixel_mask_value,
             "g_gain_value": config.get("corrections.relGainXray.gGainValue"),
-            "output_gain_map": config.get("sendGainMap"),
         }
 
         self._shmem_buffer_gain_map = None
@@ -285,11 +275,9 @@ class AgipdCorrection(BaseCorrection):
             (
                 preview_raw,
                 preview_corrected,
-            ) = self.gpu_runner.compute_preview(preview_slice_index)
-            if self._schema_cache["sendGainMap"]:
-                preview_gain = self.gpu_runner.compute_preview_gain(
-                    preview_slice_index
-                )
+                preview_raw_gain,
+                preview_gain_map,
+            ) = self.gpu_runner.compute_previews(preview_slice_index)
 
         # reusing input data hash for sending
         data_hash.set("image.data", buffer_handle)
@@ -315,26 +303,16 @@ class AgipdCorrection(BaseCorrection):
 
         self._write_output(data_hash, metadata)
         if do_generate_preview:
-            if self._schema_cache["sendGainMap"]:
-                self._write_combiner_previews(
-                    (
-                        ("preview.outputRaw", preview_raw),
-                        ("preview.outputCorrected", preview_corrected),
-                        ("preview.outputGain", preview_gain),
-                    ),
-                    train_id,
-                    source,
-                )
-                # TODO: DRY
-            else:
-                self._write_combiner_previews(
-                    (
-                        ("preview.outputRaw", preview_raw),
-                        ("preview.outputCorrected", preview_corrected),
-                    ),
-                    train_id,
-                    source,
-                )
+            self._write_combiner_previews(
+                (
+                    ("preview.outputRaw", preview_raw),
+                    ("preview.outputCorrected", preview_corrected),
+                    ("preview.outputRawGain", preview_raw_gain),
+                    ("preview.outputGainMap", preview_gain_map),
+                ),
+                train_id,
+                source,
+            )
 
     def _load_constant_to_gpu(self, constant, constant_data):
         # TODO: encode correction / constant dependencies in a clever way
diff --git a/src/calng/DsscCorrection.py b/src/calng/DsscCorrection.py
index df287d78725e4ba96c84abfbbc3d25daec6c55b3..30c99f7eaebf0923a589bf6cbbe2353638e97243 100644
--- a/src/calng/DsscCorrection.py
+++ b/src/calng/DsscCorrection.py
@@ -102,7 +102,7 @@ class DsscCorrection(BaseCorrection):
                 pulse_table,
                 warn_func=self.log_status_warn,
             )
-            preview_raw, preview_corrected = self.gpu_runner.compute_preview(
+            preview_raw, preview_corrected = self.gpu_runner.compute_previews(
                 preview_slice_index,
             )
 
diff --git a/src/calng/agipd_gpu.py b/src/calng/agipd_gpu.py
index ff212c6befaaa3c95565cc20d846f80f422c225c..df1bc811e4f804cde296ba578d9023ee6968a659 100644
--- a/src/calng/agipd_gpu.py
+++ b/src/calng/agipd_gpu.py
@@ -39,7 +39,6 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
         bad_pixel_mask_value=cupy.nan,
         gain_mode=AgipdGainMode.ADAPTIVE_GAIN,
         g_gain_value=1,
-        output_gain_map=False,
     ):
         self.gain_mode = gain_mode
         if self.gain_mode is AgipdGainMode.ADAPTIVE_GAIN:
@@ -48,7 +47,6 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             self.default_gain = cupy.uint8(gain_mode - 1)
         self.input_shape = (memory_cells, 2, pixels_x, pixels_y)
         self.processed_shape = (memory_cells, pixels_x, pixels_y)
-        self.output_gain_map = output_gain_map
         super().__init__(
             pixels_x,
             pixels_y,
@@ -57,12 +55,10 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             input_data_dtype,
             output_data_dtype,
         )
-        if self.output_gain_map:
-            self.gain_map_gpu = cupy.empty(self.processed_shape, dtype=cupy.uint8)
-            self.preview_gain = np.empty(self.preview_shape, dtype=np.float32)
-        else:
-            # TODO: don't even set in this case
-            self.gain_map_gpu = cupy.empty(0, dtype=cupy.uint8)
+        self.gain_map_gpu = cupy.empty(self.processed_shape, dtype=cupy.float32)
+        self.preview_buffer_getters.extend(
+            [self._get_raw_gain_for_preview, self._get_gain_map_for_preview]
+        )
 
         self.map_shape = (self.constant_memory_cells, self.pixels_x, self.pixels_y)
         self.gm_map_shape = self.map_shape + (3,)  # for gain-mapped constants
@@ -82,39 +78,19 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
 
         self.update_block_size((1, 1, 64))
 
-    def compute_preview_gain(self, preview_index):
-        assert self.output_gain_map
-        # TODO: abstract most of this in base_gpu to DRY
-        if preview_index < -4:
-            raise ValueError(f"No statistic with code {preview_index} defined")
-        elif preview_index >= self.memory_cells:
-            raise ValueError(f"Memory cell index {preview_index} out of range")
-
-        if preview_index >= 0:
-            self.gain_map_gpu[preview_index].astype(np.float32).get(
-                out=self.preview_gain
-            )
-        elif preview_index == -1:
-            # TODO: confirm that max is pixel and not integrated intensity
-            # separate from next case because dtype not applicable here
-            cupy.max(self.gain_map_gpu, axis=0).astype(cupy.float32).get(
-                out=self.preview_gain
-            )
-        elif preview_index in (-2, -3, -4):
-            stat_fun = {-1: cupy.max, -2: cupy.mean, -3: cupy.sum, -4: cupy.std}[
-                preview_index
-            ]
-            stat_fun(self.gain_map_gpu, axis=0, dtype=cupy.float32).get(
-                out=self.preview_gain
-            )
-        return self.preview_gain
-
-    def _preview_preprocess_raw(self):
+    def _get_raw_for_preview(self):
         return self.input_data_gpu[:, 0]
 
-    def _preview_preprocess_corr(self):
+    def _get_corrected_for_preview(self):
         return self.processed_data_gpu
 
+    # special to AGIPD
+    def _get_raw_gain_for_preview(self):
+        return self.input_data_gpu[:, 1]
+
+    def _get_gain_map_for_preview(self):
+        return self.gain_map_gpu
+
     def load_thresholds(self, threshold_map):
         # shape: y, x, memory cell, thresholds and gain values
         # note: the gain values are something like means used to derive thresholds
@@ -277,15 +253,6 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             ),
         )
 
-    def get_gain_map(self, output_order, out=None):
-        assert self.output_gain_map
-        return cupy.ascontiguousarray(
-            cupy.transpose(
-                self.gain_map_gpu,
-                utils.transpose_order(self._corrected_axis_order, output_order),
-            )
-        ).get(out=out)
-
     def _init_kernels(self):
         kernel_source = self._kernel_template.render(
             {
@@ -296,7 +263,6 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
                 "input_data_dtype": utils.np_dtype_to_c_type(self.input_data_dtype),
                 "output_data_dtype": utils.np_dtype_to_c_type(self.output_data_dtype),
                 "corr_enum": utils.enum_to_c_template(CorrectionFlags),
-                "output_gain_map": self.output_gain_map,
             }
         )
         self.source_module = cupy.RawModule(code=kernel_source)
diff --git a/src/calng/agipd_gpu_kernels.cpp b/src/calng/agipd_gpu_kernels.cpp
index 0561e84bc2b4f8efd44c4ec7dce475f972eab991..d5d5ca02394633510b5d394806b7ea8a961f6113 100644
--- a/src/calng/agipd_gpu_kernels.cpp
+++ b/src/calng/agipd_gpu_kernels.cpp
@@ -23,7 +23,7 @@ extern "C" {
 							const float g_gain_value,
 							const unsigned int* bad_pixel_map,
 							const float bad_pixel_mask_value,
-							unsigned char* gain_map,
+							float* gain_map, // TODO: more compact yet plottable representation
 							{{output_data_dtype}}* output) {
 		const size_t X = {{pixels_x}};
 		const size_t Y = {{pixels_y}};
@@ -99,6 +99,7 @@ extern "C" {
 					gain = 2;
 				}
 			}
+			gain_map[output_index] = (float)gain;
 
 			const size_t map_index = map_cell * map_stride_cell +
 				y * map_stride_y +
@@ -111,6 +112,7 @@ extern "C" {
 
 			if ((corr_flags & BPMASK) && bad_pixel_map[gm_map_index]) {
 				corrected = bad_pixel_mask_value;
+				gain_map[output_index] = bad_pixel_mask_value;
 			} else {
 				if (corr_flags & OFFSET) {
 					corrected -= offset_map[gm_map_index];
@@ -132,10 +134,6 @@ extern "C" {
 			{% else %}
 			output[output_index] = ({{output_data_dtype}})corrected;
 			{% endif %}
-
-			{% if output_gain_map %}
-			gain_map[output_index] = gain;
-			{% endif %}
 		} else {
 			// TODO: decide what to do when we cannot threshold
 			{% if output_data_dtype == "half" %}
@@ -144,9 +142,7 @@ extern "C" {
 			output[data_index] = ({{output_data_dtype}})corrected;
 			{% endif %}
 
-			{% if output_gain_map %}
 			gain_map[data_index] = 255;
-			{% endif %}
 		}
 	}
 }
diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index c8a64eb44dea00bb86a13d53e657a2fe70f0331e..49279fde4806810c1ab059a7c7eb4aba5f3a4f36 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -40,6 +40,18 @@ from ._version import version as deviceVersion
 
 PROCESSING_STATE_TIMEOUT = 10
 
+preview_schema = Schema()
+(
+    NODE_ELEMENT(preview_schema).key("image").commit(),
+    NDARRAY_ELEMENT(preview_schema).key("image.data").dtype("FLOAT").commit(),
+    UINT64_ELEMENT(preview_schema)
+    .key("image.trainId")
+    .displayedName("Train ID")
+    .assignmentOptional()
+    .defaultValue(0)
+    .commit(),
+)
+
 
 @KARABO_CLASSINFO("BaseCorrection", deviceVersion)
 class BaseCorrection(PythonDevice):
@@ -224,8 +236,8 @@ class BaseCorrection(PythonDevice):
             .metricPrefix(MetricPrefix.GIGA)
             .description(
                 "Corrected trains are written to shared memory locations. These are "
-                "pre-allocated and re-used (circular buffer). This parameter determines "
-                "much memory to set aside for the buffer."
+                "pre-allocated and re-used (circular buffer). This parameter "
+                "determines how much memory to set aside for the buffer."
             )
             .assignmentOptional()
             .defaultValue(10)
@@ -310,10 +322,10 @@ class BaseCorrection(PythonDevice):
             .key("dataFormat.outputAxisOrder")
             .displayedName("Output axis order")
             .description(
-                "Axes of main data output can be reordered after correction. Axis order "
-                "is specified as string consisting of 'x', 'y', and 'c', with the "
-                "latter indicating the memory cell axis. The default value of 'cxy' "
-                "puts pixels along the fast axes."
+                "Axes of main data output can be reordered after correction. Axis "
+                "order is specified as string consisting of 'x', 'y', and 'c', with "
+                "the latter indicating the memory cell axis. The default value of "
+                "'cxy' puts pixels on the fast axes."
             )
             .assignmentOptional()
             .defaultValue("cxy")
@@ -349,17 +361,6 @@ class BaseCorrection(PythonDevice):
             .commit()
         )
 
-        preview_schema = Schema()
-        (
-            NODE_ELEMENT(preview_schema).key("image").commit(),
-            NDARRAY_ELEMENT(preview_schema).key("image.data").dtype("FLOAT").commit(),
-            UINT64_ELEMENT(preview_schema)
-            .key("image.trainId")
-            .displayedName("Train ID")
-            .assignmentOptional()
-            .defaultValue(0)
-            .commit(),
-        )
         (
             NODE_ELEMENT(expected).key("preview").displayedName("Preview").commit(),
             OUTPUT_CHANNEL(expected)
@@ -377,7 +378,6 @@ class BaseCorrection(PythonDevice):
             .defaultValue(True)
             .reconfigurable()
             .commit(),
-            # TODO: Split into AGIPD-specific or see if others like cell ID over pulse ID
             INT32_ELEMENT(expected)
             .key("preview.index")
             .displayedName("Index (or stat) for preview")
@@ -518,7 +518,7 @@ class BaseCorrection(PythonDevice):
 
         if parse_version(karaboVersion) >= parse_version("2.11"):
             # TODO: the CalCatFriend could add these for us
-            # note: overly complicated slot function creation necessary for closure to work
+            # note: overly complicated for closure to work
             def make_wrapper_capturing_constant(constant):
                 def aux():
                     self.calcat_friend.get_specific_constant_version_and_call_me_back(
@@ -672,7 +672,7 @@ class BaseCorrection(PythonDevice):
         timestamp = Timestamp(Epochstamp(), Trainstamp(train_id))
         metadata = ChannelMetaData(source, timestamp)
         for channel_name, data in channel_data_pairs:
-            preview_hash.set("data.adc", data[..., np.newaxis])
+            preview_hash.set("image.data", data)
             channel = self.signalSlotable.getOutputChannel(channel_name)
             channel.write(preview_hash, metadata, False)
             channel.update()
@@ -809,7 +809,7 @@ class BaseCorrection(PythonDevice):
                 )
                 self.set("dataFormat.memoryCells", image_data.shape[0])
                 with self._buffer_lock:
-                    # TODO: re-validate frame filter against new number of cells
+                    self._update_frame_filter()
                     self._update_buffers()
 
             # DataAggregator typically tells us the wrong axis order
diff --git a/src/calng/base_gpu.py b/src/calng/base_gpu.py
index 4850dcbaa8c4108369daffc85719bd4184bd32ee..5a4821a0f954166587b5a1543efbf94570f5eb64 100644
--- a/src/calng/base_gpu.py
+++ b/src/calng/base_gpu.py
@@ -69,20 +69,24 @@ class BaseGpuRunner:
             self.processed_shape, dtype=output_data_dtype
         )
         self.reshaped_data_gpu = None  # currently not reusing buffer
-        self.preview_raw = np.empty(self.preview_shape, dtype=np.float32)
-        self.preview_corrected = np.empty(self.preview_shape, dtype=np.float32)
+
+        # default preview layers: raw and corrected (subclass can extend)
+        self.preview_buffer_getters = [
+            self._get_raw_for_preview,
+            self._get_corrected_for_preview,
+        ]
 
     # to get data from respective buffers to cell, x, y shape for preview computation
-    def _preview_preprocess_raw(self):
-        """Should return view of self.input_data_gpu with shape (cell, x, y)"""
+    def _get_raw_for_preview(self):
+        """Should return view of self.input_data_gpu with shape (cell, x/y, x/y)"""
         raise NotImplementedError()
 
-    def _preview_preprocess_corr(self):
-        """Should return view of self.processed_data_gpu with shape (cell, x, y)"""
+    def _get_corrected_for_preview(self):
+        """Should return view of self.processed_data_gpu with shape (cell, x/y, x/y)"""
         raise NotImplementedError()
 
     def flush_buffers(self):
-        """Optional reset GPU buffers (implement in appropriate subclasses"""
+        """Optional reset GPU buffers (implement in subclasses which need this)"""
         pass
 
     def correct(self, flags):
@@ -127,7 +131,7 @@ class BaseGpuRunner:
     def load_cell_table(self, cell_table):
         self.cell_table_gpu.set(cell_table)
 
-    def compute_preview(self, preview_index):
+    def compute_previews(self, preview_index):
         """Generate single slice or reduction preview of raw and corrected data
 
         Special values of preview_index are -1 for max, -2 for mean, -3 for sum, and
@@ -147,23 +151,29 @@ class BaseGpuRunner:
             raise ValueError(f"Memory cell index {preview_index} out of range")
 
         # TODO: enum around reduction type
-        for (preprocces, output_buffer) in (
-            (self._preview_preprocess_raw, self.preview_raw),
-            (self._preview_preprocess_corr, self.preview_corrected),
-        ):
-            image_data = preprocces()
-            if preview_index >= 0:
-                image_data[preview_index].astype(np.float32).get(out=output_buffer)
-            elif preview_index == -1:
-                # TODO: confirm that max is pixel and not integrated intensity
-                # separate from next case because dtype not applicable here
-                cupy.max(image_data, axis=0).astype(cupy.float32).get(out=output_buffer)
-            elif preview_index in (-2, -3, -4):
-                stat_fun = {-1: cupy.max, -2: cupy.mean, -3: cupy.sum, -4: cupy.std}[
-                    preview_index
-                ]
-                stat_fun(image_data, axis=0, dtype=cupy.float32).get(out=output_buffer)
-        return self.preview_raw, self.preview_corrected
+        return tuple(
+            self._compute_a_preview(image_data=getter(), preview_index=preview_index)
+            for getter in self.preview_buffer_getters
+        )
+
+    def _compute_a_preview(self, image_data, preview_index):
+        """image_data must have cells on first axis; X and Y order is not important
+        here for now (and can differ between AGIPD and DSSC)"""
+        if preview_index >= 0:
+            # TODO: reuse pinned buffers for this
+            return image_data[preview_index].astype(np.float32).get()
+        elif preview_index == -1:
+            # TODO: confirm that max is pixel and not integrated intensity
+            # separate from next case because dtype not applicable here
+            return cupy.nanmax(image_data, axis=0).astype(cupy.float32).get()
+        elif preview_index in (-2, -3, -4):
+            stat_fun = {
+                -1: cupy.nanmax,
+                -2: cupy.nanmean,
+                -3: cupy.nansum,
+                -4: cupy.nanstd,
+            }[preview_index]
+            return stat_fun(image_data, axis=0, dtype=cupy.float32).get()
 
     def update_block_size(self, full_block, target_shape=None):
         """Compute grid such that thread block grid covers target shape
diff --git a/src/calng/dssc_gpu.py b/src/calng/dssc_gpu.py
index 2b4bac53f025f8bcbe3dde08355a35294319713d..138e301647f3ed138422eeadc42cfc4438a27d6d 100644
--- a/src/calng/dssc_gpu.py
+++ b/src/calng/dssc_gpu.py
@@ -44,11 +44,11 @@ class DsscGpuRunner(base_gpu.BaseGpuRunner):
 
         self.update_block_size((1, 1, 64))
 
-    def _preview_preprocess_raw(self):
-        return cupy.transpose(self.input_data_gpu, (0, 2, 1))
+    def _get_raw_for_preview(self):
+        return self.input_data_gpu
 
-    def _preview_preprocess_corr(self):
-        return cupy.transpose(self.processed_data_gpu, (0, 2, 1))
+    def _get_corrected_for_preview(self):
+        return self.processed_data_gpu
 
     def load_offset_map(self, offset_map):
         # can have an extra dimension for some reason