structured_zstd/encoding/match_generator/mod.rs
1//! Matching algorithm used find repeated parts in the original data
2//!
3//! The Zstd format relies on finden repeated sequences of data and compressing these sequences as instructions to the decoder.
4//! A sequence basically tells the decoder "Go back X bytes and copy Y bytes to the end of your decode buffer".
5//!
6//! The task here is to efficiently find matches in the already encoded data for the current suffix of the not yet encoded data.
7
8use alloc::vec::Vec;
9// SIMD/CRC intrinsics now live in `crate::encoding::fastpath::*` where they
10// sit under per-CPU `#[target_feature]` umbrellas; no architecture-specific
11// intrinsic imports remain in this file.
12use super::CompressionLevel;
13use super::Matcher;
14use super::Sequence;
15use super::cost_model::HC_FORMAT_MINMATCH;
16#[cfg(test)]
17use super::cost_model::HC_MAX_LIT;
18#[cfg(test)]
19use super::cost_model::{
20 HC_BITCOST_MULTIPLIER, HC_OPT_NUM, HC_PREDEF_THRESHOLD, HcOptState, HcOptimalCostProfile,
21};
22#[cfg(test)]
23use super::cost_model::{HC_BLOCKSIZE_MAX, HC_MAX_LL, HC_MAX_ML, HC_MAX_OFF, HcOptPriceType};
24use super::dfast::DfastMatchGenerator;
25#[cfg(test)]
26use super::hc::HC_MIN_MATCH_LEN;
27#[cfg(test)]
28use super::match_table::storage::HC3_HASH_LOG;
29// FAST_HASH_FILL_STEP test-only re-export was tied to the legacy
30// SuffixStore MatchGenerator's interleaved hash-fill stride. The
31// upstream zstd-shape Fast kernel walks ip0 with kSearchStrength step-skip
32// acceleration instead, so the constant has no consumer in the
33// remaining live test set today.
34#[cfg(test)]
35use super::match_table::helpers::INCOMPRESSIBLE_SKIP_STEP;
36use super::match_table::helpers::MIN_MATCH_LEN;
37#[cfg(test)]
38use super::match_table::helpers::common_prefix_len;
39#[cfg(test)]
40use super::opt::ldm::HcRawSeq;
41#[cfg(test)]
42use super::opt::types::{HcCandidateQuery, MatchCandidate};
43use super::row::RowMatchGenerator;
44use super::simple::fast_matcher::{FAST_LEVEL_1_HASH_LOG, FAST_LEVEL_1_MLS, FastKernelMatcher};
45
46pub(crate) const DFAST_MIN_MATCH_LEN: usize = 5;
47// Bytes the dfast short hash reads (upstream zstd `mls = 5`). Seeding / lookahead
48// guards use it so a position is only short-hashed once its full 5-byte key
49// is in range.
50pub(crate) const DFAST_SHORT_HASH_LOOKAHEAD: usize = 5;
51pub(crate) const ROW_MIN_MATCH_LEN: usize = 5;
52// Upstream zstd `clevels.h:31` at level 3 large-input bucket sets
53// `hashLog = 17` (the long-hash table) and `chainLog = 16` (the
54// short-hash table — upstream zstd names this `chainTable` even though for
55// dfast it's used as a plain single-slot hash). Each table holds one
56// `U32` per slot; the upstream zstd overwrites on collision and recovers
57// compression quality via the inline `_search_next_long` retry
58// (after a short-hash hit, probes `hashLong[hl1]` at `ip + 1` and
59// keeps the longer match).
60//
61// We mirror that storage layout: single `u32` per bucket (no
62// `[u32; N]` array), `long_hash` sized `1 << DFAST_HASH_BITS` and
63// `short_hash` one bit smaller via `DFAST_SHORT_HASH_BITS_DELTA`.
64// Two-table footprint at Level 3: `2^17 × 4 + 2^16 × 4 = 768 KiB`,
65// exact upstream parity. The `_search_next_long` retry lives in
66// `DfastMatchGenerator::hash_candidate` (called via
67// `best_match`). Earlier revisions kept a
68// 4-slot bucket per hash position; that paid 4× the upstream zstd memory
69// without measurable ratio gain once the retry was in place.
70//
71// `dfast_hash_bits_for_window` still clamps the runtime long-hash
72// value to `[MIN_WINDOW_LOG, DFAST_HASH_BITS]`, so this const is the
73// upper bound rather than a fixed default.
74pub(crate) const DFAST_HASH_BITS: usize = 17;
75/// Difference between `long_hash_bits` and `short_hash_bits` —
76/// upstream zstd `hashLog - chainLog` is 1 at every dfast level (`clevels.h`
77/// level 2: 16-15=1; level 3: 17-16=1). The short hash is one bit
78/// smaller than the long hash so the per-bucket footprint matches
79/// upstream zstd sizing exactly.
80pub(crate) const DFAST_SHORT_HASH_BITS_DELTA: usize = 1;
81/// Sentinel value for an empty slot in the dfast hash tables. Real
82/// positions are stored as `(abs_pos - position_base + 1) as u32`, so
83/// `0` is reserved as the "empty" marker and a true relative offset
84/// of `0` never appears in the table. Mirrors the LDM table's
85/// `LdmEntry.offset == 0` convention (see `encoding/ldm/table.rs`)
86/// so both rebasing structures share
87/// one sentinel scheme.
88pub(crate) const DFAST_EMPTY_SLOT: u32 = 0;
89
90/// Guard band reserved above the high-water mark before triggering a
91/// rebase on the Dfast hash tables. When the next insert would push a
92/// relative offset above `u32::MAX - DFAST_REBASE_GUARD_BAND`, the
93/// table calls `reduce(GUARD_BAND)` to shift every slot down and
94/// advance `position_base` so future inserts stay inside the `u32`
95/// window. Same scheme as `encoding/ldm/table.rs`.
96pub(crate) const DFAST_REBASE_GUARD_BAND: u32 = 1u32 << 30;
97// `kSearchStrength` (upstream `zstd_compress_internal.h:32`). The dfast step
98// ramp grows one position every `1 << kSearchStrength` = 256 bytes travelled
99// (upstream `kStepIncr`, zstd_double_fast.c:131). A smaller value accelerates
100// the scan faster and skips source positions upstream still inserts, which
101// drops the short matches upstream finds at a block start — so the
102// `#167`-disabled path must use the upstream 8 to stay byte-identical.
103pub(crate) const DFAST_SKIP_SEARCH_STRENGTH: usize = 8;
104pub(crate) const DFAST_SKIP_STEP_GROWTH_INTERVAL: usize = 1 << DFAST_SKIP_SEARCH_STRENGTH;
105/// How densely dfast indexes a block it wrote off without searching.
106///
107/// The block is not searched, so the only reason to index it at all is that a
108/// LATER block may duplicate it, and a duplicate is block-sized: the search
109/// that scans it meets an indexed position within one step, which is far inside
110/// what it scans anyway. Indexing at every sixteenth position instead cost a
111/// third of the encode on incompressible input — two tables, sixty-five
112/// thousand stores per mebibyte — for a proximity nothing needs. Fast reads the
113/// same reasoning from [`RAW_SKIP_INDEX_STEP`].
114pub(crate) const DFAST_INCOMPRESSIBLE_SKIP_STEP: usize =
115 crate::encoding::incompressible::RAW_SKIP_INDEX_STEP;
116pub(crate) const ROW_HASH_BITS: usize = 20;
117pub(crate) const ROW_LOG: usize = 5;
118pub(crate) const ROW_SEARCH_DEPTH: usize = 16;
119pub(crate) const ROW_TARGET_LEN: usize = 48;
120pub(crate) const ROW_TAG_BITS: usize = 8;
121pub(crate) const ROW_EMPTY_SLOT: u32 = u32::MAX;
122pub(crate) const ROW_HASH_KEY_LEN: usize = 4;
123// HC_PRIME3BYTES / HC_PRIME4BYTES moved to match_table::storage
124// alongside the hash helpers in Phase 1e Stage A. Only the test
125// module references the constants directly (production code goes
126// through `MatchTable::hash_value_with_mls`).
127#[cfg(test)]
128use super::match_table::storage::{HC_PRIME3BYTES, HC_PRIME4BYTES};
129
130// HC_HASH_LOG / HC_CHAIN_LOG / HC3_HASH_LOG / HC_EMPTY live on the
131// shared storage module so MatchTable methods can reference them
132// without pulling in this module. Re-imported here so existing
133// macros / configs / tests keep their unqualified names.
134#[cfg(test)]
135use super::match_table::storage::HC_EMPTY;
136// HC3_MAX_OFFSET moved to encoding::bt alongside the hash3 candidate
137// probe macro that consumes it; the macro references it via the
138// fully-qualified `$crate::encoding::bt::HC3_MAX_OFFSET` path so this
139// module no longer needs a local import.
140pub(crate) const HC_SEARCH_DEPTH: usize = 16;
141// HC_MIN_MATCH_LEN moved to encoding::hc; re-imported here so
142// existing references compile unchanged.
143pub(crate) const HC_OPT_MIN_MATCH_LEN: usize = HC_FORMAT_MINMATCH;
144pub(crate) const HC_TARGET_LEN: usize = 48;
145
146// MAX_HC_SEARCH_DEPTH moved to encoding::hc alongside chain_candidates.
147// Per-level tuning config (the config structs + `LEVEL_TABLE` + the
148// level→params resolution chain) lives in `levels::config`; the driver imports
149// that resolution API here.
150use super::levels::config::*;
151// The HashChain / BT match generator + its optimal-parse machinery lives in
152// `hc::generator`; the driver stores it in the `HashChain` storage variant.
153use super::hc::generator::HcMatchGenerator;
154
155// Dictionary prime + CDict-equivalent snapshot lifecycle. A child module so it
156// can reach the driver's private `primed` / `reset_shape` state directly; the
157// `Matcher` trait's dict entry points forward to its inherent `*_impl` helpers.
158mod dict_prime;
159
160// `Strategy` and `StrategyTag` live in `crate::encoding::strategy`.
161// The driver carries a `StrategyTag` field set at `reset()` and
162// dispatches each block into a monomorphised `compress_block::<S>`
163// per concrete strategy.
164
165/// Backend storage for [`MatchGeneratorDriver`]. Exactly one match-finder
166/// state lives in the driver at a time — the active variant. Backend
167/// transitions in [`Matcher::reset`] drain the current variant's allocations
168/// into the shared `vec_pool` and then replace `storage` with a freshly
169/// constructed variant for the new backend.
170///
171/// Replaces the prior pattern of four parallel fields (`match_generator`,
172/// `dfast_match_generator: Option<…>`, `row_match_generator: Option<…>`,
173/// `hc_match_generator: Option<…>`) + an `active_backend: BackendTag`
174/// discriminator: the parallel layout kept drained inner structures
175/// allocated across backend switches, and every per-frame/per-slice
176/// driver operation had to dispatch on `active_backend` to pick the
177/// right field. A single enum collapses the storage and makes the
178/// dispatcher pattern-match on the storage variant directly — same
179/// number of arms, but `storage.backend()` is now the canonical source
180/// of truth and dead variants are dropped when the active backend
181/// changes.
182#[derive(Clone)]
183enum MatcherStorage {
184 /// Upstream zstd `ZSTD_fast` family. Constructed by
185 /// [`MatchGeneratorDriver::new`] as the initial variant and
186 /// re-selected by [`Matcher::reset`] for any [`CompressionLevel`]
187 /// that `resolve_level_params` maps to [`StrategyTag::Fast`]
188 /// (`Uncompressed`, `Fastest`, `Level(1)`, and any non-positive
189 /// `Level(n)` not equal to `0`).
190 Simple(FastKernelMatcher),
191 /// Upstream zstd `ZSTD_dfast` family — two-table hash chain. Selected for
192 /// any level that resolves to [`StrategyTag::Dfast`] in
193 /// `resolve_level_params` (`Default`, `Level(0)`, `Level(2)`,
194 /// `Level(3)`).
195 Dfast(DfastMatchGenerator),
196 /// Upstream zstd `ZSTD_greedy` family with row hashing. Selected for any
197 /// level that resolves to [`StrategyTag::Greedy`] (currently
198 /// `Level(4)` only).
199 Row(RowMatchGenerator),
200 /// Upstream zstd `ZSTD_lazy2` and the BT-based optimal modes
201 /// (`btopt` / `btultra` / `btultra2`). Selected for any level that
202 /// resolves to [`StrategyTag::Lazy`], [`StrategyTag::BtOpt`],
203 /// [`StrategyTag::BtUltra`], or [`StrategyTag::BtUltra2`]
204 /// (`Better`, `Best`, `Level(5..=22)`, and any `Level(n)` with
205 /// `n > MAX_LEVEL` — `resolve_level_params` clamps positive
206 /// numeric levels at `MAX_LEVEL = 22` via
207 /// `Level(n).clamp(1, MAX_LEVEL)`, so `Level(23..=i32::MAX)` all
208 /// land on `BtUltra2` here). The [`HcMatchGenerator`]'s internal
209 /// [`HcBackend`] discriminator decides whether BT scratch is
210 /// allocated.
211 HashChain(HcMatchGenerator),
212}
213
214impl MatcherStorage {
215 /// Heap bytes the active backend variant holds (tables, history, scratch).
216 fn heap_size(&self) -> usize {
217 match self {
218 Self::Simple(m) => m.heap_size(),
219 Self::Dfast(m) => m.heap_size(),
220 Self::Row(m) => m.heap_size(),
221 Self::HashChain(m) => m.heap_size(),
222 }
223 }
224
225 /// Capacity of the buffer blocks are read into, for the backends that
226 /// ingest in place. A frame sized up front reserves this exactly; one that
227 /// grew into it lands on a doubling step instead, which is what makes the
228 /// difference observable to a test.
229 #[cfg(test)]
230 fn ingest_capacity(&self) -> usize {
231 match self {
232 Self::Simple(m) => m.history_capacity(),
233 Self::Dfast(m) => m.history.capacity(),
234 Self::Row(m) => m.history.capacity(),
235 Self::HashChain(m) => m.table.history.capacity(),
236 }
237 }
238
239 /// [`super::strategy::BackendTag`] family of the active variant.
240 fn backend(&self) -> super::strategy::BackendTag {
241 use super::strategy::BackendTag;
242 match self {
243 Self::Simple(_) => BackendTag::Simple,
244 Self::Dfast(_) => BackendTag::Dfast,
245 Self::Row(_) => BackendTag::Row,
246 Self::HashChain(_) => BackendTag::HashChain,
247 }
248 }
249}
250
251/// This is the default implementation of the `Matcher` trait. It allocates and reuses the buffers when possible.
252pub struct MatchGeneratorDriver {
253 vec_pool: Vec<Vec<u8>>,
254 /// Active match-finder state. Exactly one backend lives here at a
255 /// time; [`Matcher::reset`] drains the previous variant into
256 /// `vec_pool` before swapping in a freshly constructed variant for
257 /// the new backend. `storage.backend()` is the canonical source of
258 /// truth for the parse family; `strategy_tag` carries the
259 /// compile-time strategy chosen at the last `reset()`.
260 storage: MatcherStorage,
261 // Compile-time strategy tag resolved at `reset()` from the
262 // requested `CompressionLevel`'s `LevelParams`. The driver's
263 // hot-block dispatcher in `blocks/compressed.rs` matches on
264 // this tag to enter the corresponding `Strategy`
265 // monomorphisation (`compress_block::<S>`).
266 strategy_tag: super::strategy::StrategyTag,
267 // Decoupled search-method axis resolved at `reset()` from
268 // `LevelParams.search`. The per-block dispatcher routes on this
269 // (not on `strategy_tag`) so a level's parse and search backend can
270 // be chosen independently. The `BinaryTree` arm still consults
271 // `strategy_tag` to pick the opt `Strategy` ZST.
272 search: super::strategy::SearchMethod,
273 // Decoupled parse-mode axis resolved at `reset()` from
274 // `LevelParams::parse()`. Independent of `search`: greedy / lazy /
275 // lazy2 can run on any non-opt search backend. The backends still
276 // read their own `lazy_depth` (kept in sync at `reset()`); this is
277 // the authoritative parse selector for the dispatcher.
278 pub(crate) parse: super::strategy::ParseMode,
279 /// Test-only per-level recipe override applied in `reset()` before
280 /// backend selection. Lets the parse×search matrix be exercised
281 /// without editing `LEVEL_TABLE`; never compiled into production.
282 #[cfg(test)]
283 config_override: Option<(super::strategy::SearchMethod, super::strategy::ParseMode)>,
284 /// Fine-grained per-knob overrides from the public
285 /// [`super::parameters::CompressionParameters`] surface (#27).
286 /// `None` (or an all-`None` [`super::parameters::ParamOverrides`])
287 /// keeps the resolved level geometry byte-identical to plain
288 /// level-based compression. Applied in [`Matcher::reset`] after the
289 /// level params are resolved, before backend selection. Persists
290 /// across resets (it is frame configuration, not a one-shot) until
291 /// the caller changes it.
292 param_overrides: Option<super::parameters::ParamOverrides>,
293 slice_size: usize,
294 base_slice_size: usize,
295 // Frame header window size must stay at the configured live-window budget.
296 // Dictionary retention expands internal matcher capacity only.
297 reported_window_size: usize,
298 // Tracks currently retained bytes that originated from primed dictionary
299 // history and have not been evicted yet.
300 dictionary_retained_budget: usize,
301 // Source size hint for next frame (set via set_source_size_hint, cleared on reset).
302 source_size_hint: Option<u64>,
303 // Dictionary sizes for the next frame (set via set_dictionary_size_hint,
304 // consumed on reset): the serialized size keys the CDict cParams tier the
305 // frame runs (upstream `ZSTD_getCParamRowSize` on `dictSize`), the content
306 // size the dictionary tables and attach cutoffs.
307 dictionary_size_hint: Option<super::DictionarySizes>,
308 // Normalized `ceil_log2` bucket of the frame's source-size hint, captured at
309 // `reset` (where `source_size_hint` is consumed) via [`source_size_ceil_log`].
310 // `None` means the frame was unhinted. Drives `prime_with_dictionary`'s upstream zstd
311 // `ZSTD_shouldAttachDict` mode for the Simple/Fast backend: `None` (unknown)
312 // or `<= FAST_ATTACH_DICT_CUTOFF_LOG` → attach (separate dict table, 2-cursor
313 // `compress_block_fast_dict`); larger → copy (dictionary primed into the live
314 // table, 4-cursor `compress_block_fast`). The primed-snapshot key is the
315 // resolved shape ([`reset_shape`](Self::reset_shape)), not this bucket.
316 reset_size_log: Option<u8>,
317 // Whether the loaded dictionary fits the Fast attach path's tagged position
318 // field (`<= MAX_FAST_ATTACH_DICT_REGION`). Captured at `reset` from the
319 // dict-size hint (which equals the actual dict length on load) so the Fast
320 // attach decision, the attach-epoch reset bit, and the primed-snapshot
321 // `fast_attach` bit all gate on it consistently. `true` when there is no
322 // dictionary (the attach path is then unused). A dict too large to tag falls
323 // back to copy mode instead of overflowing the packed position.
324 reset_dict_attach_ok: bool,
325 // Hint-resolved matcher shape from the last `reset`: the [`LevelParams`], the
326 // active backend's applied Dfast/Row hash-table width (`0` for HC/Fast), the
327 // Fast attach-vs-copy mode, and the active LDM override (#27). Combined with
328 // the frame's level into the [`PrimedKey`] that keys the primed snapshot, so
329 // it is only restored into a reset that resolved the identical matcher AND
330 // LDM configuration. `None` before the first `reset`.
331 reset_shape: Option<(
332 LevelParams,
333 usize,
334 bool,
335 Option<super::parameters::LdmOverride>,
336 )>,
337 // One-shot borrowed block range `[start, end)` staged by the borrowed
338 // Fast frame path (`set_borrowed_block`) for the NEXT
339 // `start_matching` / `skip_matching_with_hint`. `Some` routes that
340 // call to the Simple backend's borrowed scan instead of the owned
341 // committed-block path; consumed (reset to `None`) by the routed
342 // call. Always `None` on the owned streaming path.
343 borrowed_pending: Option<(usize, usize)>,
344 /// CDict-equivalent: snapshot of the post-prime matcher state taken
345 /// once after the first dictionary prime — the backend `storage`
346 /// (hash tables + dictionary history + offset history + window) plus
347 /// the driver-level `dictionary_retained_budget`, the only two pieces
348 /// `prime_with_dictionary` writes. Subsequent frames restore this
349 /// (a table memcpy) instead of re-hashing every dictionary position,
350 /// mirroring upstream zstd `ZSTD_compressBegin_usingCDict` copying the
351 /// precomputed `cdict->matchState`. Invalidated when the dictionary
352 /// changes; keyed by the [`PrimedKey`] resolved matcher shape so a snapshot
353 /// is only restored into a reset that produces the same matcher — see
354 /// `restore_primed_dictionary`.
355 primed: Option<(MatcherStorage, usize, PrimedKey)>,
356}
357
358/// Identity of the matcher configuration a primed snapshot was captured under:
359/// the FULLY RESOLVED matcher shape, not the raw source-size hint.
360///
361/// `reset()` resolves the hint into a [`LevelParams`] (window_log cap, the
362/// HC/Fast table and search geometry, the parse depth/target-length that get
363/// baked into the restored `storage`) plus, for the Dfast/Row backends, a
364/// table-width derived from the hint's ceil-log bucket. The mapping from hint
365/// to resolved shape is many-to-one: the source-size adjustment is monotone in
366/// `ceil_log2(hint)`, and Level 22 additionally collapses several buckets onto
367/// one upstream zstd tier (its `<= 16/128/256 KiB` thresholds). Keying on the raw hint
368/// (or even its ceil-log bucket) therefore over-keys — two hints that resolve
369/// to the identical matcher would each force a full re-prime. Keying on the
370/// resolved (`params`, `table_bits`) pair restores across them.
371///
372/// `table_bits` is the hint-dependent hash-table width the ACTIVE backend
373/// applied (`set_hash_bits` value for Dfast/Row; `0` for HC/Fast, whose widths
374/// already live in `params`). The snapshot is only ever captured on the COPY
375/// path (a hinted, above-cutoff frame), so `table_bits` is always the resolved
376/// Dfast/Row value there, never the unhinted default.
377///
378/// `level` is kept alongside the resolved `params` because some stored matcher
379/// state is derived from the level DIRECTLY, not through `params`: e.g. Dfast's
380/// `use_fast_loop` is true for L3 but false for L4, yet L3 and L4 resolve to
381/// byte-identical `params`. Without `level` a snapshot captured at L3 could be
382/// restored into an L4 reset, installing the wrong `use_fast_loop`.
383///
384/// `fast_attach` records the Fast backend's attach-vs-copy mode
385/// ([`FAST_ATTACH_DICT_CUTOFF_LOG`]) because that cutoff (8 KiB) falls INSIDE a
386/// single resolved shape: an 8192- and an 8193-byte Level 1 hint both clamp to
387/// window_log 14 with identical `params`/`table_bits`, yet 8192 attaches (a
388/// separate dict table) while 8193 copies into the live table — two different
389/// `storage` shapes. The frame compressor only captures/restores snapshots on
390/// the copy path today, but keying on the mode keeps the snapshot identity
391/// self-sufficient rather than relying on that external gate.
392///
393/// Restoring a snapshot whose key differs would reinstate the old `storage`
394/// (and its `max_window_size` / table dimensions / parse params / dict-table
395/// shape) under a reset that resolved a different shape — the encoder could
396/// then search past the frame header's window and emit an undecodable match.
397/// All fields must match before a restore is allowed.
398#[derive(Clone, Copy, PartialEq, Eq)]
399struct PrimedKey {
400 level: super::CompressionLevel,
401 params: LevelParams,
402 table_bits: usize,
403 fast_attach: bool,
404 /// Fine-grained LDM override (#27) active at capture time. The
405 /// snapshot's cloned `storage` carries `BtMatcher::ldm_producer`,
406 /// which is configured from this override; restoring a snapshot
407 /// captured under a different LDM configuration (enable flip or
408 /// changed knobs) would reinstate a stale producer. `params` already
409 /// pins `window_log` / `strategy_tag` (the rest of the producer's
410 /// identity), so folding the override completes the LDM identity.
411 /// `None` = LDM off, matching `ParamOverrides::ldm`.
412 ldm: Option<super::parameters::LdmOverride>,
413}
414
415/// Whether a configured HashChain matcher attaches the dictionary for a frame
416/// of `size_log` (see [`MatchGeneratorDriver::hc_dict_attach_mode`]).
417fn hc_attaches_dictionary(hc: &HcMatchGenerator, size_log: Option<u8>) -> bool {
418 let cutoff = if hc.table.uses_bt {
419 match hc.strategy_tag {
420 super::strategy::StrategyTag::BtUltra | super::strategy::StrategyTag::BtUltra2 => {
421 BT_ULTRA_ATTACH_DICT_CUTOFF_LOG
422 }
423 _ => BT_OPT_ATTACH_DICT_CUTOFF_LOG,
424 }
425 } else {
426 HC_ATTACH_DICT_CUTOFF_LOG
427 };
428 size_log.is_none_or(|log| log <= cutoff)
429}
430
431impl MatchGeneratorDriver {
432 /// See [`MatcherStorage::ingest_capacity`].
433 #[cfg(test)]
434 pub(crate) fn ingest_capacity(&self) -> usize {
435 self.storage.ingest_capacity()
436 }
437
438 /// `slice_size` sets the base block allocation size used for matcher input chunks.
439 /// `max_slices_in_window` determines the initial window capacity at construction
440 /// time. Effective window sizing is recalculated on every [`reset`](Self::reset)
441 /// from the resolved compression level and optional source-size hint.
442 pub(crate) fn new(slice_size: usize, max_slices_in_window: usize) -> Self {
443 // Validate inputs before deriving window_log_init. Three
444 // failure modes need explicit guards:
445 //
446 // 1. Zero args → `max_window_size = 0` → silent 1-byte
447 // degenerate window (useless).
448 // 2. Multiplication overflow on `slice_size *
449 // max_slices_in_window` → wraps silently in release.
450 // 3. `next_power_of_two` overflow when the product is
451 // above `1 << (usize::BITS - 1)` → modern Rust PANICS
452 // on overflow (older Rust returned 0).
453 //
454 // Catch all three at construction with a clear domain-
455 // specific message via `assert!` + `checked_mul` +
456 // `checked_next_power_of_two`, rather than letting either
457 // mode produce a silent degenerate matcher OR a generic
458 // panic deep in `FastKernelMatcher::with_params`.
459 assert!(
460 slice_size > 0,
461 "MatchGeneratorDriver::new requires slice_size > 0 (got 0)",
462 );
463 assert!(
464 max_slices_in_window > 0,
465 "MatchGeneratorDriver::new requires max_slices_in_window > 0 (got 0)",
466 );
467 let max_window_size = max_slices_in_window
468 .checked_mul(slice_size)
469 .expect("MatchGeneratorDriver::new: slice_size * max_slices_in_window overflows usize");
470 // Derive an effective window_log for the initial-state matcher.
471 // `MatchGeneratorDriver::new` runs BEFORE any reset, so it has
472 // no LevelParams to consult — we initialise to whatever
473 // window_log fits the caller's requested max_window_size
474 // (round up to the next power of two via `next_power_of_two`'s
475 // log). Reset() overwrites all three params from the resolved
476 // LevelParams.
477 //
478 // `checked_next_power_of_two` returns `None` if the next power
479 // of two would overflow `usize`. Modern Rust's
480 // `next_power_of_two` PANICS on overflow rather than returning
481 // 0 (the panic message is generic and unhelpful), so use the
482 // checked variant to surface the failure with a clear,
483 // domain-specific error.
484 let next_pow2 = max_window_size.checked_next_power_of_two().expect(
485 "MatchGeneratorDriver::new: max_window_size too large for \
486 next_power_of_two without overflow",
487 );
488 let window_log_init = next_pow2.trailing_zeros() as u8;
489 Self {
490 vec_pool: Vec::new(),
491 // Deferred table: `new` runs before any source size or resolved
492 // LevelParams exist, so allocating at the level-default hash_log
493 // here would be thrown away by the first frame's reset (which
494 // clamps the window to the input and reallocs at the resolved
495 // size). The deferral lets that first reset allocate exactly once.
496 storage: MatcherStorage::Simple(FastKernelMatcher::with_params_deferred(
497 window_log_init,
498 FAST_LEVEL_1_HASH_LOG,
499 FAST_LEVEL_1_MLS,
500 2, // upstream zstd default step_size (targetLength=0 → step=2)
501 )),
502 strategy_tag: super::strategy::StrategyTag::Fast,
503 search: super::strategy::SearchMethod::Fast,
504 parse: super::strategy::ParseMode::Greedy,
505 #[cfg(test)]
506 config_override: None,
507 param_overrides: None,
508 slice_size,
509 base_slice_size: slice_size,
510 // Report the ROUNDED-UP window size that the matcher
511 // actually carries (via `window_log_init = log2(next_pow2)`
512 // → matcher's `max_window_size = 1 << window_log_init =
513 // next_pow2`). For non-power-of-two `slice_size *
514 // max_slices_in_window` inputs, the unrounded value
515 // would under-report the active backend's window until
516 // the first `reset()` overwrites both sides from the
517 // resolved LevelParams.
518 reported_window_size: next_pow2,
519 reset_size_log: None,
520 reset_dict_attach_ok: true,
521 reset_shape: None,
522 dictionary_retained_budget: 0,
523 source_size_hint: None,
524 dictionary_size_hint: None,
525 borrowed_pending: None,
526 primed: None,
527 }
528 }
529
530 fn level_params(level: CompressionLevel, source_size: Option<u64>) -> LevelParams {
531 resolve_level_params(level, source_size)
532 }
533
534 /// Install the public-parameter per-knob overrides (#27) applied at
535 /// the next [`Matcher::reset`]. `None` (or an all-`None` set) restores
536 /// plain level-based geometry. Persists across resets until changed.
537 pub(crate) fn set_param_overrides(
538 &mut self,
539 overrides: Option<super::parameters::ParamOverrides>,
540 ) {
541 self.param_overrides = overrides;
542 }
543
544 /// Active backend family derived from the storage variant. Single
545 /// source of truth — no separate runtime tag to drift against.
546 pub(crate) fn active_backend(&self) -> super::strategy::BackendTag {
547 self.storage.backend()
548 }
549
550 /// Whether the borrowed (no-copy, in-place over-window) scan is
551 /// implemented for the current backend + search configuration. The
552 /// HashChain backend serves both the lazy CHAIN parser
553 /// (`SearchMethod::HashChain`) and the BT/optimal parsers
554 /// (`SearchMethod::BinaryTree`); only the lazy chain has a borrowed scan
555 /// so far, so BT/optimal stay on the owned path.
556 pub(crate) fn borrowed_supported(&self) -> bool {
557 use super::strategy::{BackendTag, SearchMethod};
558 match self.active_backend() {
559 BackendTag::Simple | BackendTag::Dfast | BackendTag::Row => true,
560 // The HashChain backend covers two searches: the lazy CHAIN parser
561 // (borrowed-capable) and the BINARY-TREE search (btlazy2 L13-15 +
562 // optimal BtOpt/BtUltra/BtUltra2 L16-22). btlazy2's BT-tree borrowed
563 // scan is byte-identical to owned (reads via live_history()), so it
564 // takes the in-place path. The OPTIMAL parsers stay owned: their
565 // cost-based DP is sensitive to candidate quality, and the borrowed
566 // continuous-index scan yields slightly different (ratio-worse)
567 // candidates than the owned evict+rehash scan — borrowed optimal
568 // both diverged from owned and fell outside the ffi ratio bound.
569 // Search-aware (not just strategy_tag) so optimal BT can never be
570 // staged on the borrowed path even via an internal caller.
571 BackendTag::HashChain => matches!(self.search, SearchMethod::HashChain),
572 }
573 }
574
575 /// Whether a borrowed scan starting now sees nothing of an earlier frame,
576 /// so the frame is the one a fresh matcher writes. Asked once per frame,
577 /// after `reset`; [`Self::borrowed_supported`] stays the per-block
578 /// invariant. Only the Dfast kernel numbers a borrowed frame's input from
579 /// zero regardless of what its tables hold; the others advance their
580 /// floor past the previous frame, borrowed or not.
581 pub(crate) fn borrowed_frame_is_independent(&self) -> bool {
582 match &self.storage {
583 MatcherStorage::Dfast(d) => !d.tables_hold_earlier_frames,
584 _ => true,
585 }
586 }
587
588 /// Make [`Self::borrowed_frame_is_independent`] hold by emptying the
589 /// tables of earlier frames.
590 pub(crate) fn forget_earlier_frames(&mut self) {
591 if let MatcherStorage::Dfast(d) = &mut self.storage {
592 d.forget_earlier_frames();
593 }
594 }
595
596 /// Whether a DICTIONARY frame can take the borrowed (no input copy) path.
597 /// Only the Simple (Fast) backend with the dictionary ATTACHED (not the
598 /// copy/merge regime) has a borrowed dict scan — `start_matching_borrowed_dict`
599 /// reads live matches from the borrowed input in place and dict matches
600 /// from the committed dict prefix via the 2-segment counter. Every other
601 /// backend, and copy-mode (large-input) dict frames, stay on the owned
602 /// path. Checked AFTER priming, so `is_attached()` reflects the resolved
603 /// attach-vs-copy decision.
604 pub(crate) fn borrowed_dict_supported(&self) -> bool {
605 matches!(
606 &self.storage,
607 MatcherStorage::Simple(m) if m.dict_is_attached()
608 )
609 }
610
611 fn simple_mut(&mut self) -> &mut FastKernelMatcher {
612 match &mut self.storage {
613 MatcherStorage::Simple(m) => m,
614 _ => panic!("simple backend must be initialized by reset() before use"),
615 }
616 }
617
618 /// Reclaim the per-block input buffer that the Simple backend
619 /// just spent inside `start_matching` / `skip_matching_with_hint`.
620 ///
621 /// `FastKernelMatcher::take_recycled_space` returns the cleared
622 /// (capacity-retained) `Vec<u8>` from the last
623 /// `extend_history_with_pending`. We push it onto `vec_pool`
624 /// as-is (with `len = 0`); `get_next_space()` is responsible for
625 /// resizing the buffer back to `slice_size` on its next pop. The
626 /// pushed length is irrelevant — only the capacity matters, and
627 /// `extend_history_with_pending` preserves it. Without this
628 /// recycle path, the Simple backend would allocate a new
629 /// `Vec<u8>` per block — a measurable hot-path cost when blocks
630 /// are small (~128 KiB) and processed at hundreds of MiB/s.
631 fn recycle_simple_space(&mut self) {
632 if let Some(space) = self.simple_mut().take_recycled_space() {
633 // `space` is already cleared (len = 0) by
634 // `extend_history_with_pending`; capacity is retained.
635 // Leaving `len = 0` here avoids the cost of zero-filling
636 // the entire allocation — `get_next_space()` resizes the
637 // popped buffer up to `slice_size` on demand, so the
638 // length the pool holds is irrelevant. This matters most
639 // after a small-source-size hint has shrunk `slice_size`
640 // mid-frame: the recycled buffer can be much larger than
641 // the current `slice_size`, and zero-filling 128 KiB+ on
642 // every block would erase the perf win the recycle path
643 // is meant to deliver.
644 self.vec_pool.push(space);
645 }
646 }
647
648 /// Register a caller-owned input buffer as the Simple backend's
649 /// borrowed one-shot match window. Only valid on the Simple (Fast)
650 /// backend; the one-shot frame path gates on that before calling.
651 ///
652 /// # Safety
653 /// Same contract as [`FastKernelMatcher::set_borrowed_window`]: the
654 /// buffer must stay live and unmodified until the window is cleared,
655 /// and must be cleared before the buffer is dropped or the matcher is
656 /// reused for another frame.
657 pub(crate) unsafe fn set_borrowed_window(&mut self, buffer: &[u8]) {
658 // SAFETY: forwarded contract — caller upholds liveness/clear.
659 match self.active_backend() {
660 super::strategy::BackendTag::Simple => unsafe {
661 self.simple_mut().set_borrowed_window(buffer)
662 },
663 super::strategy::BackendTag::Dfast => unsafe {
664 self.dfast_matcher_mut().set_borrowed_window(buffer)
665 },
666 super::strategy::BackendTag::Row => unsafe {
667 self.row_matcher_mut().set_borrowed_window(buffer)
668 },
669 super::strategy::BackendTag::HashChain => unsafe {
670 self.hc_matcher_mut().set_borrowed_window(buffer)
671 },
672 }
673 }
674
675 /// Clear the borrowed one-shot window, returning the active backend
676 /// to the owned `history` path.
677 pub(crate) fn clear_borrowed_window(&mut self) {
678 match self.active_backend() {
679 super::strategy::BackendTag::Simple => self.simple_mut().clear_borrowed_window(),
680 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().clear_borrowed_window(),
681 super::strategy::BackendTag::Row => self.row_matcher_mut().clear_borrowed_window(),
682 super::strategy::BackendTag::HashChain => self.hc_matcher_mut().clear_borrowed_window(),
683 #[allow(unreachable_patterns)]
684 _ => {}
685 }
686 self.borrowed_pending = None;
687 }
688
689 /// Stage the borrowed block range `[block_start, block_end)` for the
690 /// NEXT `start_matching` / `skip_matching_with_hint`, which the
691 /// borrowed Fast frame path uses in place of `commit_space`. While
692 /// staged, those trait calls route to the Simple backend's borrowed
693 /// scan/skip (consuming the stage) instead of the owned committed
694 /// block. See [`Matcher::start_matching`] /
695 /// [`Matcher::skip_matching_with_hint`] on this type.
696 pub(crate) fn set_borrowed_block(&mut self, block_start: usize, block_end: usize) {
697 assert!(
698 self.borrowed_supported(),
699 "borrowed block staging is not supported for the active backend/search config",
700 );
701 assert!(
702 block_start <= block_end,
703 "borrowed block range must satisfy start <= end (start={block_start} end={block_end})",
704 );
705 self.borrowed_pending = Some((block_start, block_end));
706 // Make the range visible to `get_last_space()` immediately: the
707 // emit pipeline reads `get_last_space().len()` in
708 // `collect_block_parts` BEFORE `start_matching` consumes the
709 // stage, so the staged block (not the whole borrowed window) must
710 // be reported now to keep the literal-buffer reservation right.
711 match self.active_backend() {
712 super::strategy::BackendTag::Simple => self
713 .simple_mut()
714 .stage_borrowed_block(block_start, block_end),
715 super::strategy::BackendTag::Dfast => self
716 .dfast_matcher_mut()
717 .stage_borrowed_block(block_start, block_end),
718 super::strategy::BackendTag::Row => self
719 .row_matcher_mut()
720 .stage_borrowed_block(block_start, block_end),
721 super::strategy::BackendTag::HashChain => self
722 .hc_matcher_mut()
723 .table
724 .stage_borrowed_block(block_start, block_end),
725 }
726 }
727
728 #[cfg(test)]
729 fn dfast_matcher(&self) -> &DfastMatchGenerator {
730 match &self.storage {
731 MatcherStorage::Dfast(m) => m,
732 _ => panic!("dfast backend must be initialized by reset() before use"),
733 }
734 }
735
736 fn dfast_matcher_mut(&mut self) -> &mut DfastMatchGenerator {
737 match &mut self.storage {
738 MatcherStorage::Dfast(m) => m,
739 _ => panic!("dfast backend must be initialized by reset() before use"),
740 }
741 }
742
743 #[cfg(test)]
744 pub(crate) fn row_matcher(&self) -> &RowMatchGenerator {
745 match &self.storage {
746 MatcherStorage::Row(m) => m,
747 _ => panic!("row backend must be initialized by reset() before use"),
748 }
749 }
750
751 pub(crate) fn row_matcher_mut(&mut self) -> &mut RowMatchGenerator {
752 match &mut self.storage {
753 MatcherStorage::Row(m) => m,
754 _ => panic!("row backend must be initialized by reset() before use"),
755 }
756 }
757
758 #[cfg(test)]
759 fn hc_matcher(&self) -> &HcMatchGenerator {
760 match &self.storage {
761 MatcherStorage::HashChain(m) => m,
762 _ => panic!("hash chain backend must be initialized by reset() before use"),
763 }
764 }
765
766 fn hc_matcher_mut(&mut self) -> &mut HcMatchGenerator {
767 match &mut self.storage {
768 MatcherStorage::HashChain(m) => m,
769 _ => panic!("hash chain backend must be initialized by reset() before use"),
770 }
771 }
772
773 /// Shrink the active backend's `max_window_size` by the bytes
774 /// reclaimed from the dictionary-retention budget. Returns `true`
775 /// iff any reclamation happened — the caller uses that as the
776 /// gate for [`Self::trim_after_budget_retire`] (which is a no-op
777 /// otherwise: with `max_window_size` unchanged the backend's
778 /// `trim_to_window` cannot find anything to evict, so calling it
779 /// just runs an extra `match` ladder + a single early-out check
780 /// per slice commit).
781 #[must_use]
782 fn retire_dictionary_budget(&mut self, evicted_bytes: usize) -> bool {
783 let reclaimed = evicted_bytes.min(self.dictionary_retained_budget);
784 if reclaimed == 0 {
785 return false;
786 }
787 self.dictionary_retained_budget -= reclaimed;
788 match self.active_backend() {
789 super::strategy::BackendTag::Simple => {
790 let matcher = self.simple_mut();
791 // `reclaimed` can exceed the CURRENT `max_window_size`: the
792 // retained dict budget is tracked independently and the
793 // window may already have been shrunk by a prior eviction,
794 // so the floor at 0 is the correct clamp, not a masked bug.
795 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
796 }
797 super::strategy::BackendTag::Dfast => {
798 let matcher = self.dfast_matcher_mut();
799 // `reclaimed` can exceed the CURRENT `max_window_size`: the
800 // retained dict budget is tracked independently and the
801 // window may already have been shrunk by a prior eviction,
802 // so the floor at 0 is the correct clamp, not a masked bug.
803 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
804 }
805 super::strategy::BackendTag::Row => {
806 let matcher = self.row_matcher_mut();
807 // `reclaimed` can exceed the CURRENT `max_window_size`: the
808 // retained dict budget is tracked independently and the
809 // window may already have been shrunk by a prior eviction,
810 // so the floor at 0 is the correct clamp, not a masked bug.
811 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
812 }
813 super::strategy::BackendTag::HashChain => {
814 let matcher = self.hc_matcher_mut();
815 // See the Simple arm: `reclaimed` may exceed the current
816 // window, so saturating to 0 is the correct clamp.
817 matcher.table.max_window_size =
818 matcher.table.max_window_size.saturating_sub(reclaimed);
819 }
820 }
821 true
822 }
823
824 fn trim_after_budget_retire(&mut self) {
825 loop {
826 let mut evicted_bytes = 0usize;
827 match self.active_backend() {
828 super::strategy::BackendTag::Simple => {
829 // FastKernelMatcher owns its history as a single
830 // flat `Vec<u8>` (upstream zstd's flat-buffer layout)
831 // rather than the legacy per-block `WindowEntry`
832 // stack. There are no per-block Vec allocations
833 // to recycle into `vec_pool` — `trim_to_window`
834 // drains the oldest bytes in-place and returns
835 // the count for the dictionary-budget loop's
836 // termination check.
837 let MatcherStorage::Simple(m) = &mut self.storage else {
838 unreachable!("active_backend() == Simple proven above");
839 };
840 evicted_bytes += m.trim_to_window();
841 }
842 super::strategy::BackendTag::Dfast => {
843 // Dfast doesn't retain input Vecs — `history` is the
844 // only byte store, so there is no per-block buffer
845 // to push back through a callback. Eviction byte
846 // count is derived from the `window_size` delta
847 // before/after; the Dfast variant of
848 // `trim_to_window` takes no closure, sidestepping
849 // an unused-`impl FnMut` monomorphization that
850 // would otherwise contractually never fire.
851 let dfast = self.dfast_matcher_mut();
852 let pre = dfast.window_size;
853 dfast.trim_to_window();
854 evicted_bytes += pre - dfast.window_size;
855 }
856 super::strategy::BackendTag::Row => {
857 // Row keeps bytes only in the contiguous `history` mirror
858 // (block buffers are returned to the pool per block in
859 // `add_data`), so derive the eviction count from the
860 // `window_size` delta, mirroring the Dfast / HashChain arms.
861 let row = self.row_matcher_mut();
862 let pre = row.window_size;
863 row.trim_to_window();
864 evicted_bytes += pre - row.window_size;
865 }
866 super::strategy::BackendTag::HashChain => {
867 // HC keeps bytes only in the contiguous `history` mirror
868 // (no per-block Vecs to recycle since the window<->history
869 // dedup), so derive the eviction count from the
870 // `window_size` delta, mirroring the Dfast arm above.
871 let table = &mut self.hc_matcher_mut().table;
872 let pre = table.window_size;
873 table.trim_to_window();
874 evicted_bytes += pre - table.window_size;
875 }
876 }
877 if evicted_bytes == 0 {
878 break;
879 }
880 // The loop's invariant is "the backend's previous
881 // `max_window_size` shrink had downstream bytes left to
882 // evict" — that's what `evicted_bytes != 0` proves at
883 // this point. `dictionary_retained_budget` is NOT
884 // guaranteed to be positive here: the outer
885 // `retire_dictionary_budget` call may have already
886 // drained it to zero by reclaiming the last retained
887 // bytes, while the backend still has bytes above the
888 // freshly-shrunk window cap waiting for this loop to
889 // evict. The return value of the retire call below is
890 // therefore intentionally discarded — the loop's
891 // termination is driven by `evicted_bytes == 0`, not by
892 // whether the budget has more bytes left to reclaim.
893 let _ = self.retire_dictionary_budget(evicted_bytes);
894 }
895 }
896
897 /// ATTACH (`true`) vs COPY (`false`) decision for the dms-bearing HashChain
898 /// backend (lazy hash-chain AND binary-tree/optimal levels), mirroring
899 /// upstream `ZSTD_shouldAttachDict` and its per-strategy `attachDictSizeCutoffs`:
900 /// a small / unknown source ATTACHES the dict as a separate dms (hash-chain
901 /// dms for lazy, DUBT dms for BT); a large known source COPIES it into the
902 /// live chain / tree. The cutoff is the lazy/lazy2 value for HC, the
903 /// btlazy2/btopt value for Bt{Opt}, and the smaller btultra/btultra2 value for
904 /// the deepest parses. Both `skip_matching_for_dictionary_priming` (which
905 /// stages the dict) and `prime_with_dictionary` (which builds-or-drops the
906 /// dms) read this so the two stay in lock-step.
907 fn hc_dict_attach_mode(&self) -> bool {
908 // Only the HashChain backend (lazy hash-chain + BT/optimal) routes here;
909 // a non-HashChain storage has no dms decision, so default to attach.
910 let MatcherStorage::HashChain(hc) = &self.storage else {
911 return true;
912 };
913 hc_attaches_dictionary(hc, self.reset_size_log)
914 }
915
916 fn skip_matching_for_dictionary_priming(&mut self, dict_len: usize) {
917 match self.active_backend() {
918 super::strategy::BackendTag::Simple => {
919 // Upstream zstd `ZSTD_shouldAttachDict` mode selection for the Fast
920 // strategy (cutoff 8 KB): small / unknown-size inputs ATTACH
921 // (index dict positions into a SEPARATE immutable table; the
922 // dual-probe 2-cursor `compress_block_fast_dict` then prefers
923 // recent-input matches and falls back to the dict — the path
924 // that wins small/unknown). Large known-size inputs COPY (prime
925 // dict into the live table; the 4-cursor `compress_block_fast`
926 // matches against it as window history — the path that already
927 // matches/beats the upstream zstd on large corpora). The dispatch in
928 // `start_matching` keys off `dict_table.is_some()`, which only
929 // the attach path populates. See [`FAST_ATTACH_DICT_CUTOFF_LOG`].
930 let attach = self.reset_dict_attach_ok
931 && self
932 .reset_size_log
933 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
934 if attach {
935 self.simple_mut().skip_matching_for_dict_prime(dict_len);
936 } else {
937 self.simple_mut().skip_matching_for_dict_copy();
938 }
939 self.recycle_simple_space();
940 }
941 super::strategy::BackendTag::Dfast => {
942 // Upstream zstd `ZSTD_dictMatchState` mode selection for dfast (cutoff
943 // 16 KiB): small / unknown-size inputs ATTACH (build the
944 // separate immutable dict long+short tables; the dual-probe
945 // `start_matching_fast_loop` searches live + dict, the path that
946 // avoids the per-frame dict re-prime that dominates small
947 // `compress-dict`). Larger known-size inputs COPY (re-prime the
948 // dict into the live tables via `skip_matching_dense`, where the
949 // dense scan matches it as window history). `skip_matching_for_dict_attach`
950 // self-gates on `use_fast_loop` (only fast-loop levels carry the
951 // dual-probe; general-path levels fall back to the dense copy).
952 // The tagged dictionary slots index at most
953 // `DFAST_ATTACH_DICT_MAX_LEN` bytes; a larger dictionary is
954 // copied into the live tables instead.
955 let attach = dict_len <= crate::encoding::dfast::DFAST_ATTACH_DICT_MAX_LEN
956 && self
957 .reset_size_log
958 .is_none_or(|log| log <= DFAST_ATTACH_DICT_CUTOFF_LOG);
959 if attach {
960 self.dfast_matcher_mut().skip_matching_for_dict_attach();
961 } else {
962 self.dfast_matcher_mut().invalidate_dict_cache();
963 self.dfast_matcher_mut().skip_matching_dense();
964 }
965 }
966 super::strategy::BackendTag::Row => {
967 // Upstream zstd `ZSTD_RowFindBestMatch` `dictMatchState`: small /
968 // unknown-size inputs ATTACH (build the separate immutable dict
969 // row index; the bounded dual-probe in `row_candidate_rl`
970 // searches live + dict, avoiding the per-frame dict re-index),
971 // larger known-size inputs COPY (dense re-prime into the live
972 // rows).
973 // The attach / copy decision was made with the CDict's cParams
974 // at `reset` (`RowDictPlan`); the backend indexes the dictionary
975 // block accordingly.
976 self.row_matcher_mut().prime_dictionary_current_block();
977 }
978 super::strategy::BackendTag::HashChain => {
979 // Lazy-HC AND BT/optimal both follow upstream zstd `ZSTD_shouldAttachDict`
980 // per-strategy: ATTACH (a separate dms — hash-chain dms for lazy,
981 // DUBT dms for BT) for small / unknown inputs, COPY (merge the dict
982 // into the live chain/tree) for large known inputs. ATTACH keeps
983 // the dict in history but out of the live structure via
984 // `skip_matching_dict_bt` (the cursor advance is shared by both
985 // arms); COPY routes through the normal `skip_matching` (its
986 // `uses_bt` branch fills the live tree, the lazy branch the live
987 // chain). The dms is built-or-dropped to match in
988 // `prime_with_dictionary`.
989 if self.hc_dict_attach_mode() {
990 self.hc_matcher_mut().table.skip_matching_dict_bt();
991 } else {
992 self.hc_matcher_mut().skip_matching(Some(false));
993 }
994 }
995 }
996 }
997}
998
999impl Matcher for MatchGeneratorDriver {
1000 fn supports_dictionary_priming(&self) -> bool {
1001 true
1002 }
1003
1004 fn set_source_size_hint(&mut self, size: u64) {
1005 self.source_size_hint = Some(size);
1006 }
1007
1008 fn set_dictionary_size_hint(&mut self, sizes: super::DictionarySizes) {
1009 self.dictionary_size_hint = Some(sizes);
1010 }
1011
1012 /// Dict-relevance gate for the raw-fast-path. Reached only when a dictionary
1013 /// is active (the caller short-circuits on `dict_active`), so this answers
1014 /// "could the dict compress this otherwise-incompressible-looking block?".
1015 /// The Simple (Fast) backend samples its dict table precisely
1016 /// ([`FastKernelMatcher::block_samples_match_dict`]); the other backends
1017 /// (Dfast / Row / HashChain / BT) have their own dict structures and no cheap
1018 /// probe here, so they answer CONSERVATIVELY `true`: without a probe they
1019 /// cannot tell whether the dict compresses an incompressible-LOOKING block,
1020 /// and answering `false` would let the raw-fast-path emit such a block raw
1021 /// and miss an embedded dict segment. `dictionary_segment_in_incompressible_input_is_matched`
1022 /// pins this for Dfast/Row/BT — the 512-byte dict run inside high-entropy
1023 /// filler is matched only because these backends stay on the scan. So they
1024 /// keep the blanket scan the old `!dict_active` gate gave them; only the
1025 /// Simple/Fast backend trades it for the precise probe.
1026 fn block_samples_match_dict(&self, block: &[u8]) -> bool {
1027 match &self.storage {
1028 MatcherStorage::Simple(m) => m.block_samples_match_dict(block),
1029 _ => true,
1030 }
1031 }
1032
1033 /// Heap bytes this driver owns: the active backend's tables/history, the
1034 /// recycled input-buffer pool, and the primed-dictionary snapshot (a cloned
1035 /// backend kept for CDict-equivalent reuse). The inline struct itself is
1036 /// accounted by the owner's `size_of`.
1037 fn heap_size(&self) -> usize {
1038 let pool: usize = self.vec_pool.capacity() * core::mem::size_of::<Vec<u8>>()
1039 + self.vec_pool.iter().map(Vec::capacity).sum::<usize>();
1040 let snapshot = self
1041 .primed
1042 .as_ref()
1043 .map_or(0, |(storage, _, _)| storage.heap_size());
1044 pool + self.storage.heap_size() + snapshot
1045 }
1046
1047 fn clear_param_overrides(&mut self) {
1048 self.param_overrides = None;
1049 }
1050
1051 fn reset(&mut self, level: CompressionLevel) {
1052 let hint = self.source_size_hint.take();
1053 // An empty dictionary is "no dictionary": it primes nothing, so every
1054 // dictionary-frame decision below must see `None` for it.
1055 let dict_hint = self
1056 .dictionary_size_hint
1057 .take()
1058 .filter(|sizes| sizes.content > 0);
1059 // Snapshot the hint's normalized ceil-log bucket for the primed-snapshot
1060 // key and prime_with_dictionary's attach/copy mode decision (the hint is
1061 // consumed here, but priming happens just after reset). Storing the
1062 // bucket rather than the raw bytes means two hints that resolve to the
1063 // same matcher shape share one snapshot instead of each re-priming.
1064 self.reset_size_log = hint.map(source_size_ceil_log);
1065 // A dictionary too large for the tagged attach position field falls back
1066 // to copy mode. Captured here (from the load-set size hint = actual dict
1067 // length) so the prime decision and the snapshot-key / epoch bits agree.
1068 self.reset_dict_attach_ok =
1069 dict_hint.is_none_or(|sizes| sizes.content <= MAX_FAST_ATTACH_DICT_REGION);
1070 let hinted = hint.is_some();
1071 // A dictionary frame takes its cParams and match-finder from the
1072 // CDict's cParams (upstream `ZSTD_resetCCtx_usingCDict`), whose tier
1073 // is keyed by the serialized dictionary size; a lazy-band CDict also
1074 // carries `dict_plan` to the Row backend. The dictionary is prepared
1075 // under the caller's parameters, so they are part of those cParams.
1076 let overrides = self.param_overrides.unwrap_or_default();
1077 // A dictionary frame runs the shape the dictionary was prepared with,
1078 // whatever the source size. Upstream stops doing that once the source
1079 // outgrows the dictionary and resolves the frame's own instead
1080 // (zstd_compress.c:5264). That alternative was implemented and measured
1081 // against this one and against the reference, in one session with the
1082 // arms interleaved, over five levels and two dictionary shapes, and it
1083 // loses on every row where the two differ. Time per frame over a 1 MiB
1084 // source, ours / the alternative / the reference:
1085 //
1086 // 3 KB dictionary 110 KB dictionary
1087 // L1 1.13 / 1.13 / 0.73 1.14 / 1.15 / 0.79 ms
1088 // L3 0.24 / 0.30 / 0.27 0.26 / 0.32 / 0.42 ms
1089 // L5 0.52 / 2.69 / 0.38 0.43 / 2.77 / 1.16 ms
1090 // L9 0.50 / 5.36 / 1.37 0.71 / 5.58 / 2.28 ms
1091 // L12 0.87 / 5.56 / 1.87 1.60 / 5.58 / 3.08 ms
1092 //
1093 // Three to eight times the time for bytes that match to within a
1094 // percent, and under the larger dictionary this shape also beats the
1095 // reference on BOTH axes from L3 up (at L9, 0.71 ms and 243 bytes
1096 // against 2.28 ms and 1241), which the alternative would have given
1097 // away. A control arm at 64 KiB, under the size where the alternative
1098 // can run at all, stayed within 2.4% with identical output.
1099 //
1100 // The bench host is a VMware guest with no PMU passthrough, so cycles
1101 // and instructions are unavailable there (`perf stat -e cycles` reports
1102 // the event as unsupported); the figures above are `task-clock`, and at
1103 // this margin an instruction count would not be what decides it.
1104 //
1105 // See `dictionary_describes_frame`, which the C ABI still reads for a
1106 // question the codec cannot answer.
1107 let (params, dict_plan) = match dict_hint {
1108 Some(sizes) => crate::encoding::levels::config::resolve_level_params_with_dict(
1109 level, hint, sizes, &overrides,
1110 ),
1111 None => (Self::level_params(level, hint), None),
1112 };
1113 #[cfg_attr(not(test), allow(unused_mut))]
1114 let mut params = params;
1115 // Test-only: apply a parse×search override so the matrix can be
1116 // exercised without editing `LEVEL_TABLE`. Mutating `params` here
1117 // (before `next_backend`) flows the override through storage
1118 // selection, `configure`, and the `self.search`/`self.parse`
1119 // writes uniformly. Consumed with `take()` so it is one-shot: the
1120 // synthetic pairing applies to exactly this `reset()`, and a later
1121 // reset on the same driver falls back to the level's real config.
1122 #[cfg(test)]
1123 if let Some((search, parse)) = self.config_override.take() {
1124 params.search = search;
1125 params.lazy_depth = parse.lazy_depth();
1126 // The matrix sweep can pair a level with a backend its native
1127 // row doesn't populate (e.g. greedy L5, which carries only `row`,
1128 // run on HashChain). Synthesize a default config for the
1129 // overridden backend so its `configure` arm has something to read.
1130 use super::strategy::SearchMethod;
1131 match search {
1132 SearchMethod::Fast => {
1133 params.fast.get_or_insert(FAST_L1);
1134 }
1135 SearchMethod::DoubleFast => {
1136 params.dfast.get_or_insert(DFAST_L3);
1137 }
1138 SearchMethod::RowHash | SearchMethod::BinaryTreeLazy => {
1139 let row = params.row.get_or_insert(ROW_CONFIG);
1140 row.bt = matches!(search, SearchMethod::BinaryTreeLazy);
1141 }
1142 SearchMethod::HashChain | SearchMethod::BinaryTree => {
1143 params.hc.get_or_insert(HC_CONFIG);
1144 }
1145 }
1146 }
1147 // Public-parameter overrides (#27): apply the per-knob set on top
1148 // of the level-resolved params. A strategy override re-routes the
1149 // backend, so this must precede `next_backend` selection. The
1150 // all-`None` case is skipped so default level geometry stays
1151 // byte-identical to plain level-based compression. Shared with the
1152 // workspace estimate, which has to build what this builds.
1153 if let Some(ov) = self.param_overrides {
1154 crate::encoding::levels::config::apply_frame_overrides(
1155 &mut params,
1156 &ov,
1157 dict_hint.is_some(),
1158 hint,
1159 );
1160 }
1161 // A dictionary frame's hash-chain / binary-tree widths are the CDict's
1162 // (`resolve_level_params_with_dict`): verbatim when the dictionary is
1163 // copied into the live tables (`ZSTD_resetCCtx_byCopyingCDict` builds
1164 // the context from the CDict's cParams), re-adjusted to the source
1165 // alone when it is attached (`byAttachingCDict`: the live tables hold
1166 // no dictionary entry, the dms carries its own dict-sized tables), so
1167 // a small source under a large attached dictionary keeps small live
1168 // tables. Nothing re-sizes `params.hc` here.
1169 // Upstream `ZSTD_resolveRowMatchFinderMode` (zstd_compress.c:238): the
1170 // greedy/lazy/lazy2 band searches rows only above a 2^14 window and a
1171 // hash chain otherwise. The Row backend runs that switch itself
1172 // (`RowMatchGenerator::use_chain`), so both finders share ONE parse
1173 // (upstream's single `lazy_generic`) and the level stays on `RowHash`.
1174 let next_backend = params.backend();
1175 let max_window_size = 1usize << params.window_log;
1176 self.dictionary_retained_budget = 0;
1177 // Drop any frame-local borrowed staging so it can't leak across a
1178 // reset and misroute the next start/skip into borrowed dispatch.
1179 self.borrowed_pending = None;
1180 if self.active_backend() != next_backend {
1181 // Drain the outgoing backend's allocations into the shared
1182 // pool. The `match &mut self.storage { ... }` block runs to
1183 // completion before the assignment below replaces the
1184 // variant, so the inner state we just drained is dropped
1185 // with the old variant.
1186 match &mut self.storage {
1187 MatcherStorage::Simple(_m) => {
1188 // FastKernelMatcher owns a flat Vec<u8> history
1189 // and a Vec<u32> hash table — both drop with the
1190 // variant assignment below, no per-block buffers
1191 // to recycle into the driver pools. The
1192 // assignment-replace path collapses to a noop
1193 // pre-pass for this backend.
1194 }
1195 MatcherStorage::Dfast(m) => {
1196 // Drop the long / short hash table allocations
1197 // before calling `m.reset`. Without this prepass,
1198 // `DfastMatchGenerator::reset` would `fill` both
1199 // tables with `DFAST_EMPTY_SLOT` sentinels — wasted
1200 // work given the next assignment to `self.storage`
1201 // is about to drop `m` entirely. `reset` itself
1202 // short-circuits on `if !self.tables.is_empty()`, so
1203 // handing it an empty `Vec` skips the fill loop.
1204 // Mirrors the pre-drain pattern in the HashChain
1205 // arm below (and serves the same peak-memory
1206 // purpose: release the table-allocation footprint
1207 // before constructing the replacement variant).
1208 m.tables = Vec::new();
1209 m.reset();
1210 }
1211 MatcherStorage::Row(m) => {
1212 // One buffer holds the positions and, in its byte tail,
1213 // the cursors and tags — releasing it releases all three.
1214 m.release_tables();
1215 m.reset();
1216 }
1217 MatcherStorage::HashChain(m) => {
1218 // Release oversized tables when switching away from
1219 // HashChain so Best's larger allocations don't persist.
1220 // hash3_table must be released alongside the other
1221 // two: BtUltra2's `1 << HC3_HASH_LOG` entries would
1222 // otherwise stay pinned across the backend switch,
1223 // even though no future caller of this backend will
1224 // touch them.
1225 m.table.tables = Vec::new();
1226 m.table.chain_off = 0;
1227 m.table.hash3_off = 0;
1228 let vec_pool = &mut self.vec_pool;
1229 m.reset(|mut data| {
1230 data.resize(data.capacity(), 0);
1231 vec_pool.push(data);
1232 });
1233 }
1234 }
1235 // Swap in a fresh variant for the new backend. The previous
1236 // `storage` is dropped here.
1237 self.storage = match next_backend {
1238 super::strategy::BackendTag::Simple => {
1239 // Per-level Fast cParams from resolve_level_params:
1240 // Level(1) gets (hash_log=14, mls=7); Level(-7..=-1)
1241 // get upstream zstd row-0 (hash_log=13, mls=7); Fastest /
1242 // Uncompressed keep (hash_log=14, mls=6). See
1243 // resolve_level_params for rationale.
1244 let fast = params.fast.expect("Fast level row carries a FastConfig");
1245 MatcherStorage::Simple(FastKernelMatcher::with_params(
1246 params.window_log,
1247 fast.hash_log,
1248 fast.mls,
1249 fast.step_size,
1250 ))
1251 }
1252 super::strategy::BackendTag::Dfast => {
1253 MatcherStorage::Dfast(DfastMatchGenerator::new(max_window_size))
1254 }
1255 super::strategy::BackendTag::Row => {
1256 MatcherStorage::Row(RowMatchGenerator::new(max_window_size))
1257 }
1258 super::strategy::BackendTag::HashChain => {
1259 MatcherStorage::HashChain(HcMatchGenerator::new(max_window_size))
1260 }
1261 };
1262 }
1263
1264 // Single source of truth: `LevelParams::strategy_tag` is the
1265 // authoritative mapping from `CompressionLevel` to strategy.
1266 // `storage.backend()` derives the parse family from the variant,
1267 // so there is no separate runtime tag that could drift against
1268 // `LEVEL_TABLE`.
1269 self.strategy_tag = params.strategy_tag;
1270 self.search = params.search;
1271 self.parse = params.parse();
1272 self.slice_size = self.base_slice_size.min(max_window_size);
1273 self.reported_window_size = max_window_size;
1274 let strategy_tag = self.strategy_tag;
1275 // Source-proportional table window for the backends whose hash-table
1276 // widths are recomputed here (Dfast / Row). Like the HC / Fast caps
1277 // in `adjust_params_for_source_size`, this sizes the internal tables
1278 // from the RAW source log (not the wire `window_log` floor) so a
1279 // small frame zeroes a small table; it never exceeds the real window.
1280 let table_window_size = match hint {
1281 Some(h) => {
1282 let raw_log = source_size_ceil_log(h);
1283 // Clamp the shift below the pointer width before `1usize <<`:
1284 // an oversized hint (>= 2^63 + 1, and on 32-bit usize any hint
1285 // >= 2^32) drives `raw_log` to 64 / >= 32, and the shift would
1286 // overflow (panic in debug, wrap to 0 in release) before the
1287 // `.min(max_window_size)` cap below could bound it. The min cap
1288 // still provides the real semantic window bound.
1289 let shift = raw_log.max(MIN_WINDOW_LOG).min(usize::BITS as u8 - 1);
1290 (1usize << shift).min(max_window_size)
1291 }
1292 None => max_window_size,
1293 };
1294 // The hint-dependent hash-table width the active backend applies, for
1295 // the primed-snapshot key. Dfast/Row compute it from `table_window_size`
1296 // below; HC/Fast leave it `0` because their widths live in `params`
1297 // (`hc.{hash,chain}_log` / `fast_hash_log`) — already part of the key.
1298 let mut resolved_table_bits: usize = 0;
1299 match &mut self.storage {
1300 MatcherStorage::Simple(m) => {
1301 // Per-level Fast cParams threaded from
1302 // resolve_level_params (see Simple-backend swap
1303 // arm above for the (level → params) mapping).
1304 let fast = params.fast.expect("Fast level row carries a FastConfig");
1305 // Same attach/copy split the dict-prime dispatch applies
1306 // below (`prime_with_dictionary`): only attach-mode dict
1307 // frames may keep the main table across the reset via an
1308 // epoch advance — copy-mode and no-dict frames must memset
1309 // it back to bias 0 for the raw-slice kernels.
1310 let dict_attach_epoch = dict_hint.is_some()
1311 && self.reset_dict_attach_ok
1312 && self
1313 .reset_size_log
1314 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1315 // Copy-mode dictionary frame whose primed snapshot matches
1316 // this exact resolved shape: `restore_primed_dictionary`
1317 // (called right after this reset; the caller gates the
1318 // restore on the same size bucket and the restore re-checks
1319 // the same key) will `clone_from` the snapshot over this
1320 // matcher, replacing the table contents and bias wholesale —
1321 // the reset's full-table memset would be thrown away. The
1322 // key components mirror `reset_shape` below: Simple leaves
1323 // `resolved_table_bits` 0, never carries an LDM override,
1324 // and `fast_attach` is false in copy mode by construction.
1325 let table_overwritten_by_restore = dict_hint.is_some()
1326 && !dict_attach_epoch
1327 && self.primed.as_ref().is_some_and(|(_, _, captured)| {
1328 *captured
1329 == PrimedKey {
1330 level,
1331 params,
1332 table_bits: 0,
1333 fast_attach: false,
1334 ldm: None,
1335 }
1336 });
1337 // Cap `hash_log <= window_log + 1` (upstream zstd
1338 // `ZSTD_adjustCParams_internal`): once `window_log` is resized
1339 // down for a small source, a level-default `1 << hash_log`
1340 // table is mostly wasted address space whose per-frame memset
1341 // dominates the compress cost on tiny frames (a 4 KB frame at
1342 // window_log 12 still zero-fills the 64 KiB hash_log-14 table).
1343 // Gated to no-dict frames: the dict-attach path shares one
1344 // hash_log between the main and dict tables (so one hash keys
1345 // both), and shrinking only the main table would break that
1346 // invariant and the small-frame dict ratio.
1347 let hash_log = if dict_hint.is_some() {
1348 fast.hash_log
1349 } else {
1350 fast.hash_log.min(params.window_log as u32 + 1)
1351 };
1352 // The attached dictionary table takes the CDict's `hashLog`
1353 // (upstream `ZSTD_createCDict`: `ZSTD_getCParams(level,
1354 // UNKNOWN, dictSize)` adjusted for a `minSrcSize` source), not
1355 // the source-capped main width: a small source must not
1356 // shrink the table a large dictionary was sized for.
1357 m.set_dict_table_hash_log(dict_hint.map(|sizes| {
1358 crate::encoding::cparams::get_cdict_cparams(
1359 crate::encoding::levels::config::numeric_level(level),
1360 sizes.serialized,
1361 &overrides,
1362 )
1363 .hash_log
1364 }));
1365 m.reset(
1366 params.window_log,
1367 hash_log,
1368 fast.mls,
1369 fast.step_size,
1370 dict_attach_epoch,
1371 table_overwritten_by_restore,
1372 );
1373 }
1374 MatcherStorage::Dfast(dfast) => {
1375 dfast.max_window_size = max_window_size;
1376 let dcfg = params
1377 .dfast
1378 .expect("Dfast level row must carry a DfastConfig");
1379 // Upstream zstd `cParams.hashLog`/`chainLog`, capped by the
1380 // source-size window when hinted so tiny inputs don't
1381 // over-allocate.
1382 let long_bits = if hinted {
1383 dfast_hash_bits_for_window(table_window_size).min(dcfg.long_hash_log as usize)
1384 } else {
1385 dcfg.long_hash_log as usize
1386 };
1387 let short_bits = if hinted {
1388 dfast_hash_bits_for_window(table_window_size).min(dcfg.short_hash_log as usize)
1389 } else {
1390 dcfg.short_hash_log as usize
1391 };
1392 resolved_table_bits = long_bits;
1393 dfast.set_hash_bits(long_bits, short_bits);
1394 // The attached dictionary tables take the CDict's geometry
1395 // (upstream hashes the dictMatchState tables with
1396 // `dictCParams`), not the source-capped live widths.
1397 dfast.set_dict_table_bits(dict_hint.map(|sizes| {
1398 let cd = crate::encoding::cparams::get_cdict_cparams(
1399 crate::encoding::levels::config::numeric_level(level),
1400 sizes.serialized,
1401 &overrides,
1402 );
1403 (cd.hash_log as usize, cd.chain_log as usize)
1404 }));
1405 // A copy-mode frame (source past the attach cutoff, or a
1406 // dictionary too large to tag) merges the dictionary into the
1407 // live tables; a resident attach-mode table must not be
1408 // re-borrowed under it (same decision as the prime dispatch).
1409 // The width-change invalidation in `set_hash_bits` does not
1410 // cover an unhinted attach frame followed by a large hinted
1411 // one whose live widths coincide at the level's full widths.
1412 let dfast_attach_next = dict_hint.is_some_and(|sizes| {
1413 sizes.content <= crate::encoding::dfast::DFAST_ATTACH_DICT_MAX_LEN
1414 }) && self
1415 .reset_size_log
1416 .is_none_or(|log| log <= DFAST_ATTACH_DICT_CUTOFF_LOG);
1417 if dict_hint.is_some() && !dfast_attach_next {
1418 dfast.invalidate_dict_cache();
1419 }
1420 // Dfast holds no per-block input Vecs (history owns the
1421 // bytes and `add_data` returns each Vec eagerly), so
1422 // `reset` takes no `reuse_space` callback.
1423 dfast.reset();
1424 }
1425 MatcherStorage::Row(row) => {
1426 row.max_window_size = max_window_size;
1427 row.lazy_depth = params.lazy_depth;
1428 row.set_dict_plan(dict_plan);
1429 let mut row_cfg = params.row.expect("Row level row carries a RowConfig");
1430 // A dictionary frame's widths already come from the CDict's
1431 // cParams (adjusted to the source when attached); only a plain
1432 // hinted frame caps them by the window here.
1433 if hinted && dict_plan.is_none() {
1434 // Clamp the configured hash width by the hinted window
1435 // (upstream zstd `ZSTD_adjustCParams` caps hashLog by windowLog) —
1436 // `min`, not replace, so an explicit `hash_log` param
1437 // override (`row_cfg.hash_bits`) survives the hinted path
1438 // instead of being overwritten by the window value.
1439 //
1440 // Clamp BEFORE `configure` so the backend sees ONE width
1441 // per frame. Configuring with the unclamped level width
1442 // and then re-clamping made `row_hash_log` oscillate on
1443 // every hinted frame, and each width change clears the
1444 // row tables — `ensure_tables` then re-filled all three
1445 // every frame in a reused compressor.
1446 row_cfg.hash_bits = row_cfg
1447 .hash_bits
1448 .min(row_hash_bits_for_window(table_window_size));
1449 }
1450 row.configure(row_cfg);
1451 // Key the primed snapshot on the width the backend ACTUALLY
1452 // applied (`set_hash_bits` clamps the request): recording the
1453 // request — or the 0 default on the unhinted path — keys
1454 // identical table geometries apart and forces needless
1455 // dictionary re-primes.
1456 resolved_table_bits = row.hash_bits();
1457 row.reset();
1458 }
1459 MatcherStorage::HashChain(hc) => {
1460 hc.table.max_window_size = max_window_size;
1461 hc.hc.lazy_depth = params.lazy_depth;
1462 let mut hc_cfg = params.hc.expect("HashChain level row carries an HcConfig");
1463 // Cap the hash / chain table logs by the hinted window so a small
1464 // input doesn't allocate the full level's tables (the upstream zstd
1465 // `ZSTD_adjustCParams_internal` clamp: `hashLog <= windowLog + 1`,
1466 // and `cycleLog <= windowLog` — `cycleLog == chainLog` for the HC
1467 // finder, `chainLog - 1` for the BT pair table, so `chainLog <=
1468 // windowLog` (+1 for BT)). Ratio-neutral: a hinted window of
1469 // `2^wlog` bytes holds at most `2^wlog` positions, so the slots
1470 // beyond that are never populated — capping only sheds unused
1471 // allocation. Was the source of L10-lazy peak-alloc ~2.15x the
1472 // upstream zstd on a 1 MiB input. Only applied when hinted; an
1473 // unknown-size stream keeps the full level tables.
1474 // Skip for dictionary frames: their `hc_cfg.{hash,chain}_log`
1475 // are the CDict's (verbatim when the dictionary is copied
1476 // into the live tables, source-adjusted when it is attached);
1477 // re-applying the source-window cap would collapse a copied
1478 // dictionary's tables to the small hinted source.
1479 if hinted && dict_hint.is_none() {
1480 let wlog = hc_hash_bits_for_window(table_window_size);
1481 let uses_bt = matches!(
1482 strategy_tag,
1483 super::strategy::StrategyTag::Btlazy2
1484 | super::strategy::StrategyTag::BtOpt
1485 | super::strategy::StrategyTag::BtUltra
1486 | super::strategy::StrategyTag::BtUltra2
1487 );
1488 hc_cfg.hash_log = hc_cfg.hash_log.min(wlog + 1);
1489 hc_cfg.chain_log = hc_cfg.chain_log.min(if uses_bt { wlog + 1 } else { wlog });
1490 }
1491 hc.configure(hc_cfg, strategy_tag, params.window_log);
1492 // A copy-mode frame merges the dictionary into the live tables,
1493 // so the previous attach frame's dms must not be re-borrowed
1494 // under it (the same decision the prime makes, taken here
1495 // because the reset below re-borrows on a primed dms alone).
1496 if dict_hint.is_some() && !hc_attaches_dictionary(hc, self.reset_size_log) {
1497 hc.table.dms.invalidate();
1498 }
1499 let vec_pool = &mut self.vec_pool;
1500 hc.reset(|mut data| {
1501 data.resize(data.capacity(), 0);
1502 vec_pool.push(data);
1503 });
1504 // When the source size is known, pre-size the history mirror to
1505 // the expected total (dictionary + payload) so per-block growth
1506 // does not overshoot via Vec capacity doubling (upstream zstd sizes its
1507 // window buffer exactly). Dominates peak once the match-finder
1508 // tables are dictionary-tier-small. Unhinted streams skip this
1509 // and keep doubling growth.
1510 if let Some(src) = hint {
1511 // `src` is a u64 hint and may be the u64::MAX "unknown
1512 // size" sentinel, which truncates under `as usize` on
1513 // 32-bit targets and overflows when the dict hint is
1514 // added. Saturate the source size, then saturate the
1515 // dict-hint addition; `reserve_history` applies the
1516 // tighter window ceiling to the result.
1517 let src_hint = usize::try_from(src).unwrap_or(usize::MAX);
1518 let expected =
1519 src_hint.saturating_add(dict_hint.map_or(0, |sizes| sizes.content));
1520 hc.table.reserve_history(expected);
1521 }
1522 }
1523 }
1524 // LDM wiring (#27): attach (or clear) the long-distance-match
1525 // producer on the optimal (BT) backend. LDM is the only
1526 // back-reference path that crosses the regular window, so it
1527 // only has a home on the `BtMatcher`; non-BT strategies drop the
1528 // producer. Built AFTER `hc.reset()` because `BtMatcher::reset`
1529 // clears an existing producer's table but does not null the
1530 // slot — installing here gives the new frame a fresh producer.
1531 #[cfg(feature = "ldm")]
1532 {
1533 // Resolve the derived LDM params first (immutable borrow of the
1534 // overrides), then reuse the existing producer's allocation below.
1535 let derived_ldm = self
1536 .param_overrides
1537 .as_ref()
1538 .and_then(|ov| ov.ldm)
1539 .map(|ldm_ov| crate::encoding::levels::config::frame_ldm_params(¶ms, &ldm_ov));
1540 if let MatcherStorage::HashChain(hc) = &mut self.storage {
1541 // Reuse the existing producer's hash-table allocation when the
1542 // derived params are unchanged: only `clear()` (re-zero the
1543 // table + re-seed the rolling hash, no allocation) is needed for
1544 // the new frame. A params change (or the first frame) forces a
1545 // fresh `LdmProducer::new`. On the reused-encoder compress-dict
1546 // path this avoids re-allocating the LDM hash table (large at
1547 // btultra2) every frame — upstream zstd reuses its `ldmState_t`
1548 // the same way. `clear()` is mandatory here for correctness
1549 // regardless of what `BtMatcher::reset` did to the old table.
1550 let producer = derived_ldm.map(|p| match hc.take_ldm_producer() {
1551 Some(mut existing) if existing.params() == p => {
1552 existing.clear();
1553 existing
1554 }
1555 _ => super::ldm::LdmProducer::new(p),
1556 });
1557 hc.set_ldm_producer(producer);
1558 }
1559 }
1560 // Record the resolved matcher shape for the primed-snapshot key. Captured
1561 // here (post-resolution, after the test-only param override) so the key
1562 // reflects exactly the geometry the restored `storage` must match. The
1563 // Fast attach-vs-copy mode is part of the shape ONLY for the Simple
1564 // backend (it decides the distinct dict-table shape that backend builds).
1565 // Dfast/Row/HashChain have their OWN attach/copy regimes, but this bit
1566 // models only the Fast table split; those backends are keyed by the
1567 // resolved matcher geometry instead, so folding the Fast bit into their
1568 // key would over-key identical resolved shapes. When it applies it
1569 // matches the decision `prime_with_dictionary` makes from the same
1570 // `reset_size_log`.
1571 let fast_attach = matches!(next_backend, super::strategy::BackendTag::Simple)
1572 && self.reset_dict_attach_ok
1573 && self
1574 .reset_size_log
1575 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1576 // The LDM override is part of the snapshot identity ONLY on the
1577 // optimal (BinaryTree) path: that is the only backend whose cloned
1578 // `storage` carries a `BtMatcher::ldm_producer`. On Fast / Dfast /
1579 // Row and lazy-HashChain resets the producer slot does not exist,
1580 // so folding the override there would over-key the snapshot and
1581 // force needless re-primes when LDM is toggled. Gated like
1582 // `fast_attach` (a key bit only participates where it changes the
1583 // cloned matcher shape).
1584 let active_ldm = if matches!(params.search, super::strategy::SearchMethod::BinaryTree) {
1585 self.param_overrides.and_then(|ov| ov.ldm)
1586 } else {
1587 None
1588 };
1589 self.reset_shape = Some((params, resolved_table_bits, fast_attach, active_ldm));
1590 }
1591
1592 // Dictionary entry points forward to the `dict_prime` child module, which
1593 // owns the prime / snapshot lifecycle (it reaches the driver's private
1594 // `primed` / `reset_shape` state directly as a descendant module).
1595
1596 #[inline]
1597 fn dictionary_is_resident(&self) -> bool {
1598 self.dictionary_is_resident_impl()
1599 }
1600
1601 #[inline]
1602 fn reapply_resident_dictionary(&mut self, offset_hist: [u32; 3]) {
1603 self.reapply_resident_dictionary_impl(offset_hist)
1604 }
1605
1606 #[inline]
1607 fn prime_with_dictionary(&mut self, dict_content: &[u8], offset_hist: [u32; 3]) {
1608 self.prime_with_dictionary_impl(dict_content, offset_hist)
1609 }
1610
1611 #[inline]
1612 fn restore_primed_dictionary(&mut self, level: super::CompressionLevel) -> bool {
1613 self.restore_primed_dictionary_impl(level)
1614 }
1615
1616 #[inline]
1617 fn capture_primed_dictionary(&mut self, level: super::CompressionLevel) {
1618 self.capture_primed_dictionary_impl(level)
1619 }
1620
1621 #[inline]
1622 fn invalidate_primed_dictionary(&mut self) {
1623 self.invalidate_primed_dictionary_impl()
1624 }
1625
1626 #[inline]
1627 fn seed_dictionary_entropy(
1628 &mut self,
1629 huff: Option<&crate::huff0::huff0_encoder::HuffmanTable>,
1630 ll: Option<&crate::fse::fse_encoder::FSETable>,
1631 ml: Option<&crate::fse::fse_encoder::FSETable>,
1632 of: Option<&crate::fse::fse_encoder::FSETable>,
1633 ) {
1634 self.seed_dictionary_entropy_impl(huff, ll, ml, of)
1635 }
1636
1637 fn window_size(&self) -> u64 {
1638 self.reported_window_size as u64
1639 }
1640
1641 fn get_next_space(&mut self) -> Vec<u8> {
1642 if let Some(mut space) = self.vec_pool.pop() {
1643 if space.len() > self.slice_size {
1644 space.truncate(self.slice_size);
1645 }
1646 if space.len() < self.slice_size {
1647 space.resize(self.slice_size, 0);
1648 }
1649 return space;
1650 }
1651 alloc::vec![0; self.slice_size]
1652 }
1653
1654 fn get_last_space(&mut self) -> &[u8] {
1655 match &self.storage {
1656 MatcherStorage::Simple(m) => m.last_committed_space(),
1657 MatcherStorage::Dfast(m) => m.get_last_space(),
1658 MatcherStorage::Row(m) => m.get_last_space(),
1659 MatcherStorage::HashChain(m) => m.table.get_last_space(),
1660 }
1661 }
1662
1663 /// Read the next block STRAIGHT into the backend's history buffer, so the
1664 /// owned block loop does not stage it in a scratch `Vec` and copy it in.
1665 ///
1666 /// Returns `None` when the active backend has no in-place ingest, in which
1667 /// case the caller keeps the staged-copy path. Dfast, Row and HashChain
1668 /// implement it; Simple stages the block in a `pending` slot that the
1669 /// kernel consumes, so its bytes do not reach `history` until match time
1670 /// and the two-phase shape does not apply to it as written.
1671 ///
1672 /// On `Some`, the bytes are in the buffer but not yet part of the window:
1673 /// the caller picks the block boundary from
1674 /// [`Self::uncommitted_input`] and then calls [`Self::commit_filled`].
1675 fn fill_in_place(
1676 &mut self,
1677 capacity: usize,
1678 fill: &mut dyn FnMut(&mut Vec<u8>) -> (usize, bool),
1679 ) -> Option<(usize, bool)> {
1680 match &mut self.storage {
1681 MatcherStorage::Dfast(m) => Some(m.fill_uncommitted(capacity, fill)),
1682 MatcherStorage::Row(m) => Some(m.fill_uncommitted(capacity, fill)),
1683 MatcherStorage::HashChain(m) => Some(m.table.fill_uncommitted(capacity, fill)),
1684 MatcherStorage::Simple(_) => None,
1685 }
1686 }
1687
1688 fn reserve_for_frame(&mut self, bytes: usize) {
1689 match &mut self.storage {
1690 MatcherStorage::Dfast(m) => m.reserve_for_frame(bytes),
1691 MatcherStorage::Row(m) => m.reserve_for_frame(bytes),
1692 MatcherStorage::HashChain(m) => m.table.reserve_for_frame(bytes),
1693 MatcherStorage::Simple(m) => m.reserve_for_frame(bytes),
1694 }
1695 }
1696
1697 /// Bytes read by [`Self::fill_in_place`] that no block has claimed yet.
1698 fn uncommitted_input(&self) -> &[u8] {
1699 match &self.storage {
1700 MatcherStorage::Dfast(m) => m.uncommitted(),
1701 MatcherStorage::Row(m) => m.uncommitted(),
1702 MatcherStorage::HashChain(m) => m.table.uncommitted(),
1703 MatcherStorage::Simple(_) => &[],
1704 }
1705 }
1706
1707 /// Claim `len` bytes of [`Self::uncommitted_input`] as the next block.
1708 ///
1709 /// Runs the same eviction accounting as [`Self::commit_space`]: a
1710 /// dictionary inflates `max_window_size` so the primed bytes stay
1711 /// reachable, and once eviction carries them out of the window that
1712 /// inflation has to be retired. Skipping it leaves the backend admitting
1713 /// matches older than the window the frame header reports, which encodes
1714 /// an offset no decoder can resolve.
1715 fn commit_filled(&mut self, len: usize) {
1716 // Same derivation as `commit_space`: the eviction loop decrements
1717 // `window_size` per evicted block before the commit adds `len`, so
1718 // `evicted = pre + len - post`.
1719 let pre = match &self.storage {
1720 MatcherStorage::Dfast(m) => m.window_size,
1721 MatcherStorage::Row(m) => m.window_size,
1722 MatcherStorage::HashChain(m) => m.table.window_size,
1723 MatcherStorage::Simple(_) => 0,
1724 };
1725 match &mut self.storage {
1726 MatcherStorage::Dfast(m) => m.commit_block(len),
1727 MatcherStorage::Row(m) => m.commit_block(len),
1728 MatcherStorage::HashChain(m) => m.table.commit_block(len),
1729 MatcherStorage::Simple(_) => return,
1730 }
1731 let post = match &self.storage {
1732 MatcherStorage::Dfast(m) => m.window_size,
1733 MatcherStorage::Row(m) => m.window_size,
1734 MatcherStorage::HashChain(m) => m.table.window_size,
1735 MatcherStorage::Simple(_) => 0,
1736 };
1737 // `saturating_sub` floors the no-eviction case at 0; `pre + len` are
1738 // byte counts bounded by the window, so the sum cannot overflow.
1739 let evicted_bytes = (pre + len).saturating_sub(post);
1740 if self.retire_dictionary_budget(evicted_bytes) {
1741 self.trim_after_budget_retire();
1742 }
1743 }
1744
1745 fn commit_space(&mut self, space: Vec<u8>) {
1746 let mut evicted_bytes = 0usize;
1747 // Split borrows manually so the `add_data` closures can write
1748 // into `vec_pool` while the backend itself holds an exclusive
1749 // borrow via `storage`. (Suffix-store recycling went away
1750 // with the legacy `MatchGenerator`; the FastKernelMatcher
1751 // arm below has no pool interaction.)
1752 let vec_pool = &mut self.vec_pool;
1753 match &mut self.storage {
1754 MatcherStorage::Simple(m) => {
1755 // FastKernelMatcher owns its history as a single
1756 // flat Vec<u8> and the hash table as a Vec<u32> —
1757 // neither recycles into the driver-side pools. The
1758 // eager pre-commit eviction inside
1759 // `FastKernelMatcher::accept_data` drops bytes when
1760 // accepting this block would push history past 2×
1761 // max_window_size; that delta is what feeds
1762 // `evicted_bytes` here via the `pre / post`
1763 // history-length comparison.
1764 let pre = m.history_len_for_eviction_accounting();
1765 m.accept_data(space);
1766 let post = m.history_len_for_eviction_accounting();
1767 // `accept_data` performs eager pre-commit window
1768 // eviction (so this `pre - post` delta correctly
1769 // feeds the dictionary-budget retire flow). See
1770 // `FastKernelMatcher::accept_data` for the
1771 // commit-time-visibility rationale (closes #216
1772 // CodeRabbit review #5 / Copilot review #1: without
1773 // eager eviction, the delta was always 0 and the
1774 // dict budget never retired, leaving max_window_size
1775 // inflated post-dict-prime → matcher could emit
1776 // offsets exceeding the frame header's window).
1777 evicted_bytes += pre.saturating_sub(post);
1778 }
1779 MatcherStorage::Dfast(m) => {
1780 // Dfast's `add_data` callback receives the INPUT
1781 // `Vec<u8>` for pool recycling (Dfast stores its
1782 // bytes in the contiguous `history` buffer, not in
1783 // per-block Vecs — there is no per-block buffer to
1784 // pop off and hand back). Counting `data.len()` as
1785 // evicted bytes would conflate "new bytes ingested"
1786 // with "old bytes evicted from window"; the two
1787 // happen to coincide when the previous window was
1788 // saturated and the new input fills it 1:1, but
1789 // diverge when the eviction pop-loop drops blocks
1790 // of a different size than the incoming input. The
1791 // `dictionary_retained_budget` retire decision
1792 // downstream then gets driven by inflated eviction
1793 // counts and shrinks `max_window_size` prematurely.
1794 //
1795 // Derive the real eviction delta from `window_size`
1796 // before/after the call. The pop loop inside
1797 // `add_data` decrements `window_size` by each
1798 // evicted block length and then the final
1799 // `extend_from_slice + push_back` adds `space_len`,
1800 // so `evicted = pre + space_len - post`.
1801 let pre = m.window_size;
1802 let space_len = space.len();
1803 m.add_data(space, |data| {
1804 // Same per-block recycle as the HashChain arm: push
1805 // the spent input buffer back as-is rather than
1806 // zero-filling to capacity. `add_data` mirrors the
1807 // bytes into `history` and calls this every block, so
1808 // capacity-wide zeroing would be hot-path waste;
1809 // `get_next_space` zeroes at most `slice_size` bytes
1810 // when it later reuses the buffer.
1811 vec_pool.push(data);
1812 });
1813 // Plain `+` (the `saturating_sub` floors at 0): `pre` + one
1814 // block are byte counts bounded by the window, no overflow.
1815 evicted_bytes += (pre + space_len).saturating_sub(m.window_size);
1816 }
1817 MatcherStorage::Row(m) => {
1818 // RowMatchGenerator::add_data recycles the *input* buffer
1819 // through this callback every commit (its bytes are mirrored
1820 // into `history`), not the evicted chunks. Derive the eviction
1821 // delta from `window_size` before/after — `evicted = pre +
1822 // space_len - post` — exactly like the Simple / HashChain arms.
1823 // Counting the callback argument as evicted would charge the
1824 // whole committed block as evicted and prematurely retire
1825 // dictionary budget on a window that evicts nothing.
1826 let pre = m.window_size;
1827 let space_len = space.len();
1828 m.add_data(space, |data| {
1829 // Recycle the spent buffer as-is; `add_data` runs this for
1830 // every committed block, so zero-filling to capacity here
1831 // would be hot-path waste (`get_next_space` zeroes at most
1832 // `slice_size` on reuse).
1833 vec_pool.push(data);
1834 });
1835 // Plain `+` (the `saturating_sub` floors at 0): `pre` + one
1836 // block are byte counts bounded by the window, no overflow.
1837 evicted_bytes += (pre + space_len).saturating_sub(m.window_size);
1838 }
1839 MatcherStorage::HashChain(m) => {
1840 // MatchTable::add_data now recycles the *incoming* buffer
1841 // through `reuse_space` (its bytes are copied into the
1842 // contiguous `history` mirror), so the callback no longer
1843 // reports evicted chunks. Derive the eviction delta from
1844 // `window_size` before/after, exactly like the Simple arm:
1845 // `evicted = pre + space_len - post`.
1846 let pre = m.table.window_size;
1847 let space_len = space.len();
1848 m.table.add_data(space, |data| {
1849 // Recycle the spent input buffer to the pool as-is.
1850 // `add_data` runs this callback for every committed
1851 // block (the bytes are mirrored into `history`), so
1852 // growing the buffer to its full capacity here would
1853 // zero the whole allocation on the hot path.
1854 // `get_next_space` resizes a popped buffer to
1855 // `slice_size` on demand, touching at most
1856 // `slice_size` bytes — never the larger capacity the
1857 // pool retains.
1858 vec_pool.push(data);
1859 });
1860 // Plain `+` (the `saturating_sub` floors at 0): byte counts
1861 // bounded by the window, no overflow.
1862 evicted_bytes += (pre + space_len).saturating_sub(m.table.window_size);
1863 }
1864 }
1865 // Gate the second backend trim pass on actual budget
1866 // reclamation. Without it, every slice commit on the
1867 // no-dictionary / no-eviction path (the common case) would
1868 // run a backend `match` ladder + `trim_to_window` early-out
1869 // for no reason — `trim_after_budget_retire` only does
1870 // meaningful work when `retire_dictionary_budget` shrank
1871 // `max_window_size` enough to make the backend's
1872 // `window_size > max_window_size` invariant trigger
1873 // eviction.
1874 if self.retire_dictionary_budget(evicted_bytes) {
1875 self.trim_after_budget_retire();
1876 }
1877 }
1878
1879 fn start_matching(&mut self, mut handle_sequence: impl for<'a> FnMut(Sequence<'a>)) {
1880 use super::strategy::{self, StrategyTag};
1881 // Borrowed one-shot Fast path: if the frame driver staged a
1882 // block range via `set_borrowed_block`, scan it in place against
1883 // the borrowed window instead of the owned committed block. Only
1884 // the Simple backend is instrumented (the gate guarantees it),
1885 // and the stage is consumed so the next block re-stages.
1886 if let Some((block_start, block_end)) = self.borrowed_pending.take() {
1887 match self.active_backend() {
1888 super::strategy::BackendTag::Simple => {
1889 let m = self.simple_mut();
1890 if m.dict_is_attached() {
1891 // Dict-attach borrowed scan: live matches read the
1892 // borrowed input in place, dict matches read the
1893 // committed dict prefix via the 2-segment counter.
1894 m.start_matching_borrowed_dict(
1895 block_start,
1896 block_end,
1897 &mut handle_sequence,
1898 );
1899 } else {
1900 m.start_matching_borrowed(block_start, block_end, &mut handle_sequence);
1901 }
1902 }
1903 super::strategy::BackendTag::Dfast => self
1904 .dfast_matcher_mut()
1905 .start_matching_borrowed(block_start, block_end, &mut handle_sequence),
1906 super::strategy::BackendTag::Row => {
1907 // Same greedy/lazy parse split as the owned RowHash arm.
1908 let greedy = self.parse == super::strategy::ParseMode::Greedy;
1909 self.row_matcher_mut().start_matching_borrowed(
1910 block_start,
1911 block_end,
1912 greedy,
1913 &mut handle_sequence,
1914 );
1915 }
1916 super::strategy::BackendTag::HashChain => match self.search {
1917 super::strategy::SearchMethod::HashChain => self
1918 .hc_matcher_mut()
1919 .start_matching_lazy_borrowed(block_start, block_end, &mut handle_sequence),
1920 // `borrowed_supported()` keeps the optimal parsers on the
1921 // owned path and `set_borrowed_block` asserts it.
1922 other => {
1923 unreachable!("HashChain backend with unexpected borrowed search {other:?}")
1924 }
1925 },
1926 }
1927 return;
1928 }
1929 // Decoupled parse×search dispatch (fires once per block). The
1930 // search axis (`self.search`) picks the candidate-finding backend;
1931 // the parse axis (greedy vs lazy depth) is carried by the
1932 // backend's runtime `lazy_depth`, set per level at `reset()`.
1933 // The two are independent, so any parse can run on any search
1934 // backend. The `BinaryTree` arm still selects the opt `Strategy`
1935 // ZST off `strategy_tag` so `compress_block::<S>` keeps its
1936 // const-folded optimal-parser monomorphisation.
1937 use super::strategy::SearchMethod;
1938 match self.search {
1939 SearchMethod::Fast => {
1940 self.simple_mut().start_matching(&mut handle_sequence);
1941 self.recycle_simple_space();
1942 }
1943 SearchMethod::DoubleFast => {
1944 self.dfast_matcher_mut()
1945 .start_matching(&mut handle_sequence);
1946 }
1947 SearchMethod::RowHash | SearchMethod::BinaryTreeLazy => {
1948 // One upstream `lazy_generic` body for greedy (depth 0: a
1949 // repcode hit is stored without a search, no lookahead), lazy
1950 // / lazy2 (`lazy_depth` 1 / 2) and btlazy2 (depth 2 over the
1951 // binary-tree finder); the finder (rows / chain / tree) was
1952 // resolved at `configure`.
1953 self.row_matcher_mut().start_matching(&mut handle_sequence);
1954 }
1955 SearchMethod::HashChain => {
1956 // Greedy/lazy/lazy2 all flow through the lazy parser; it
1957 // reads `hc.lazy_depth` (0 = greedy commit).
1958 self.hc_matcher_mut()
1959 .start_matching_lazy(&mut handle_sequence);
1960 }
1961 SearchMethod::BinaryTree => match self.strategy_tag {
1962 StrategyTag::BtOpt => self.compress_block::<strategy::BtOpt>(&mut handle_sequence),
1963 StrategyTag::BtUltra => {
1964 self.compress_block::<strategy::BtUltra>(&mut handle_sequence)
1965 }
1966 StrategyTag::BtUltra2 => {
1967 self.compress_block::<strategy::BtUltra2>(&mut handle_sequence)
1968 }
1969 _ => unreachable!(
1970 "SearchMethod::BinaryTree requires an optimal strategy tag (BtOpt/BtUltra/BtUltra2)"
1971 ),
1972 },
1973 }
1974 }
1975
1976 fn skip_matching(&mut self) {
1977 self.skip_matching_with_hint(None);
1978 }
1979
1980 fn skip_matching_with_hint(&mut self, incompressible_hint: Option<bool>) {
1981 // Borrowed one-shot Fast path: a staged block range routes to the
1982 // borrowed skip (records the range for `get_last_space`, primes
1983 // hashes on the dict-priming hint) with no owned-history append
1984 // and nothing to recycle. Stage is consumed.
1985 if let Some((block_start, block_end)) = self.borrowed_pending.take() {
1986 match self.active_backend() {
1987 super::strategy::BackendTag::Simple => self.simple_mut().skip_matching_borrowed(
1988 block_start,
1989 block_end,
1990 incompressible_hint,
1991 ),
1992 super::strategy::BackendTag::Dfast => self
1993 .dfast_matcher_mut()
1994 .skip_matching_borrowed(block_start, block_end, incompressible_hint),
1995 super::strategy::BackendTag::Row => self.row_matcher_mut().skip_matching_borrowed(
1996 block_start,
1997 block_end,
1998 incompressible_hint,
1999 ),
2000 super::strategy::BackendTag::HashChain => self
2001 .hc_matcher_mut()
2002 .skip_matching_borrowed(block_start, block_end, incompressible_hint),
2003 }
2004 return;
2005 }
2006 match self.active_backend() {
2007 super::strategy::BackendTag::Simple => {
2008 self.simple_mut()
2009 .skip_matching_with_hint(incompressible_hint);
2010 self.recycle_simple_space();
2011 }
2012 super::strategy::BackendTag::Dfast => {
2013 self.dfast_matcher_mut().skip_matching(incompressible_hint)
2014 }
2015 super::strategy::BackendTag::Row => self
2016 .row_matcher_mut()
2017 .skip_matching_with_hint(incompressible_hint),
2018 super::strategy::BackendTag::HashChain => {
2019 self.hc_matcher_mut().skip_matching(incompressible_hint)
2020 }
2021 }
2022 }
2023}
2024
2025impl MatchGeneratorDriver {
2026 /// Monomorphised optimal-parser entry point. Only the `BinaryTree`
2027 /// search arm of [`Matcher::start_matching`] routes here, selecting
2028 /// the concrete opt `S: Strategy` (BtOpt / BtUltra / BtUltra2) off
2029 /// `strategy_tag`, so the optimiser keeps the cost-model predicates
2030 /// (`S::USE_BT` / `S::USE_HASH3` / `S::ACCURATE_PRICE` /
2031 /// `S::TWO_PASS_SEED`) const-folded per strategy. The non-opt search
2032 /// backends (Fast / DoubleFast / RowHash / HashChain) are dispatched
2033 /// directly off the search axis and never reach this method, so all
2034 /// strategies arriving here are HashChain-backed.
2035 fn compress_block<S: super::strategy::Strategy>(
2036 &mut self,
2037 handle_sequence: &mut impl for<'a> FnMut(Sequence<'a>),
2038 ) {
2039 debug_assert_eq!(S::BACKEND, super::strategy::BackendTag::HashChain);
2040 debug_assert!(
2041 S::USE_BT,
2042 "compress_block only handles the optimal (BT) path"
2043 );
2044 self.hc_matcher_mut()
2045 .start_matching_strategy::<S>(handle_sequence);
2046 }
2047}
2048
2049/// Stage D: backend storage discriminator.
2050///
2051/// HC (lazy / lazy2) modes carry no extra per-frame state beyond the
2052/// shared `MatchTable` and `HcMatcher` runtime knobs, so the
2053/// [`HcBackend::Hc`] variant is zero-sized — no BT scratch is
2054/// allocated. BT-flavoured modes (`btopt` / `btultra` / `btultra2`)
2055/// hold the full [`super::bt::BtMatcher`] inside the
2056/// [`HcBackend::Bt`] variant (cost model, optimal-parser scratch
2057/// arenas, LDM candidate buffer).
2058///
2059/// The discriminator lives next to `parse_mode` so `configure()` can
2060/// promote between the two on a level change without touching the
2061/// `MatchTable` storage.
2062#[derive(Clone)]
2063pub(crate) enum HcBackend {
2064 /// Lazy / lazy2 modes — no per-frame backend state.
2065 Hc,
2066 /// BT-driven modes — owns the optimal parser's per-frame scratch.
2067 /// Boxed so the enum stays pointer-sized: HC-only matchers pay
2068 /// just the `Box`-niche, not the 4 KiB `BtMatcher` payload.
2069 Bt(alloc::boxed::Box<super::bt::BtMatcher>),
2070}
2071
2072#[cfg(feature = "bench-internals")]
2073pub(crate) fn level22_block_ranges(data: &[u8]) -> Vec<(usize, usize)> {
2074 let mut ranges = Vec::new();
2075 let mut cursor = 0usize;
2076 let mut savings = 0i64;
2077 while cursor < data.len() {
2078 let remaining = data.len() - cursor;
2079 let candidate_len = remaining.min(super::cost_model::HC_BLOCKSIZE_MAX);
2080 let block_len = crate::encoding::frame_compressor::optimal_block_size(
2081 CompressionLevel::Level(22),
2082 &data[cursor..cursor + candidate_len],
2083 remaining,
2084 super::cost_model::HC_BLOCKSIZE_MAX,
2085 savings,
2086 )
2087 .min(candidate_len)
2088 .max(1);
2089 ranges.push((cursor, block_len));
2090 cursor += block_len;
2091 // The exact upstream zstd gate uses compressed-size savings. For this corpus
2092 // parity harness, after the first full block has compressed, savings is
2093 // sufficient to authorize the same pre-block splitter path.
2094 if cursor >= super::cost_model::HC_BLOCKSIZE_MAX {
2095 savings = 3;
2096 }
2097 }
2098 ranges
2099}
2100
2101#[cfg(feature = "bench-internals")]
2102fn merge_block_delimiters(sequences: Vec<(usize, usize, usize)>) -> Vec<(usize, usize, usize)> {
2103 let mut out = Vec::with_capacity(sequences.len());
2104 let mut pending_lits = 0usize;
2105 for (lit_len, offset, match_len) in sequences {
2106 if offset == 0 && match_len == 0 {
2107 pending_lits = pending_lits.saturating_add(lit_len);
2108 continue;
2109 }
2110 out.push((lit_len.saturating_add(pending_lits), offset, match_len));
2111 pending_lits = 0;
2112 }
2113 if pending_lits > 0 {
2114 out.push((pending_lits, 0, 0));
2115 }
2116 out
2117}
2118
2119/// White-box capture of the level-22 sequence stream (literal-length,
2120/// offset, match-length triples) the match generator emits for `data`,
2121/// with block-delimiter pseudo-sequences merged into the following
2122/// triple's literal run. Pure Rust; the C-conformance comparison that
2123/// consumes it lives in the `ffi-bench` crate.
2124#[cfg(feature = "bench-internals")]
2125pub(crate) fn collect_level22_sequences(data: &[u8]) -> Vec<(usize, usize, usize)> {
2126 merge_block_delimiters(collect_level22_sequences_with_delimiters(data))
2127 .into_iter()
2128 .filter(|(_, offset, match_len)| *offset != 0 || *match_len != 0)
2129 .collect()
2130}
2131
2132#[cfg(feature = "bench-internals")]
2133fn collect_level22_sequences_with_delimiters(data: &[u8]) -> Vec<(usize, usize, usize)> {
2134 let mut driver = MatchGeneratorDriver::new(super::cost_model::HC_BLOCKSIZE_MAX, 1);
2135 driver.set_source_size_hint(data.len() as u64);
2136 driver.reset(CompressionLevel::Level(22));
2137
2138 let mut sequences = Vec::new();
2139 for (chunk_start, chunk_len) in level22_block_ranges(data) {
2140 let chunk = &data[chunk_start..chunk_start + chunk_len];
2141 let mut space = driver.get_next_space();
2142 space[..chunk.len()].copy_from_slice(chunk);
2143 space.truncate(chunk.len());
2144 driver.commit_space(space);
2145 driver.start_matching(|seq| {
2146 let entry = match seq {
2147 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
2148 Sequence::Triple {
2149 literals,
2150 offset,
2151 match_len,
2152 } => (literals.len(), offset, match_len),
2153 };
2154 sequences.push(entry);
2155 });
2156 }
2157 sequences
2158}
2159
2160#[cfg(test)]
2161mod tests;