Files
Jungfraujoch/image_analysis/image_preprocessing/CBFDecoderGPU.h
T
leonarski_fandClaude Opus 5.5 f4252ad39d miniCBF frames are decoded on the device
The CBF reader decoded every frame's x-CBF_BYTE_OFFSET stream on the worker thread - 19-30 % of the
image-loop worker CPU on CBF sets (7qij, 4mtk) - and the 4-byte image was then uploaded. ReadRawImage
now hands over the stream as the file stores it, in the image's own buffer, as a new read-only
CompressionAlgorithm::CBF_BYTE_OFFSET (mode Int32, what it decodes to), and ImagePreprocessorGPU
decodes it on the card into the same int32 image and runs the same preprocessing kernel on it.

The device decode (CBFDecoderGPU) cuts the stream into 256-byte tiles. A token is 1, 3, 7 or 15 bytes,
so a tile can be entered at 15 offsets; for each one thread works out where the decode leaves the
tile, how many pixels it emitted and their sum modulo 2^32 (the walks from different entries meet
within a few bytes, after which they are one walk). An exclusive scan composing these maps gives each
tile its true entry, first pixel and running value, and a second walk writes the pixels. A stream
that ends before the image is refused as on the host.

The host decoder moves to compression/ (DecodeCBFByteOffset) and is reached through
JFJochDecompressPtr, so every host consumer - the CPU build, the azimuthal-integration worker, the
pre-scan's shadow and gain passes - decodes exactly as before, now from the stream. It is the CPU
path, not a fallback: a frame the device refuses is not retried on the host. A process file written
from CBF input stores the decoded int32 image, as it always did. .cbf.gz is still inflated on the host.

Tests: CBFDecoderGPU_MatchesHost (every token size, 0x80 inside payloads, escape runs across tiles,
trailing CIF text), CBFDecoderGPU_ShortStreamIsRefused, ImagePreprocessorGPU_StoredEncodingsMatchCPU.
Checked locally against the host decoder on 36 real frames of 9 open-arm CBF sets (signed 32-bit,
signed and unsigned 16-bit, plain and gzip): identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:37:37 +02:00

68 lines
3.0 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include "../../common/CompressedImage.h"
#include "../indexing/CUDAMemHelpers.h"
// What one tile of the byte-offset stream does to a decode that enters it at each of the 15 byte
// positions a token can straddle the tile boundary by: where the decode leaves the tile (the offset
// into the next one), how many pixels it emitted, and what they added to the running value. Entry and
// exit 15 is "the stream has ended", which every later tile passes on.
struct CBFTileMap {
static constexpr int DEAD = 15;
uint32_t count[16];
uint32_t sum[16]; // modulo 2^32, which is all an int32 pixel keeps of the int64 running value
uint8_t exit[16];
};
// x-CBF_BYTE_OFFSET decoded ON THE DEVICE, exactly as DecodeCBFByteOffset does on the host: the same
// int32 pixels, and the same refusal of a stream that ends before the image does.
//
// The format is a running sum of deltas whose tokens are 1, 3, 7 or 15 bytes long, so where a token
// starts depends on every token before it. The stream is cut into tiles; for each tile and each of the
// 15 offsets a decode can enter it at, one thread works out where it leaves, how many pixels it emits
// and what they sum to (the walks from different offsets meet within a few bytes, after which they
// are the same walk). Composing those maps across tiles is a scan, which gives every tile its true
// entry offset, its first pixel and the running value there; a second walk of each tile then writes
// the pixels.
class CBFDecoderGPU {
std::shared_ptr<CudaStream> stream;
// The reader's buffer is pageable; copied here first, the upload is one DMA (see BSLZ4DecoderGPU).
CudaHostPtr<uint8_t> host_compressed;
CudaDevicePtr<uint8_t> gpu_compressed;
size_t compressed_capacity = 0;
CudaDevicePtr<CBFTileMap> gpu_maps;
CudaDevicePtr<CBFTileMap> gpu_entries; // exclusive scan of gpu_maps: the state each tile is entered in
CudaDevicePtr<uint8_t> gpu_scan_temp;
size_t scan_temp_bytes = 0;
size_t tile_capacity = 0;
CudaDevicePtr<uint32_t> gpu_total; // pixels the whole stream decodes to
CudaHostPtr<uint32_t> host_total;
size_t last_npixels = 0;
CudaEvent decode_start;
CudaEvent decode_stop;
public:
explicit CBFDecoderGPU(std::shared_ptr<CudaStream> stream);
~CBFDecoderGPU() { if (stream) cudaStreamSynchronize(*stream); }
// Upload the stream and queue its decode into gpu_out (image width x height int32 pixels) on the
// decoder's stream. The caller synchronises, then calls ThrowIfDecodeFailed().
void Decode(const CompressedImage &image, int32_t *gpu_out);
// Report a stream that ended before the image did. MUST be called after the caller has
// synchronised the stream.
void ThrowIfDecodeFailed() const;
// Device time of the last upload and decode, in seconds. Valid after the caller synchronises.
[[nodiscard]] float GetDecodeTime_s() const;
};