structured_zstd/encoding/levels/config.rs
1//! Per-level compression tuning: the matcher config structs, the level
2//! parameter table, and the level → params resolution chain.
3//!
4//! Moved verbatim from `match_generator.rs` (no behaviour change): the
5//! `HcConfig` / `RowConfig` / `DfastConfig` / `FastConfig` knobs, `LevelParams`
6//! and `LEVEL_TABLE`, the public-parameter overrides, the source-size tiering,
7//! and the workspace estimators. `match_generator` imports this resolution API
8//! instead of carrying it inline. Encoding-level paths are written absolute
9//! (`crate::encoding::…`) so the module can live under `levels/` unchanged.
10
11use crate::encoding::CompressionLevel;
12use crate::encoding::match_generator::{HC_SEARCH_DEPTH, HC_TARGET_LEN, ROW_MIN_MATCH_LEN};
13#[cfg(test)]
14use crate::encoding::match_generator::{ROW_HASH_BITS, ROW_LOG, ROW_SEARCH_DEPTH, ROW_TARGET_LEN};
15#[cfg(test)]
16use crate::encoding::match_table::storage::{HC_CHAIN_LOG, HC_HASH_LOG};
17/// Bundled tuning knobs for the hash-chain matcher. Using a typed config
18/// instead of positional `usize` args eliminates parameter-order hazards.
19#[derive(Copy, Clone, PartialEq, Eq)]
20pub(crate) struct HcConfig {
21 pub(crate) hash_log: usize,
22 pub(crate) chain_log: usize,
23 pub(crate) search_depth: usize,
24 pub(crate) target_len: usize,
25 /// Binary-tree finder hash width (upstream zstd `mls = BOUNDED(4, minMatch, 6)`),
26 /// carried explicitly per level so it is NOT inferred from `target_len`
27 /// (a `target_length` override must not silently flip the finder between
28 /// 5- and 4-byte hashing). Only the BT body reads it; HC/lazy levels keep
29 /// it at 4 (their `hash_position` is always 4-byte). 5 for the
30 /// minMatch=5 BT levels (btlazy2 + btopt L16), 4 elsewhere.
31 pub(crate) search_mls: usize,
32}
33
34#[derive(Copy, Clone, PartialEq, Eq)]
35pub(crate) struct RowConfig {
36 pub(crate) hash_bits: usize,
37 pub(crate) row_log: usize,
38 pub(crate) search_depth: usize,
39 pub(crate) target_len: usize,
40 /// Upstream zstd `cParams.minMatch` for the row matcher: the regular-search
41 /// acceptance floor (a row candidate must extend to >= `mls` bytes).
42 /// The C-like advanced API surfaces this as the row min-match knob.
43 /// `ROW_MIN_MATCH_LEN` (5) is the default; the row hash key width stays
44 /// 4 bytes (an internal detail), so this only tunes the acceptance
45 /// floor, not the candidate hash distribution.
46 pub(crate) mls: usize,
47}
48
49// Only used as the default HashChain config when the test-only parse×search
50// override pairs a level with a backend its native row doesn't populate.
51#[cfg(test)]
52pub(crate) const HC_CONFIG: HcConfig = HcConfig {
53 hash_log: HC_HASH_LOG,
54 chain_log: HC_CHAIN_LOG,
55 search_depth: HC_SEARCH_DEPTH,
56 target_len: HC_TARGET_LEN,
57 search_mls: 4,
58};
59
60/// Base HashChain config synthesized when a public-parameter strategy
61/// override ([`crate::encoding::parameters`]) routes a level to the HC / BT
62/// backend whose native level row didn't populate `hc` (e.g. forcing
63/// `Strategy::Lazy2` onto a level the table resolves to Fast). Mirrors
64/// the mid-band lazy defaults; the per-knob overrides then refine it.
65pub(crate) const HC_OVERRIDE_DEFAULT: HcConfig = HcConfig {
66 hash_log: crate::encoding::match_table::storage::HC_HASH_LOG,
67 chain_log: crate::encoding::match_table::storage::HC_CHAIN_LOG,
68 search_depth: HC_SEARCH_DEPTH,
69 target_len: HC_TARGET_LEN,
70 search_mls: 4,
71};
72
73pub(crate) const BTULTRA2_HC_CONFIG: HcConfig = HcConfig {
74 hash_log: 24,
75 chain_log: 24,
76 search_depth: 512,
77 target_len: 256,
78 search_mls: 4,
79};
80
81pub(crate) const BTULTRA2_HC_CONFIG_L22: HcConfig = HcConfig {
82 hash_log: 25,
83 chain_log: 27,
84 search_depth: 512,
85 target_len: 999,
86 search_mls: 4,
87};
88
89pub(crate) const BTULTRA2_HC_CONFIG_L22_256K: HcConfig = HcConfig {
90 hash_log: 19,
91 chain_log: 19,
92 search_depth: 1 << 13,
93 target_len: 999,
94 search_mls: 4,
95};
96
97pub(crate) const BTULTRA2_HC_CONFIG_L22_128K: HcConfig = HcConfig {
98 hash_log: 17,
99 chain_log: 18,
100 search_depth: 1 << 11,
101 target_len: 999,
102 search_mls: 4,
103};
104
105pub(crate) const BTULTRA2_HC_CONFIG_L22_16K: HcConfig = HcConfig {
106 hash_log: 15,
107 chain_log: 15,
108 search_depth: 1 << 10,
109 target_len: 999,
110 search_mls: 4,
111};
112
113// Default Row config: only used by tests and the test-only parse×search
114// override (production greedy L5 carries its own `ROW_L5`).
115#[cfg(test)]
116pub(crate) const ROW_CONFIG: RowConfig = RowConfig {
117 hash_bits: ROW_HASH_BITS,
118 row_log: ROW_LOG,
119 search_depth: ROW_SEARCH_DEPTH,
120 target_len: ROW_TARGET_LEN,
121 mls: ROW_MIN_MATCH_LEN,
122};
123
124// Level-5 greedy is the ONLY strategy routed to the Row backend
125// (`StrategyTag::backend`: greedy -> Row; lazy / btopt / btultra* ->
126// HashChain), so it is the only level whose `row:` field is read. The upstream zstd
127// `clevels.h` default row (srcSize > 256 KB) for level 5 is searchLog=3,
128// targetLength=2, from which the row matcher derives:
129// rowLog = clamp(searchLog, 4, 6) = 4
130// search_depth = 1 << min(searchLog, rowLog) = 8 (= nbAttempts)
131// target_len = targetLength = 2 (nice-match early-out)
132// The shared `ROW_CONFIG` (row_log=5, search_depth=16, target_len=48) ran a
133// level-12-grade search here: 16 slots per row, never early-exiting until a
134// 48-byte match. That exhaustive walk was the dominant cost in greedy L5's
135// encode-speed regression vs FFI. `hash_bits` matches upstream zstd's
136// `ZSTD_getCParams(5, .., 0).hashLog` = 19 (verified via
137// `cparams_check 5`), so the row table is the same width as upstream's
138// (2^19 slots); the previous `ROW_HASH_BITS` (20) doubled both row tables vs
139// upstream, the dominant peak-memory excess on the greedy band.
140pub(crate) const ROW_L5: RowConfig = RowConfig {
141 hash_bits: 19,
142 row_log: 4,
143 search_depth: 8,
144 target_len: 2,
145 mls: ROW_MIN_MATCH_LEN,
146};
147
148// Upstream zstd `clevels.h` unbounded defaults for the lazy band, verified via
149// `ZSTD_getCParams(level, 0, 0)`:
150// L6 { w21 c18 h19 s3 mml5 t4 lazy } → rowLog 4, depth 1<<3 = 8
151// L7 { w21 c19 h20 s4 mml5 t8 lazy } → rowLog 4, depth 16
152// L8 { w21 c19 h20 s4 mml5 t16 lazy2 } → rowLog 4, depth 16
153// L9 { w22 c20 h21 s4 mml5 t16 lazy2 } → rowLog 4, depth 16
154// L10 { w22 c21 h22 s5 mml5 t16 lazy2 } → rowLog 5, depth 32
155// L11 { w22 c21 h22 s6 mml5 t16 lazy2 } → rowLog 6, depth 64
156// L12 { w22 c22 h23 s6 mml5 t32 lazy2 } → rowLog 6, depth 64
157// `rowLog = clamp(searchLog, 4, 6)`, `depth = 1 << min(searchLog, rowLog)`
158// (same derivation as `ROW_L5` above). `hash_bits` carries the upstream zstd
159// `hashLog`; the hinted-source clamp in `configure` caps it by the window
160// exactly like the upstream zstd `ZSTD_adjustCParams` path.
161pub(crate) const ROW_L6: RowConfig = RowConfig {
162 hash_bits: 19,
163 row_log: 4,
164 search_depth: 8,
165 target_len: 4,
166 mls: ROW_MIN_MATCH_LEN,
167};
168pub(crate) const ROW_L7: RowConfig = RowConfig {
169 hash_bits: 20,
170 row_log: 4,
171 search_depth: 16,
172 target_len: 8,
173 mls: ROW_MIN_MATCH_LEN,
174};
175pub(crate) const ROW_L8: RowConfig = RowConfig {
176 hash_bits: 20,
177 row_log: 4,
178 search_depth: 16,
179 target_len: 16,
180 mls: ROW_MIN_MATCH_LEN,
181};
182pub(crate) const ROW_L9: RowConfig = RowConfig {
183 hash_bits: 21,
184 row_log: 4,
185 search_depth: 16,
186 target_len: 16,
187 mls: ROW_MIN_MATCH_LEN,
188};
189pub(crate) const ROW_L10: RowConfig = RowConfig {
190 hash_bits: 22,
191 row_log: 5,
192 search_depth: 32,
193 target_len: 16,
194 mls: ROW_MIN_MATCH_LEN,
195};
196pub(crate) const ROW_L11: RowConfig = RowConfig {
197 hash_bits: 22,
198 row_log: 6,
199 search_depth: 64,
200 target_len: 16,
201 mls: ROW_MIN_MATCH_LEN,
202};
203pub(crate) const ROW_L12: RowConfig = RowConfig {
204 hash_bits: 23,
205 row_log: 6,
206 search_depth: 64,
207 target_len: 32,
208 mls: ROW_MIN_MATCH_LEN,
209};
210
211/// Per-level Double-Fast hash sizing, mirroring the upstream zstd `clevels.h` columns
212/// (config-driven, not a hardcoded constant): `long_hash_log` =
213/// `cParams.hashLog` (the long 8-byte hash table), `short_hash_log` =
214/// `cParams.chainLog` (the short hash table dfast repurposes as its
215/// secondary index). Only the Dfast backend reads it, so non-dfast level
216/// rows carry `dfast: None`. `minMatch` stays the upstream zstd-fixed `5`
217/// (`DFAST_MIN_MATCH_LEN`, used in const contexts).
218#[derive(Copy, Clone, PartialEq, Eq)]
219pub(crate) struct DfastConfig {
220 pub(crate) long_hash_log: u8,
221 pub(crate) short_hash_log: u8,
222}
223
224// Upstream zstd clevels.h default row (srcSize > 256 KB): L3 {hashLog 17, chainLog 16},
225// L4 {hashLog 18, chainLog 18}.
226pub(crate) const DFAST_L3: DfastConfig = DfastConfig {
227 long_hash_log: 17,
228 short_hash_log: 16,
229};
230pub(crate) const DFAST_L4: DfastConfig = DfastConfig {
231 long_hash_log: 18,
232 short_hash_log: 18,
233};
234
235/// Per-level Fast-strategy tuning, only consumed by the `FastKernelMatcher`
236/// (Simple backend): `hash_log` = upstream zstd `cParams.hashLog`, `mls` = upstream zstd
237/// `cParams.minMatch` (4..=8), `step_size` = upstream zstd `stepSize`. Carried as
238/// `LevelParams.fast` (`Some` only on Fast level rows; `None` elsewhere).
239#[derive(Copy, Clone, PartialEq, Eq)]
240pub(crate) struct FastConfig {
241 pub(crate) hash_log: u32,
242 pub(crate) mls: u32,
243 pub(crate) step_size: usize,
244}
245
246pub(crate) const FAST_L1: FastConfig = FastConfig {
247 hash_log: 14,
248 // Tier-0 (srcSize > 256 KiB) `cParams.minMatch`. Upstream zstd selects the
249 // Level-1 row from a 4-way srcSize-tiered table (`ZSTD_getCParams_internal`
250 // → `ZSTD_defaultCParameters[tableID][1]`), and minMatch shrinks for
251 // smaller inputs: 7 (>256 KiB) / 6 (16..256 KiB) / 5 (<=16 KiB). The base
252 // here is the tier-0 value; `fast_l1_mls_for_source_size` lowers it per the
253 // tier in `adjust_params_for_source_size`.
254 mls: 7,
255 step_size: 2,
256};
257pub(crate) const FAST_L2: FastConfig = FastConfig {
258 hash_log: 16,
259 mls: 6,
260 step_size: 2,
261};
262
263/// Resolved tuning parameters for a compression level. The
264/// [`StrategyTag`] is the single source of truth for the backend
265/// family and the compile-time strategy consts; the runtime
266/// [`BackendTag`] used by the driver dispatcher is derived via
267/// [`StrategyTag::backend`] so the two cannot drift.
268#[derive(Copy, Clone, PartialEq, Eq)]
269pub(crate) struct LevelParams {
270 pub(crate) strategy_tag: crate::encoding::strategy::StrategyTag,
271 /// Decoupled search-method axis. Independent of `strategy_tag`'s
272 /// parse half: a level can pair any parse (greedy / lazy depth via
273 /// `lazy_depth`) with any search backend here. Defaults to the
274 /// historical pairing (`strategy_tag.search()`) but is overridable
275 /// per level so the parse×search matrix can be swept and tuned.
276 pub(crate) search: crate::encoding::strategy::SearchMethod,
277 pub(crate) window_log: u8,
278 pub(crate) lazy_depth: u8,
279 /// Per-strategy tuning. Exactly one is `Some` on each level row, matching
280 /// `strategy_tag`'s backend, so the table self-documents which knobs a
281 /// level actually consumes (the others are `None`, not dead placeholders):
282 /// `fast` for the Fast/Simple backend, `dfast` for Double-Fast, `hc` for
283 /// the HashChain (lazy / btopt / btultra*) backend, `row` for the Row
284 /// (greedy L5) backend.
285 pub(crate) fast: Option<FastConfig>,
286 pub(crate) dfast: Option<DfastConfig>,
287 pub(crate) hc: Option<HcConfig>,
288 pub(crate) row: Option<RowConfig>,
289}
290
291impl LevelParams {
292 /// Backend family (storage variant) for the driver dispatcher.
293 /// Derived from the decoupled `search` axis so a level can route to
294 /// a different search backend than its `strategy_tag` historically
295 /// implied.
296 pub(crate) fn backend(&self) -> crate::encoding::strategy::BackendTag {
297 self.search.backend()
298 }
299
300 /// Parse mode derived from the decoupled `search` axis: the binary-tree
301 /// search path carries `ParseMode::Optimal`; every other search backend
302 /// derives greedy/lazy/lazy2 from `lazy_depth`. Reading `search` (not the
303 /// strategy tag) keeps the parse×search decoupling complete even when a
304 /// level whose tag is `Bt*` is overridden to a non-BT search backend.
305 pub(crate) fn parse(&self) -> crate::encoding::strategy::ParseMode {
306 match self.search {
307 crate::encoding::strategy::SearchMethod::BinaryTree => {
308 crate::encoding::strategy::ParseMode::Optimal
309 }
310 _ => crate::encoding::strategy::ParseMode::from_lazy_depth(self.lazy_depth),
311 }
312 }
313
314 /// Cheap fingerprint pre-splitter level (the C-like `blockSplitterLevel`):
315 /// the EFFECTIVE upstream `ZSTD_splitBlock` level that
316 /// `ZSTD_optimalBlockSize` dispatches, i.e. `splitLevels[strategy] - 2`
317 /// (clamped at 0), NOT the raw `splitLevels[]` value. `split_level == 0`
318 /// routes to the cheap from-borders heuristic; `1..=4` to byChunks with
319 /// internal sampling level `split_level - 1`. See the body for the
320 /// per-strategy tier table and why the raw-table mapping was wrong.
321 pub(crate) fn pre_split(&self) -> Option<u8> {
322 use crate::encoding::strategy::StrategyTag;
323 // Effective upstream `ZSTD_splitBlock` level = `splitLevels[strat] - 2`
324 // (clamped at 0). Upstream `splitLevels[] = {0,0,1,2,2,3,3,4,4,4}` then
325 // subtracts 2 before dispatch, so the byChunks sampling tier is two
326 // steps coarser than the raw table: greedy/lazy(d1)=0 (from-borders),
327 // lazy2/btlazy2=1 (byChunks rate 43), btopt+=2 (byChunks rate 11).
328 // An earlier version mirrored the RAW table AND bumped lazy2 to the
329 // rate-1 full scan (split 4) to dodge a periodic-input phantom-split —
330 // that ran the pre-splitter at up to 43x upstream's sampling cost
331 // (~87% of L9 encode time on the decode corpus). Per the drop-in
332 // contract ratio only needs to stay <= upstream, so matching upstream's
333 // sampling tier (and accepting upstream's identical over-split on
334 // periodic input) is the dominant large-input encode-speed win.
335 Some(match self.strategy_tag {
336 // splitLevels 0/1 -> 0: upstream does not pre-split fast/dfast at
337 // all; from-borders is the cheapest stand-in and rarely splits.
338 StrategyTag::Fast | StrategyTag::Dfast => 0,
339 // greedy / lazy(depth 1): splitLevels 2 -> 0 (from-borders).
340 StrategyTag::Greedy => 0,
341 StrategyTag::Lazy => {
342 if self.lazy_depth >= 2 {
343 1 // lazy2: splitLevels 3 -> 1 (byChunks rate 43)
344 } else {
345 0 // lazy depth 1: splitLevels 2 -> 0 (from-borders)
346 }
347 }
348 StrategyTag::Btlazy2 => 1, // splitLevels 3 -> 1 (byChunks rate 43)
349 StrategyTag::BtOpt | StrategyTag::BtUltra | StrategyTag::BtUltra2 => 2,
350 })
351 }
352}
353
354/// Apply the public-parameter per-knob overrides (#27) onto the
355/// level-resolved [`LevelParams`], in place. Runs in [`Matcher::reset`]
356/// after the level params are computed and before backend selection, so
357/// a strategy override re-routes the backend uniformly. An all-`None`
358/// override is a no-op the caller skips via
359/// [`crate::encoding::parameters::ParamOverrides::is_empty`], keeping the default
360/// level geometry byte-identical.
361pub(crate) fn apply_param_overrides(
362 params: &mut LevelParams,
363 ov: &crate::encoding::parameters::ParamOverrides,
364) {
365 use crate::encoding::strategy::SearchMethod;
366
367 // 1. Strategy override re-derives tag / search / lazy depth.
368 if let Some(strategy) = ov.strategy {
369 let tag = strategy.tag();
370 params.strategy_tag = tag;
371 params.search = tag.search();
372 params.lazy_depth = strategy.lazy_depth();
373 }
374
375 // 2. Ensure the active backend's config row exists (synthesize a
376 // default when a strategy override moved off the native row).
377 match params.search {
378 SearchMethod::Fast => {
379 params.fast.get_or_insert(FAST_L1);
380 }
381 SearchMethod::DoubleFast => {
382 params.dfast.get_or_insert(DFAST_L3);
383 }
384 SearchMethod::RowHash => {
385 params.row.get_or_insert(ROW_L5);
386 }
387 SearchMethod::HashChain | SearchMethod::BinaryTree => {
388 // A `Btlazy2` strategy override moved off a non-HC row needs the
389 // BT 5-byte finder hash (upstream zstd minMatch 5); other synthesized HC
390 // rows keep the 4-byte default. An explicit `min_match` override
391 // below refines this further.
392 params.hc.get_or_insert(HcConfig {
393 search_mls: if matches!(
394 params.strategy_tag,
395 crate::encoding::strategy::StrategyTag::Btlazy2
396 ) {
397 5
398 } else {
399 HC_OVERRIDE_DEFAULT.search_mls
400 },
401 ..HC_OVERRIDE_DEFAULT
402 });
403 }
404 }
405
406 // 3. window_log (bounds-checked at <= 30 by the builder).
407 if let Some(window_log) = ov.window_log {
408 params.window_log = window_log;
409 }
410
411 // 4. Per-backend numeric knobs map into the active config, mirroring
412 // the upstream zstd `cParams` -> matcher translation documented on each
413 // config struct.
414 match params.search {
415 SearchMethod::Fast => {
416 if let Some(fast) = params.fast.as_mut() {
417 if let Some(hash_log) = ov.hash_log {
418 fast.hash_log = hash_log;
419 }
420 if let Some(min_match) = ov.min_match {
421 fast.mls = min_match;
422 }
423 }
424 }
425 SearchMethod::DoubleFast => {
426 if let Some(dfast) = params.dfast.as_mut() {
427 // hashLog -> long table, chainLog -> short table (the
428 // dfast secondary index). Both bounds-checked <= 30, so
429 // the `u8` casts are lossless.
430 if let Some(hash_log) = ov.hash_log {
431 dfast.long_hash_log = hash_log as u8;
432 }
433 if let Some(chain_log) = ov.chain_log {
434 dfast.short_hash_log = chain_log as u8;
435 }
436 }
437 }
438 SearchMethod::RowHash => {
439 if let Some(row) = params.row.as_mut() {
440 // Row hash-table width override (mirrors dfast `long_hash_log`
441 // / hc `hash_log`). Row has no separate chain table — the
442 // per-row depth comes from `search_log` below — so only
443 // `hash_log` maps here; `chain_log` has no Row analogue.
444 if let Some(hash_log) = ov.hash_log {
445 row.hash_bits = hash_log as usize;
446 }
447 if let Some(search_log) = ov.search_log {
448 // Upstream zstd: rowLog = clamp(searchLog, 4, 6);
449 // nbAttempts = 1 << min(searchLog, rowLog).
450 let row_log = (search_log as usize).clamp(4, 6);
451 row.row_log = row_log;
452 row.search_depth = 1usize << (search_log as usize).min(row_log);
453 }
454 if let Some(target_length) = ov.target_length {
455 row.target_len = target_length as usize;
456 }
457 if let Some(min_match) = ov.min_match {
458 row.mls = min_match as usize;
459 }
460 }
461 }
462 SearchMethod::HashChain | SearchMethod::BinaryTree => {
463 if let Some(hc) = params.hc.as_mut() {
464 if let Some(hash_log) = ov.hash_log {
465 hc.hash_log = hash_log as usize;
466 }
467 if let Some(chain_log) = ov.chain_log {
468 hc.chain_log = chain_log as usize;
469 }
470 if let Some(search_log) = ov.search_log {
471 hc.search_depth = 1usize << search_log;
472 }
473 if let Some(target_length) = ov.target_length {
474 hc.target_len = target_length as usize;
475 }
476 if let Some(min_match) = ov.min_match {
477 // BT finder hash width, derived from cParams.minMatch exactly
478 // as upstream zstd: `mls = BOUNDED(3, cParams.minMatch, 6)`
479 // (zstd_opt.c:896 ZSTD_selectBtGetAllMatches). minMatch=3
480 // tiers hash on 3 bytes (btultra/btultra2 path). Only the BT
481 // body reads `search_mls`; HC/lazy hash on 4 bytes regardless.
482 hc.search_mls = (min_match as usize).clamp(3, 6);
483 }
484 }
485 }
486 }
487}
488
489/// Map the resolved runtime strategy to the upstream zstd LDM strategy ordinal
490/// (1..=9) that [`crate::encoding::ldm::params::LdmParams::adjust_for`] expects.
491/// The collapsed `Lazy` tag splits on `lazy_depth` (lazy = 4, lazy2 = 5).
492#[cfg(feature = "hash")]
493pub(crate) fn ldm_strategy_ordinal(
494 tag: crate::encoding::strategy::StrategyTag,
495 lazy_depth: u8,
496) -> u32 {
497 use crate::encoding::strategy::StrategyTag;
498 match tag {
499 StrategyTag::Fast => 1,
500 StrategyTag::Dfast => 2,
501 StrategyTag::Greedy => 3,
502 StrategyTag::Lazy => {
503 if lazy_depth >= 2 {
504 5
505 } else {
506 4
507 }
508 }
509 // Upstream zstd `ZSTD_btlazy2` ordinal.
510 StrategyTag::Btlazy2 => 6,
511 StrategyTag::BtOpt => 7,
512 StrategyTag::BtUltra => 8,
513 StrategyTag::BtUltra2 => 9,
514 }
515}
516
517/// `ceil(log2(size))` of a source-size hint, with a zero hint floored to
518/// [`MIN_WINDOW_LOG`]. This is the single quantization every hint-dependent
519/// matcher parameter is derived from: the window-log cap, the HC / Fast hash
520/// and chain widths, the Dfast / Row table widths, the L22 config buckets, and
521/// the Fast attach-vs-copy cutoff. Two hints sharing this value resolve to the
522/// identical matcher shape, which is why it (not the raw byte count) keys the
523/// primed-dictionary snapshot — see [`PrimedKey`]. Operates on the full `u64`
524/// so callers comparing a hint against a cutoff get the same bucketed decision
525/// here and at the driver, with no `as usize` truncation on 32-bit targets.
526pub(crate) fn source_size_ceil_log(size: u64) -> u8 {
527 if size == 0 {
528 MIN_WINDOW_LOG
529 } else {
530 (64 - (size - 1).leading_zeros()) as u8
531 }
532}
533
534/// Attach-vs-copy cutoff for the Fast strategy, as a ceil-log bucket: a hint at
535/// or below `2^this` (or unknown, `None`) ATTACHES the dictionary (a separate
536/// immutable table scanned in place via the borrowed dual-base kernel); a larger
537/// hint would COPY it into the live table.
538///
539/// We set this to `31` so every dictionary source up to 2 GiB attaches,
540/// diverging from upstream zstd's 8 KiB `ZSTD_shouldAttachDict` cutoff ON
541/// PURPOSE: upstream copy mode copies the small CDict TABLES into the cctx and
542/// still scans the input in place, but our flat-history copy path memmoves the
543/// whole INPUT into history every frame (profiled at 30% `__memmove` + 14%
544/// `__memset` on a reused 1 MiB dict encode). Attach mode scans the caller's
545/// input in place with the dict as a separate prefix base, so it is strictly
546/// faster for every frame size here (measured: 1 MiB dict frame 167 us -> 52 us,
547/// 0.42x of C; 10 KiB 20.4 us -> 4.4 us, 0.17x of C). The dual-base kernel
548/// carries `window_low`, so over-window inputs stay in-window and C-decodable.
549///
550/// `31` is also the largest bucket the borrowed kernel can attach: it stores
551/// virtual positions as `u32` (`cur_abs as u32`), so the maximum attached source
552/// `1 << 31` (plus the dict prefix) stays below `u32::MAX`; the next bucket `32`
553/// (4 GiB) would wrap that arithmetic. Sources past 2 GiB therefore fall back to
554/// copy mode — rare in practice, and the relative copy cost shrinks as the
555/// source grows. Per the drop-in-not-binary-parity contract, we make this match
556/// decision ourselves.
557/// Shared by `reset` (records the mode in the primed-snapshot key) and
558/// `prime_with_dictionary` (acts on it).
559pub(crate) const FAST_ATTACH_DICT_CUTOFF_LOG: u8 = 31;
560
561/// Largest dictionary region (bytes) the Fast attach path can index. The tagged
562/// dict table packs each position into `32 - DICT_TAG_BITS` (= 24) bits, so a
563/// region past `2^24` (16 MiB) would overflow the packed position. Dictionaries
564/// this large fall back to COPY mode, whose live table stores full `u32`
565/// positions and handles them. The size hint set on dict load equals the actual
566/// dict content length, so the attach-vs-copy decision (and the matching
567/// snapshot-key / epoch bits) can gate on it consistently at reset time.
568pub(crate) const MAX_FAST_ATTACH_DICT_REGION: usize = 1 << 24;
569
570/// Dfast counterpart of [`FAST_ATTACH_DICT_CUTOFF_LOG`]: upstream zstd
571/// `ZSTD_dictMatchState` attach cutoff for the double-fast strategy is 16 KiB
572/// (`2^14`), so small / unknown-size inputs ATTACH (separate immutable dict
573/// long+short tables + dual-probe in `start_matching_fast_loop`) and larger
574/// known-size inputs COPY (re-prime the dict into the live tables, where the
575/// dense scan matches it as window history). The attach build also self-gates
576/// on `use_fast_loop` inside `skip_matching_for_dict_attach` — only the
577/// fast-loop levels (L3 / Default / L0) carry the dual-probe.
578pub(crate) const DFAST_ATTACH_DICT_CUTOFF_LOG: u8 = 14;
579
580/// `ZSTD_dictMatchState` attach cutoff for the Row (greedy/lazy) strategy is
581/// 32 KiB (`2^15`, upstream zstd `attachDictSizeCutoffs`): small / unknown-size inputs
582/// ATTACH the dict into the separate immutable row index (bounded dual-probe in
583/// `row_candidate_rl`), larger known-size inputs dense-COPY into the live rows.
584pub(crate) const ROW_ATTACH_DICT_CUTOFF_LOG: u8 = 15;
585
586/// 32 KiB (`2^15`, upstream zstd `attachDictSizeCutoffs[ZSTD_lazy2]`): small /
587/// unknown-size inputs ATTACH the dict as a separate hash-chain dms (the dual
588/// search in `find_best_match` walks the live input chain + the dms), larger
589/// known-size inputs dense-COPY (merge the dict into the live chain and search
590/// the one combined chain).
591pub(crate) const HC_ATTACH_DICT_CUTOFF_LOG: u8 = 15;
592
593/// BT/optimal attach cutoff for `btlazy2` + `btopt`: 32 KiB (`2^15`, upstream
594/// zstd `attachDictSizeCutoffs[ZSTD_btlazy2]` == `[ZSTD_btopt]`). Small /
595/// unknown-size inputs ATTACH the dict as a separate DUBT dms; larger known-size
596/// inputs COPY the dict into the LIVE binary tree (upstream zstd
597/// `ZSTD_resetCCtx_byCopyingCDict`).
598pub(crate) const BT_OPT_ATTACH_DICT_CUTOFF_LOG: u8 = 15;
599
600/// BT/optimal attach cutoff for `btultra` + `btultra2`: 8 KiB (`2^13`, upstream
601/// zstd `attachDictSizeCutoffs[ZSTD_btultra]` == `[ZSTD_btultra2]`). The deepest
602/// parses copy the dict into the live tree past a much smaller source than the
603/// `btopt` tier, matching upstream's per-strategy cutoff table.
604pub(crate) const BT_ULTRA_ATTACH_DICT_CUTOFF_LOG: u8 = 13;
605
606// Source-size cap for the dfast hash bits when a size hint is present: a tiny
607// input needs no larger hash than its window. The upstream zstd `cParams.hashLog` /
608// `chainLog` (from `DfastConfig`) caps it from above at the call site.
609pub(crate) fn dfast_hash_bits_for_window(max_window_size: usize) -> usize {
610 let window_log = (usize::BITS - 1 - max_window_size.leading_zeros()) as usize;
611 window_log.max(MIN_WINDOW_LOG as usize)
612}
613
614pub(crate) fn row_hash_bits_for_window(max_window_size: usize) -> usize {
615 // Upstream zstd `ZSTD_adjustCParams_internal` cap: `hashLog <= windowLog + 1`.
616 // The `+ 1` is load-bearing for L12, whose upstream zstd hashLog (23) exceeds
617 // its windowLog (22) — a plain `windowLog` cap would shrink the L12
618 // table on EVERY hinted reset and split primed snapshots between
619 // hinted and unhinted frames that resolve to the identical geometry.
620 // No constant upper clamp: the old `ROW_HASH_BITS` (20) ceiling
621 // predates the lazy band moving onto Row (L9-12 carry upstream zstd hashLog
622 // 21-23).
623 let window_log = (usize::BITS - 1 - max_window_size.leading_zeros()) as usize;
624 (window_log + 1).max(MIN_WINDOW_LOG as usize)
625}
626
627/// `floor(log2(window))` for the HashChain table-log cap (upstream zstd
628/// `ZSTD_adjustCParams_internal`). The caller clamps the level's `hash_log` /
629/// `chain_log` from above with this so a small hinted input doesn't allocate the
630/// full level's tables.
631pub(crate) fn hc_hash_bits_for_window(max_window_size: usize) -> usize {
632 let window_log = (usize::BITS - 1 - max_window_size.leading_zeros()) as usize;
633 window_log.max(MIN_WINDOW_LOG as usize)
634}
635
636/// Parameter table for numeric compression levels 1–22.
637///
638/// Each entry maps a zstd compression level to the best-available matcher
639/// backend and tuning knobs. High levels map to dedicated parse modes:
640/// btopt (16-17), btultra (18), btultra2 (19-22) — matching upstream zstd
641/// `clevels.h` (level 19 is `ZSTD_btultra2`, not plain btultra).
642///
643/// Index 0 = level 1, index 21 = level 22.
644#[rustfmt::skip]
645pub(crate) const LEVEL_TABLE: [LevelParams; 22] = [
646 // Exactly one of fast/dfast/hc/row is Some per row, matching the strategy
647 // backend; the rest are None (not dead placeholders).
648 // Lvl Strategy wlog lazy per-strategy config
649 // --- -------------- ---- ---- -------------------
650 /* 1 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Fast, search: crate::encoding::strategy::SearchMethod::Fast, window_log: 19, lazy_depth: 0, fast: Some(FAST_L1), dfast: None, hc: None, row: None },
651 /* 2 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Fast, search: crate::encoding::strategy::SearchMethod::Fast, window_log: 20, lazy_depth: 0, fast: Some(FAST_L2), dfast: None, hc: None, row: None },
652 /* 3 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Dfast, search: crate::encoding::strategy::SearchMethod::DoubleFast, window_log: 21, lazy_depth: 1, fast: None, dfast: Some(DFAST_L3), hc: None, row: None },
653 /* 4 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Dfast, search: crate::encoding::strategy::SearchMethod::DoubleFast, window_log: 21, lazy_depth: 1, fast: None, dfast: Some(DFAST_L4), hc: None, row: None },
654 // target_len column for L5..=L15 matches upstream zstd cParams.targetLength
655 // from clevels.h table[0] (default — srcSize > 256 KB). Upstream zstd uses
656 // it as the lazy outer loop's `sufficient_len` (nice-match) threshold.
657 // Inflating it above upstream zstd forces the chain walk to complete
658 // search_depth iterations instead of breaking on the first
659 // long-enough match — the dominant cost in the L5..=L15 speed
660 // regression vs FFI (see lazy_band_target_len_matches_default_table).
661 /* 5 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Greedy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 0, fast: None, dfast: None, hc: None, row: Some(ROW_L5) },
662 // L6-12: the upstream zstd runs the lazy/lazy2 strategies on the ROW-based
663 // match finder by default (`ZSTD_resolveRowMatchFinderMode`: row mode
664 // is on for greedy..lazy2 whenever SIMD is available) — a bounded
665 // SIMD tag scan per row instead of a pointer-chasing hash-chain walk.
666 // Our HashChain walk on these levels was ~75% of L10 wall time on the
667 // 1 MiB corpus (dependent chain-table loads). Same `RowConfig`
668 // derivation as `ROW_L5` above, upstream zstd values per level in the
669 // `ROW_L6..ROW_L12` comment block.
670 /* 6 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 1, fast: None, dfast: None, hc: None, row: Some(ROW_L6) },
671 /* 7 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 1, fast: None, dfast: None, hc: None, row: Some(ROW_L7) },
672 /* 8 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L8) },
673 /* 9 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L9) },
674 /*10 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L10) },
675 /*11 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L11) },
676 /*12 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Lazy, search: crate::encoding::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L12) },
677 // L13-15: reference uses btlazy2 (binary-tree finder) with searchLog 4/5/6
678 // (search_depth 16/32/64) and targetLength 32. We run the hash-chain Lazy
679 // parser here, so we mirror the reference search budget rather than inflate
680 // it: matching the table keeps speed near the reference and makes per-level
681 // perf divergences comparable. The binary-tree finder that would let a
682 // smaller searchLog find longer matches (and re-establish a strict ratio
683 // ladder above L12) is tracked separately; until it lands these levels sit
684 // close to L12 on hash-chain inputs by design.
685 /*13 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Btlazy2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 22, search_depth: 16, target_len: 32, search_mls: 5 }), row: None },
686 /*14 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Btlazy2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 23, chain_log: 22, search_depth: 32, target_len: 32, search_mls: 5 }), row: None },
687 /*15 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::Btlazy2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 23, chain_log: 23, search_depth: 64, target_len: 32, search_mls: 5 }), row: None },
688 /*16 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtOpt, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 22, search_depth: 32, target_len: 48, search_mls: 5 }), row: None },
689 /*17 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtOpt, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 23, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 23, search_depth: 32, target_len: 64, search_mls: 4 }), row: None },
690 /*18 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtUltra, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 23, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 23, search_depth: 64, target_len: 64, search_mls: 4 }), row: None },
691 /*19 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtUltra2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 23, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 24, search_depth: 128, target_len: 256, search_mls: 4 }), row: None },
692 /*20 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtUltra2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 25, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 23, chain_log: 25, search_depth: 128, target_len: 256, search_mls: 4 }), row: None },
693 /*21 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtUltra2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 26, lazy_depth: 2, fast: None, dfast: None, hc: Some(BTULTRA2_HC_CONFIG), row: None },
694 /*22 */ LevelParams { strategy_tag: crate::encoding::strategy::StrategyTag::BtUltra2, search: crate::encoding::strategy::SearchMethod::BinaryTree, window_log: 27, lazy_depth: 2, fast: None, dfast: None, hc: Some(BTULTRA2_HC_CONFIG_L22), row: None },
695];
696
697/// Upstream `ZSTD_createCDict` table geometry: the `(hash_log, chain_log)` a
698/// dictionary's prepared match-finder tables get. Thin adapter over the single
699/// cParams source [`crate::encoding::cparams::create_cdict_table_logs`], which mirrors
700/// `ZSTD_adjustCParams_internal` under `ZSTD_cpm_createCDict`. `window_log` is
701/// the resolved compress window; `hash_log` / `chain_log` are the level's own
702/// widths; `uses_bt` selects the binary-tree `cycleLog` (`chainLog - 1`).
703pub(crate) fn cdict_table_logs(
704 window_log: u8,
705 hash_log: usize,
706 chain_log: usize,
707 uses_bt: bool,
708 dict_size: usize,
709) -> (usize, usize) {
710 let (h, c) = crate::encoding::cparams::create_cdict_table_logs(
711 window_log,
712 hash_log as u32,
713 chain_log as u32,
714 uses_bt,
715 dict_size,
716 );
717 (h as usize, c as usize)
718}
719
720/// Smallest window_log the encoder will use regardless of source size.
721pub(crate) const MIN_WINDOW_LOG: u8 = 10;
722/// Conservative floor for source-size-hinted window tuning.
723///
724/// Hinted windows below 16 KiB (`window_log < 14`) currently regress C-FFI
725/// interoperability on certain compressed-block patterns. Keep hinted
726/// windows at 16 KiB or larger until that compatibility gap is closed.
727pub(crate) const MIN_HINTED_WINDOW_LOG: u8 = 14;
728
729/// Adjust level parameters for a known source size.
730///
731/// This derives a cap from `ceil(log2(src_size))`, then clamps it to
732/// [`MIN_HINTED_WINDOW_LOG`] (16 KiB). A zero-byte size hint is treated as
733/// [`MIN_WINDOW_LOG`] for the raw ceil-log step and then promoted to the hinted
734/// floor. This keeps tables bounded for small inputs while preserving the
735/// encoder's baseline minimum supported window.
736/// For the HC backend, `hash_log` and `chain_log` are reduced
737/// proportionally.
738/// Source-size tier index, matching upstream `ZSTD_getCParams_internal`'s
739/// `tableID = (rSize<=256K)+(rSize<=128K)+(rSize<=16K)`: 0 = > 256 KiB or
740/// unknown, 1 = 128..256 KiB, 2 = 16..128 KiB, 3 = <= 16 KiB.
741fn cparams_tier(source_size: Option<u64>) -> usize {
742 match source_size {
743 Some(size) if size <= 16 * 1024 => 3,
744 Some(size) if size <= 128 * 1024 => 2,
745 Some(size) if size <= 256 * 1024 => 1,
746 _ => 0,
747 }
748}
749
750/// Override a Fast (L1/L2) or Dfast (L3) level row's table-shaping cParams
751/// (hashLog / chainLog / minMatch) by source-size tier, matching the
752/// reference `ZSTD_defaultCParameters[tableID][level]`. L1 keeps its base
753/// hashLog (the source-size window clamp in `adjust_params_for_source_size`
754/// already lands on the reference value) and only tiers minMatch; L2 also
755/// tiers hashLog (the tier-0 value 16 oversized the table on medium inputs,
756/// the page-fault pathology); L3 tiers both dfast hash widths. Strategy
757/// switches (L2 tier 1, L4) are intentionally not applied here.
758/// Translate a verbatim upstream `ZSTD_defaultCParameters[tier][level]` row
759/// (`cparams::CParams`) into our resolved [`LevelParams`], reproducing
760/// upstream's cParams -> matcher-config derivation so the encoder follows C's
761/// source-size-tiered STRATEGY + table widths rather than a single hand-tuned
762/// `LEVEL_TABLE`. Derivation (verified against L6/L16/L22 vs clevels.h):
763/// `search_depth = 1 << searchLog`; row `row_log = clamp(searchLog, 4, 6)`; the
764/// per-strategy sub-config carries the verbatim `hashLog` / `chainLog` /
765/// `targetLength` / `minMatch`. Strategy numbers are upstream `ZSTD_strategy`
766/// (fast=1, dfast=2, greedy=3, lazy=4, lazy2=5, btlazy2=6, btopt=7, btultra=8,
767/// btultra2=9).
768fn level_params_from_cparams(cp: crate::encoding::cparams::CParams) -> LevelParams {
769 use crate::encoding::strategy::{SearchMethod, StrategyTag};
770 let window_log = cp.window_log as u8;
771 let search_depth = 1usize << cp.search_log;
772 let target_len = cp.target_length as usize;
773 let hc = HcConfig {
774 hash_log: cp.hash_log as usize,
775 chain_log: cp.chain_log as usize,
776 search_depth,
777 target_len,
778 search_mls: cp.min_match as usize,
779 };
780 let row = RowConfig {
781 hash_bits: cp.hash_log as usize,
782 row_log: cp.search_log.clamp(4, 6) as usize,
783 search_depth,
784 target_len,
785 mls: cp.min_match as usize,
786 };
787 let bt = |tag| LevelParams {
788 strategy_tag: tag,
789 search: SearchMethod::BinaryTree,
790 window_log,
791 lazy_depth: 2,
792 fast: None,
793 dfast: None,
794 hc: Some(hc),
795 row: None,
796 };
797 let row_lvl = |tag, lazy_depth| LevelParams {
798 strategy_tag: tag,
799 search: SearchMethod::RowHash,
800 window_log,
801 lazy_depth,
802 fast: None,
803 dfast: None,
804 hc: None,
805 row: Some(row),
806 };
807 match cp.strategy {
808 1 => LevelParams {
809 strategy_tag: StrategyTag::Fast,
810 search: SearchMethod::Fast,
811 window_log,
812 lazy_depth: 0,
813 // Upstream fast `stepSize`: `targetLength + 1` (0 -> 1, so step 2).
814 fast: Some(FastConfig {
815 hash_log: cp.hash_log,
816 mls: cp.min_match,
817 step_size: target_len.max(1) + 1,
818 }),
819 dfast: None,
820 hc: None,
821 row: None,
822 },
823 2 => LevelParams {
824 strategy_tag: StrategyTag::Dfast,
825 search: SearchMethod::DoubleFast,
826 window_log,
827 lazy_depth: 1,
828 fast: None,
829 dfast: Some(DfastConfig {
830 long_hash_log: cp.hash_log as u8,
831 short_hash_log: cp.chain_log as u8,
832 }),
833 hc: None,
834 row: None,
835 },
836 3 => row_lvl(StrategyTag::Greedy, 0),
837 4 => row_lvl(StrategyTag::Lazy, 1),
838 5 => row_lvl(StrategyTag::Lazy, 2),
839 6 => bt(StrategyTag::Btlazy2),
840 7 => bt(StrategyTag::BtOpt),
841 8 => bt(StrategyTag::BtUltra),
842 _ => bt(StrategyTag::BtUltra2),
843 }
844}
845
846fn apply_cparams_tier(level: i32, source_size: Option<u64>, p: &mut LevelParams) {
847 let tier = cparams_tier(source_size);
848 // Single source for the table data: the verbatim upstream
849 // `ZSTD_defaultCParameters[tier][level]` row (`cparams::default_cparams`).
850 // The encoder consumes only the table-shaping widths here; the window /
851 // `table_log` clamp lives in `adjust_params_for_source_size`.
852 match level {
853 // Fast, all tiers — minMatch only (hashLog handled by the window clamp).
854 1 => {
855 if let Some(f) = p.fast.as_mut() {
856 f.mls = crate::encoding::cparams::default_cparams(tier, 1).min_match;
857 }
858 }
859 // Fast (base strategy; tier 1 is dfast upstream — not switched here).
860 2 => {
861 if let Some(f) = p.fast.as_mut() {
862 let cp = crate::encoding::cparams::default_cparams(tier, 2);
863 f.hash_log = cp.hash_log;
864 f.mls = cp.min_match;
865 }
866 }
867 // Dfast, all tiers — long hashLog (`hash_log`) + short chainLog (`chain_log`).
868 3 => {
869 if let Some(d) = p.dfast.as_mut() {
870 let cp = crate::encoding::cparams::default_cparams(tier, 3);
871 d.long_hash_log = cp.hash_log as u8;
872 d.short_hash_log = cp.chain_log as u8;
873 }
874 }
875 _ => {}
876 }
877}
878
879pub(crate) fn adjust_params_for_source_size(mut params: LevelParams, src_size: u64) -> LevelParams {
880 // Derive a source-size-based cap from ceil(log2(src_size)), then
881 // clamp first to MIN_WINDOW_LOG (baseline encoder minimum) and then to
882 // MIN_HINTED_WINDOW_LOG (16 KiB hinted floor). For tiny or zero hints we
883 // therefore keep a 16 KiB effective minimum window in hinted mode.
884 // Raw ceil(log2(src_size)) drives the internal table sizes. The
885 // advertised `window_log` is separately floored at MIN_HINTED_WINDOW_LOG
886 // (a decoder-interop requirement on the wire format), but the hash /
887 // chain table widths are internal and never appear in the frame, so they
888 // can track the actual source size below that floor.
889 let raw_src_log = source_size_ceil_log(src_size);
890 let src_log = raw_src_log.max(MIN_WINDOW_LOG).max(MIN_HINTED_WINDOW_LOG);
891 if src_log < params.window_log {
892 params.window_log = src_log;
893 }
894 // Internal match-finder tables are sized from `table_log` — the RAW
895 // source log (floored only at the baseline `MIN_WINDOW_LOG`), NOT the
896 // wire `window_log` floor. The table widths never appear in the frame, so
897 // for small inputs they can track the actual source size and avoid
898 // zeroing a window-sized table per frame; large inputs keep the level's
899 // widths. The cap is applied with the same per-backend headroom the
900 // level table uses, so the load factor (and match quality) is unchanged.
901 // The Dfast backend derives its table widths from the source in `reset`
902 // (`set_hash_bits` recomputes there), so it is not adjusted here. The Row
903 // backend's width IS capped here, mirroring the upstream zstd (see the Row branch).
904 let table_log = raw_src_log.max(MIN_WINDOW_LOG);
905 let backend = params.backend();
906 if backend == crate::encoding::strategy::BackendTag::HashChain {
907 let hc = params
908 .hc
909 .as_mut()
910 .expect("HashChain level row carries an HcConfig");
911 if (table_log + 2) < hc.hash_log as u8 {
912 hc.hash_log = (table_log + 2) as usize;
913 }
914 if (table_log + 1) < hc.chain_log as u8 {
915 hc.chain_log = (table_log + 1) as usize;
916 }
917 } else if backend == crate::encoding::strategy::BackendTag::Row {
918 let row = params
919 .row
920 .as_mut()
921 .expect("Row level row carries a RowConfig");
922 // Upstream zstd `ZSTD_adjustCParams_internal` (zstd_compress.c): once
923 // the window is source-capped, `hashLog <= windowLog + 1`. The row
924 // table is `2^hash_bits` slots, exactly upstream's row hashTable
925 // `2^hashLog` slots, so the same cap applies. Without it the row table
926 // stays at the level's unbounded width (e.g. L12 hash_bits 23 = 4x
927 // upstream's source-capped 21), the dominant peak-memory excess on the
928 // row band.
929 let row_cap = (table_log + 1) as usize;
930 if row_cap < row.hash_bits {
931 row.hash_bits = row_cap;
932 }
933 } else if backend == crate::encoding::strategy::BackendTag::Simple {
934 let fast = params
935 .fast
936 .as_mut()
937 .expect("Fast level row carries a FastConfig");
938 let fast_cap = (table_log + 1) as u32;
939 if fast_cap < fast.hash_log {
940 fast.hash_log = fast_cap;
941 }
942 }
943 params
944}
945
946fn level22_btultra2_params_for_source_size(source_size: Option<u64>) -> LevelParams {
947 let mut hc = match source_size {
948 Some(size) if size <= 16 * 1024 => BTULTRA2_HC_CONFIG_L22_16K,
949 Some(size) if size <= 128 * 1024 => BTULTRA2_HC_CONFIG_L22_128K,
950 Some(size) if size <= 256 * 1024 => BTULTRA2_HC_CONFIG_L22_256K,
951 _ => BTULTRA2_HC_CONFIG_L22,
952 };
953 let mut window_log = match source_size {
954 Some(size) if size <= 16 * 1024 => 14,
955 Some(size) if size <= 128 * 1024 => 17,
956 Some(size) if size <= 256 * 1024 => 18,
957 _ => 27,
958 };
959 if let Some(size) = source_size
960 && size > 256 * 1024
961 {
962 let src_log = source_size_ceil_log(size);
963 window_log = window_log.min(src_log.max(MIN_WINDOW_LOG));
964 let adjusted_table_log = window_log as usize + 1;
965 hc.hash_log = hc.hash_log.min(adjusted_table_log);
966 hc.chain_log = hc.chain_log.min(adjusted_table_log);
967 }
968 LevelParams {
969 strategy_tag: crate::encoding::strategy::StrategyTag::BtUltra2,
970 search: crate::encoding::strategy::SearchMethod::BinaryTree,
971 window_log,
972 lazy_depth: 2,
973 fast: None,
974 dfast: None,
975 hc: Some(hc),
976 row: None,
977 }
978}
979
980/// Estimated steady-state heap footprint of a one-shot compression context
981/// at `level` (window history + match-finder tables + block staging), in
982/// bytes. Computed from the same per-level tuning table the encoder
983/// resolves at frame start, so the estimate tracks the real allocations;
984/// it is an upper-bound style budget figure, not an exact accounting.
985pub fn estimated_compression_workspace_bytes(level: CompressionLevel) -> usize {
986 use crate::encoding::strategy::StrategyTag;
987 let params = resolve_level_params(level, None);
988 let window = 1usize << params.window_log;
989 // Mirror `configure()`: the HC3 short-match side table exists only on
990 // the btultra/btultra2 tags (minMatch 3), capped by the window log; the
991 // BT pointer-pair layout fits inside the `4 << chain_log` chain term
992 // (pairs over `chain_log - 1` nodes).
993 let wants_hash3 = matches!(
994 params.strategy_tag,
995 StrategyTag::BtUltra | StrategyTag::BtUltra2
996 );
997 let uses_bt = matches!(
998 params.strategy_tag,
999 StrategyTag::Btlazy2 | StrategyTag::BtOpt | StrategyTag::BtUltra | StrategyTag::BtUltra2
1000 );
1001 let tables = params.fast.map(|f| 4usize << f.hash_log).unwrap_or(0)
1002 + params
1003 .dfast
1004 .map(|d| (4usize << d.long_hash_log) + (4usize << d.short_hash_log))
1005 .unwrap_or(0)
1006 + params
1007 .hc
1008 .map(|h| {
1009 let hash3 = if wants_hash3 {
1010 4usize
1011 << crate::encoding::match_table::storage::HC3_HASH_LOG
1012 .min(params.window_log as usize)
1013 } else {
1014 0
1015 };
1016 (4usize << h.hash_log) + (4usize << h.chain_log) + hash3
1017 })
1018 .unwrap_or(0)
1019 + params
1020 .row
1021 .map(|r| (4usize << r.hash_bits) + (2usize << r.hash_bits))
1022 .unwrap_or(0);
1023 // BT modes box a `BtMatcher`; its retained scratch layout is budgeted
1024 // next to the struct so estimator and allocator evolve together.
1025 let bt = if uses_bt {
1026 crate::encoding::bt::BtMatcher::estimated_workspace_bytes()
1027 } else {
1028 0
1029 };
1030 // Block staging: literal + sequence buffers plus the compressed-block
1031 // scratch, each bounded by the 128 KiB block size.
1032 let staging = 3 * (128 * 1024);
1033 window + tables + bt + staging
1034}
1035
1036/// Extra steady-state workspace the binary-tree strategies (ordinals 6..=9,
1037/// btlazy2..btultra2) retain beyond the hash/chain tables: the boxed matcher
1038/// plus its scratch arenas, and the HC3 short-match side table for
1039/// btultra/btultra2 (capped by the window log). 0 for non-BT ordinals.
1040pub fn estimated_bt_strategy_extra_bytes(strategy_ordinal: u32, window_log: u32) -> usize {
1041 if !(6..=9).contains(&strategy_ordinal) {
1042 return 0;
1043 }
1044 let hash3 = if matches!(strategy_ordinal, 8 | 9) {
1045 4usize << crate::encoding::match_table::storage::HC3_HASH_LOG.min(window_log as usize)
1046 } else {
1047 0
1048 };
1049 crate::encoding::bt::BtMatcher::estimated_workspace_bytes() + hash3
1050}
1051
1052/// Resolve a [`CompressionLevel`] (+ optional source-size hint) to the
1053/// concrete [`LevelParams`] the matcher runs: strategy tag, search method
1054/// (match-finder), window log, and per-backend config.
1055///
1056/// ## CRITICAL: input size changes the match-finder (and can change strategy)
1057///
1058/// The resolved geometry is a function of the SOURCE SIZE, not the level
1059/// alone. This is the easy-to-miss part (so read this before assuming a level
1060/// maps to one fixed match-finder). It mirrors three upstream zstd stages:
1061///
1062/// 1. [`LEVEL_TABLE`] holds the tier-0 (source > 256 KiB) base row per level
1063/// (upstream `ZSTD_defaultCParameters[0]`). L6-L12 carry
1064/// `SearchMethod::RowHash` (the Row match-finder), like upstream's
1065/// greedy/lazy default.
1066/// 2. [`apply_cparams_tier`] overrides the table-shaping widths for the
1067/// smaller source tiers (upstream `ZSTD_getCParams_internal` tier table).
1068/// NOTE: upstream ALSO switches STRATEGY in some tiers (L2 → dfast, L4 →
1069/// greedy on small sources); those backend switches are NOT yet replicated,
1070/// so those levels keep their base strategy on small inputs.
1071/// 3. [`adjust_params_for_source_size`] caps `window_log` to
1072/// ~`ceil_log2(source_size)` (upstream `ZSTD_adjustCParams_internal`).
1073///
1074/// THEN, in the matcher `reset`, the greedy/lazy band falls back from
1075/// `RowHash` to `SearchMethod::HashChain` when the resolved `window_log <= 14`
1076/// — exactly upstream's `ZSTD_resolveRowMatchFinderMode` (the Row match-finder
1077/// is used for greedy/lazy/lazy2 ONLY when `windowLog > 14`). Net effect for
1078/// the SAME level:
1079///
1080/// * small input (e.g. a 10 KiB fixture → `window_log` 14) → **HashChain**
1081/// (`ZSTD_HcFindBestMatch`, scalar chain walk);
1082/// * large input (e.g. 1 MiB → `window_log` 20) → **RowHash** (the SIMD-tag
1083/// row match-finder).
1084///
1085/// A dictionary does NOT change the match-finder: it only downsizes the
1086/// prepared tables (`cdict_table_logs`, mirroring `ZSTD_createCDict`'s
1087/// small-source assumption), while `window_log` stays source-derived. So
1088/// `(L6, 10 KiB, +dict)` is HashChain and `(L6, 1 MiB, +dict)` is RowHash,
1089/// both matching upstream. When comparing against C on a fixture, resolve the
1090/// match-finder from the fixture's size first, or you may optimise/benchmark a
1091/// path C does not even take for that input.
1092pub(crate) fn resolve_level_params(
1093 level: CompressionLevel,
1094 source_size: Option<u64>,
1095) -> LevelParams {
1096 // Levels at or above MAX_LEVEL clamp to the max: route every out-of-range
1097 // level through the same btultra2 source-size resolver as Level(22), so a
1098 // caller passing Level(23+) gets the max config instead of falling through
1099 // to the generic cParams path (which would resolve a different geometry).
1100 if matches!(level, CompressionLevel::Level(n) if n >= CompressionLevel::MAX_LEVEL) {
1101 return level22_btultra2_params_for_source_size(source_size);
1102 }
1103 let params = match level {
1104 CompressionLevel::Uncompressed => LevelParams {
1105 strategy_tag: crate::encoding::strategy::StrategyTag::Fast,
1106 search: crate::encoding::strategy::SearchMethod::Fast,
1107 // Uncompressed frames emit raw blocks and never reference
1108 // history; advertising a larger window only inflates
1109 // decoder-side buffer reservation. Stay at 17 (128 KiB).
1110 window_log: 17,
1111 lazy_depth: 0,
1112 // Beyond-upstream zstd: hash_log=14 (vs upstream zstd's 13) for 2× fewer
1113 // collisions on structured corpora. Upstream zstd's "base for negative"
1114 // row has targetLength=1 → step_size = 1 + 0 + 1 = 2.
1115 fast: Some(FastConfig {
1116 hash_log: 14,
1117 mls: 6,
1118 step_size: 2,
1119 }),
1120 dfast: None,
1121 hc: None,
1122 row: None,
1123 },
1124 CompressionLevel::Fastest => {
1125 // Only the Fast-specific cParams
1126 // (fast_hash_log / fast_mls / fast_step_size) align
1127 // with Uncompressed / negative-base row. window_log
1128 // stays at LEVEL_TABLE[0]'s value (19) — Fastest still
1129 // does real compression on a full window, unlike
1130 // Uncompressed which clamps to 17.
1131 let mut p = LEVEL_TABLE[0];
1132 p.fast = Some(FastConfig {
1133 hash_log: 14,
1134 mls: 6,
1135 step_size: 2,
1136 });
1137 p
1138 }
1139 CompressionLevel::Default => {
1140 // Default == Level(DEFAULT_LEVEL); tier it the same way an explicit
1141 // positive level is, so hinted default compression shrinks its
1142 // table widths on small / medium frames instead of keeping the
1143 // tier-0 row (the oversized-table page-fault pathology).
1144 let mut p = LEVEL_TABLE[CompressionLevel::DEFAULT_LEVEL as usize - 1];
1145 apply_cparams_tier(CompressionLevel::DEFAULT_LEVEL, source_size, &mut p);
1146 p
1147 }
1148 CompressionLevel::Better => LEVEL_TABLE[6],
1149 // Level 13: the first dominant point of the deep-lazy band. The
1150 // mls-wide row key lifted the shallow band's ratio enough that
1151 // level 11 no longer strictly beats level 7 on the ladder corpus;
1152 // the `Best` alias belongs on a config that dominates everything
1153 // below it rather than on a hair-thin margin.
1154 CompressionLevel::Best => LEVEL_TABLE[12],
1155 CompressionLevel::Level(n) => {
1156 if n > 0 {
1157 // Source-size-tiered cParams, derived verbatim from upstream
1158 // `ZSTD_defaultCParameters[tableID][level]` (the same 4-way table
1159 // C selects via `ZSTD_getCParams_internal`). This follows C's
1160 // per-(level, srcSize) STRATEGY + table widths, not a single
1161 // hand-tuned `LEVEL_TABLE` row: small / medium frames now ramp to
1162 // the reference strategy (e.g. btopt at L11 for <= 16 KiB) instead
1163 // of staying on the tier-0 backend.
1164 let tier = cparams_tier(source_size);
1165 let lvl = (n as usize).min(CompressionLevel::MAX_LEVEL as usize);
1166 level_params_from_cparams(crate::encoding::cparams::default_cparams(tier, lvl))
1167 } else if n == 0 {
1168 // Level 0 = default, matching C zstd semantics. Tier it like the
1169 // `Default` alias so `Level(0)` and `Default` stay identical.
1170 let mut p = LEVEL_TABLE[CompressionLevel::DEFAULT_LEVEL as usize - 1];
1171 apply_cparams_tier(CompressionLevel::DEFAULT_LEVEL, source_size, &mut p);
1172 p
1173 } else {
1174 // Negative levels — upstream zstd sets
1175 // targetLength = -level (clampedCompressionLevel),
1176 // yielding step_size = (-level) + 1 since
1177 // !(targetLength) = 0 when targetLength > 0.
1178 // So L-1..L-7 get step_size 2..8. Acceleration
1179 // gradient comes from larger step skipping more
1180 // positions per iter (faster, worse ratio).
1181 // Clamp to upstream zstd's MIN_LEVEL before negating so
1182 // i32::MIN can't overflow on `-n`.
1183 let clamped = n.max(CompressionLevel::MIN_LEVEL);
1184 let target_length = (-clamped) as usize;
1185 let step_size = target_length + 1;
1186 // Upstream zstd row-0 ("base for negative", clevels.h srcSize>256KB):
1187 // hashLog=13, minMatch=7. The 32 KiB hash table (2^13 * 4B)
1188 // is L1d-resident on contemporary cores, so every probe is an
1189 // L1 hit; hashLog=14 (64 KiB) overflows a 32 KiB L1d and turns
1190 // each probe into an L2 access. minMatch=7 (vs 6) skips
1191 // short-distance 6-byte matches: fewer sequences, less
1192 // extension/emit work, and parity with the upstream zstd's negative
1193 // ladder on both ratio and throughput.
1194 LevelParams {
1195 strategy_tag: crate::encoding::strategy::StrategyTag::Fast,
1196 search: crate::encoding::strategy::SearchMethod::Fast,
1197 window_log: 19,
1198 lazy_depth: 0,
1199 fast: Some(FastConfig {
1200 hash_log: 13,
1201 mls: 7,
1202 step_size,
1203 }),
1204 dfast: None,
1205 hc: None,
1206 row: None,
1207 }
1208 }
1209 }
1210 };
1211 if let Some(size) = source_size {
1212 adjust_params_for_source_size(params, size)
1213 } else {
1214 params
1215 }
1216}
1217
1218/// The cheap fingerprint pre-splitter level for a compression level (the
1219/// C-like `blockSplitterLevel`), resolved through the same per-level
1220/// `LevelParams` table as every other tuning knob. `None` keeps the whole
1221/// 128 KiB block. The frame loop reads this instead of hardcoding the
1222/// level→split mapping at the call site.
1223pub(crate) fn level_pre_split(level: CompressionLevel) -> Option<usize> {
1224 // Resolve through `resolve_level_params` directly — NOT via the legacy
1225 // `numeric_level()` alias — so named presets read the SAME table row as
1226 // every other tuning knob (`Best` maps to its own row there, which is
1227 // not the row its numeric alias points at). `Uncompressed` (raw
1228 // blocks) never splits.
1229 if matches!(level, CompressionLevel::Uncompressed) {
1230 return None;
1231 }
1232 resolve_level_params(level, None)
1233 .pre_split()
1234 .map(usize::from)
1235}