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