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
90 lines
3.0 KiB
C++
90 lines
3.0 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "CBFByteOffset.h"
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#if defined(__SSE2__) || defined(_M_X64) || defined(_M_AMD64)
|
|
# include <emmintrin.h>
|
|
# define MINICBF_STREAM_STORE 1
|
|
#endif
|
|
|
|
#include "../common/JFJochException.h"
|
|
|
|
namespace {
|
|
|
|
int16_t ReadI16(const uint8_t *p) { int16_t v; std::memcpy(&v, p, 2); return v; }
|
|
int32_t ReadI32(const uint8_t *p) { int32_t v; std::memcpy(&v, p, 4); return v; }
|
|
int64_t ReadI64(const uint8_t *p) { int64_t v; std::memcpy(&v, p, 8); return v; }
|
|
|
|
// Nothing on this side reads the decoded image back: it goes to the GPU by DMA, or a CPU engine
|
|
// walks it once and drops it. An ordinary store therefore pays twice over - it reads every cache
|
|
// line before overwriting it, and it evicts 72 MB of live cache to make room - so the pixels go
|
|
// straight past the cache. Measured on an 18 Mpx frame: 27 ms of ordinary stores against 11 ms of
|
|
// these. The bytes written are the same either way; the fence is what makes them visible to the
|
|
// copy that follows.
|
|
void StorePixel(int32_t *out, int32_t value) {
|
|
#ifdef MINICBF_STREAM_STORE
|
|
_mm_stream_si32(out, value);
|
|
#else
|
|
*out = value;
|
|
#endif
|
|
}
|
|
|
|
void StoreFence() {
|
|
#ifdef MINICBF_STREAM_STORE
|
|
_mm_sfence();
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// The x-CBF_BYTE_OFFSET scheme: a running value, each pixel stored as a delta in the smallest
|
|
// container that holds it, escaping to the next size with that container's most negative value.
|
|
// Following Bernstein & Hammersley (2006) Int. Tables Cryst. G, 37-43
|
|
void DecodeCBFByteOffset(const uint8_t *data, size_t size, int32_t *out, size_t n_pixels) {
|
|
int64_t value = 0;
|
|
size_t pos = 0;
|
|
size_t written = 0;
|
|
|
|
while (written < n_pixels) {
|
|
if (pos >= size)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"miniCBF byte-offset stream ended after " + std::to_string(written)
|
|
+ " of " + std::to_string(n_pixels) + " pixels");
|
|
|
|
int64_t delta = static_cast<int8_t>(data[pos]);
|
|
pos += 1;
|
|
|
|
if (delta == -128) {
|
|
if (pos + 2 > size) break;
|
|
delta = ReadI16(data + pos);
|
|
pos += 2;
|
|
if (delta == -32768) {
|
|
if (pos + 4 > size) break;
|
|
delta = ReadI32(data + pos);
|
|
pos += 4;
|
|
if (delta == INT32_MIN) {
|
|
if (pos + 8 > size) break;
|
|
delta = ReadI64(data + pos);
|
|
pos += 8;
|
|
}
|
|
}
|
|
}
|
|
|
|
value += delta;
|
|
StorePixel(out + written, static_cast<int32_t>(value));
|
|
written++;
|
|
}
|
|
|
|
StoreFence();
|
|
|
|
if (written != n_pixels)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"miniCBF byte-offset stream ended after " + std::to_string(written)
|
|
+ " of " + std::to_string(n_pixels) + " pixels");
|
|
}
|
|
|