PinInputBuffer() page-locks the buffer an image is decompressed into, which an uncompressed image never uses: it is read straight out of the reader's own buffer, so the upload came from pageable memory. Measured on this card, 72.6 MB crosses at 5.6 GB/s pageable and 14.6 GB/s registered - 12.9 ms against 5.0 ms, on every image of a sweep that stores its frames uncompressed. PinInputRegion() page-locks a region the caller owns and remembers it, so a worker that reads every frame into the same buffer registers it once. The registration is dropped with the engine, so the three workers that build one declare their raw image before it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
60 lines
3.1 KiB
C++
60 lines
3.1 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
#include <vector>
|
|
#include "../common/CompressedImage.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../common/PixelMask.h"
|
|
#include "ImagePreprocessorBuffer.h"
|
|
|
|
struct alignas(8) ImageStatistics {
|
|
unsigned long long error_pixel_count = 0;
|
|
unsigned long long saturated_pixel_count = 0;
|
|
unsigned long long masked_pixel_count = 0;
|
|
long long max_value = INT64_MIN;
|
|
long long min_value = INT64_MAX;
|
|
};
|
|
|
|
class ImagePreprocessor {
|
|
protected:
|
|
const size_t npixels;
|
|
const DiffractionExperiment &experiment;
|
|
const int64_t saturation_limit;
|
|
public:
|
|
ImagePreprocessor(const DiffractionExperiment &experiment);
|
|
virtual ~ImagePreprocessor() = default;
|
|
virtual ImageStatistics Analyze(ImagePreprocessorBuffer &processed_image, const uint8_t *decompressed_image, CompressedImageMode image_mode) = 0;
|
|
|
|
// Analyze straight from the COMPRESSED image, decompressing wherever the implementation prefers.
|
|
// The GPU implementation uploads the compressed chunk and decodes it on the device, so only a few
|
|
// MB cross PCIe instead of the whole frame and the host never decompresses at all.
|
|
// Returns false when this implementation cannot handle the image - the CPU preprocessor always,
|
|
// and the GPU one for any algorithm without a device decoder - and the caller then decompresses
|
|
// on the host and calls Analyze() as before. Keeping the fallback explicit means a format we
|
|
// cannot decode on the device is a slower path, never a wrong answer.
|
|
virtual bool AnalyzeCompressed(ImagePreprocessorBuffer &processed_image, const CompressedImage &image,
|
|
ImageStatistics &stats) { return false; }
|
|
|
|
// Device time the last AnalyzeCompressed() spent getting the chunk across and decompressing it,
|
|
// so the caller can still report a decompression cost once the host no longer does the work.
|
|
// Meaningless unless the previous call returned true.
|
|
[[nodiscard]] virtual float GetLastDecompressionTime_s() const { return 0.0f; }
|
|
|
|
// Resize the buffer an image will be decompressed into and page-lock it, so that the host->device
|
|
// copy of Analyze() is a real DMA. Without page-locking the driver stages the copy through its own
|
|
// pinned pool, which is a host-side copy on the calling thread: it does not overlap and it degrades
|
|
// badly with the number of workers. Nothing to do on the CPU.
|
|
virtual void PinInputBuffer(std::vector<uint8_t> &buffer, size_t size) {}
|
|
|
|
// The same for a region the caller already owns. An uncompressed image is never decompressed
|
|
// into a buffer of ours - the upload reads the reader's own bytes - so that is what has to be
|
|
// page-locked. The region is remembered, so a worker handing over the same buffer on every frame
|
|
// registers it once. The registration is dropped when this object is destroyed, which is why an
|
|
// engine must not outlive the buffer it was given.
|
|
virtual void PinInputRegion(const void *ptr, size_t bytes) {}
|
|
};
|