Compress() and FrameTransformation::CompressImage() returned int64_t with a
negative value meaning "did not fit". That is a footgun: the negative result
silently converts to a huge size_t if a caller forgets to check it. Return
size_t and instead throw a named CompressionBufferTooSmallException (deriving
from JFJochException, Compression category) when the output would not fit the
destination buffer.
The receiver catches it explicitly and drops just that frame, as before; the
offline/GetCompressedImage path uses a worst-case buffer so it never throws.
Add a test that a too-small destination throws and a worst-case buffer does not.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the fixed-element DefaultBlockSize with a byte target divided by
elem_size to get the block element count, so the per-block working set (and
thus cache behaviour) stays constant across pixel bit depths instead of halving
from 8- to 16- to 32-bit. The target is per-algorithm, following the measured
sweet spots on sparse data: LZ4 wants a small, cache-resident block for
throughput (16 kB), ZSTD/RLE want a large block for ratio (128 kB). The gap is
widest on extreme-sparsity inputs such as the uint32 pixel_mask, where
large-block ZSTD reaches 100-1800x vs ~160x for LZ4.
The block size is read back per-dataset from the bitshuffle stream header
(block_size = header_bytes / elem_size) and the HDF5 filter params, so the
decompressor and external readers (XDS/Neggia/Durin/CrystFEL) need no change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On sparse lyso frames the larger block improves compression ratio across all
bshuf algorithms (16-bit data): ZSTD 8.58 -> 9.30, LZ4 7.38 -> 7.58, RLE
6.82 -> 6.90. 16384 captures most of the gain available from even larger
blocks (ZSTD tops out ~9.55 at 65536) while staying close to the cache sweet
spot: the cheap codecs (LZ4, RLE) peak in throughput once a block's working
set fits L1d (~4096 elem here), so very large blocks trade real throughput for
diminishing ratio - and that penalty is worse on the Xeon Gold/Platinum
production hosts (smaller private L2, shared-L3 contention under many parallel
compression threads).
The block size is stored per-dataset in the bitshuffle HDF5 filter params, so
existing readers (XDS/Neggia/Durin/CrystFEL) stay compatible.
Move the per-block bitshuffle scratch off the inline member array onto a
lazily-sized heap vector, like tmp_space, so the block size no longer bloats
every stack-allocated compressor (incl. the transient ones in
CBORStream2Serializer).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JFJochBitShuffleCompressor::Compress now takes a dest_size and returns a
negative value when the compressed output would not fit, instead of writing
past the destination buffer. The check is lazy: before each block it verifies
the remaining space still covers that block's worst case (mirrored by the new
MaxCompressedBlockSize helper, consistent with MaxCompressedSize so a dest
sized to MaxCompressedSize never fails). On overflow the dest content is
undefined - no rescue.
The receiver uses this to compress directly into the writer buffer slot and
drop just the oversized frame instead of pre-reserving the full worst-case
image size next to the per-image CBOR metadata.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>