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