Skip to main content

panelspec/
lib.rs

1//! A panel spec (`config/panels/*.toml`) and the driver-chip library it names
2//! (`config/chips/*.toml`). Nothing here knows a receiver card's record or
3//! image layout; `rcvbp` turns a spec into Colorlight's formats.
4
5pub mod chips;
6
7pub use chips::{ChipLibrary, ScanPatch};
8
9/// The chip libraries and panel specs under `config/`, embedded at build
10/// time as `(path, text)` pairs.
11///
12/// The path is relative to the repository root. Non-mined files first,
13/// then `mined/`, each alphabetical.
14pub mod embedded {
15    use anyhow::Context as _;
16
17    include!(concat!(env!("OUT_DIR"), "/libraries.rs"));
18
19    /// The chip library at `path` (`config/chips/...`).
20    #[must_use]
21    pub fn chip(path: &str) -> Option<&'static str> {
22        CHIPS.iter().find(|(p, _)| *p == path).map(|(_, t)| *t)
23    }
24
25    /// The panel spec at `path` (`config/panels/...`).
26    #[must_use]
27    pub fn panel(path: &str) -> Option<&'static str> {
28        PANELS.iter().find(|(p, _)| *p == path).map(|(_, t)| *t)
29    }
30
31    /// True for a file under a `mined/` directory: a corpus default, not a
32    /// measurement.
33    #[must_use]
34    pub fn is_mined(path: &str) -> bool {
35        path.contains("/mined/")
36    }
37
38    /// Every embedded panel spec, parsed, as `(path, spec)` in embedding
39    /// order.
40    ///
41    /// # Errors
42    /// Fails on a spec that does not parse; the crate's tests keep that from
43    /// being embedded.
44    pub fn specs() -> anyhow::Result<Vec<(&'static str, crate::PanelSpec)>> {
45        PANELS
46            .iter()
47            .map(|&(path, text)| {
48                let spec = crate::PanelSpec::parse(text)
49                    .with_context(|| format!("parse {path}"))?;
50                Ok((path, spec))
51            })
52            .collect()
53    }
54
55    /// The embedded chip library for a chip family id, as `(path, text)`:
56    /// a library an embedded panel spec names wins over one none does
57    /// (the SM16269S family has four), then embedding order.
58    #[must_use]
59    pub fn chip_by_family(family_id: u16) -> Option<(&'static str, &'static str)> {
60        let named: Vec<String> = specs()
61            .unwrap_or_default()
62            .into_iter()
63            .map(|(_, spec)| spec.chip.library)
64            .collect();
65        let has_id = |&&(_, text): &&(&str, &str)| {
66            crate::ChipLibrary::parse(text).is_ok_and(|c| c.family_id == family_id)
67        };
68        CHIPS
69            .iter()
70            .filter(|(path, _)| named.iter().any(|n| n == path))
71            .chain(CHIPS.iter())
72            .find(has_id)
73            .copied()
74    }
75
76    #[cfg(test)]
77    mod tests {
78        use super::*;
79
80        #[test]
81        fn every_embedded_spec_parses_and_carries_meta() {
82            let specs = specs().unwrap();
83            assert_eq!(specs.len(), PANELS.len());
84            for (path, text) in PANELS {
85                let table: toml::Table = text.parse().unwrap();
86                assert!(table.contains_key("meta"), "{path}: no [meta] table");
87            }
88            let (path, bench) = &specs[0];
89            assert_eq!(*path, "config/panels/p25-128x64-sm16269s.toml");
90            assert_eq!(bench.meta.status, crate::Status::Tested);
91            assert_eq!(bench.meta.origin, crate::Origin::Bench);
92            assert_eq!(bench.meta.pitch_mm, Some(2.5));
93            for (path, spec) in &specs[1..] {
94                assert!(is_mined(path));
95                assert_eq!(spec.meta.status, crate::Status::Generates, "{path}");
96                assert_eq!(spec.meta.origin, crate::Origin::Mined, "{path}");
97                assert!(spec.meta.sources > 0, "{path}");
98                assert!(!spec.meta.examples.is_empty(), "{path}");
99            }
100        }
101
102        #[test]
103        fn a_chip_id_finds_the_library_the_shipped_specs_use() {
104            // Four libraries carry 0x14C; the bench spec's is the one chosen.
105            let (path, text) = chip_by_family(0x14C).unwrap();
106            assert_eq!(path, "config/chips/sm16269s-factory.toml");
107            assert_eq!(crate::ChipLibrary::parse(text).unwrap().family_id, 0x14C);
108            assert_eq!(chip_by_family(0x85).unwrap().0, "config/chips/mined/icn2053.toml");
109            assert!(chip_by_family(0xFFFF).is_none());
110        }
111
112        #[test]
113        fn the_bench_files_are_embedded_before_the_mined_ones() {
114            assert!(chip("config/chips/sm16269s-factory.toml").is_some());
115            assert!(chip("config/chips/mined/icn2053.toml").is_some());
116            assert!(chip("config/chips/x.toml").is_none());
117            assert_eq!(PANELS[0].0, "config/panels/p25-128x64-sm16269s.toml");
118            assert!(panel(PANELS[0].0).is_some());
119            let sorted = |xs: &[(&str, &str)]| {
120                let plain: Vec<&str> = xs.iter().map(|(p, _)| *p).filter(|p| !is_mined(p)).collect();
121                let mined: Vec<&str> = xs.iter().map(|(p, _)| *p).filter(|p| is_mined(p)).collect();
122                xs.iter().take(plain.len()).all(|(p, _)| !is_mined(p))
123                    && plain.windows(2).all(|w| w[0] < w[1])
124                    && mined.windows(2).all(|w| w[0] < w[1])
125            };
126            assert!(sorted(CHIPS) && sorted(PANELS));
127            for (p, text) in PANELS {
128                assert!(crate::PanelSpec::parse(text).is_ok(), "{p}");
129            }
130            for (p, text) in CHIPS {
131                assert!(crate::ChipLibrary::parse(text).is_ok(), "{p}");
132            }
133        }
134    }
135}
136
137use anyhow::{bail, Context, Result};
138use serde::{Deserialize, Serialize, Serializer};
139use std::collections::BTreeMap;
140use std::path::Path;
141
142/// Payload length of record 0x01, the bound on `record01_overrides` keys.
143pub const RECORD01_LEN: usize = 764;
144
145/// Maps a spec's `[chip].library` path to the library's TOML text: the
146/// filesystem for the CLI, an embedded set in the browser.
147pub type Loader<'a> = &'a dyn Fn(&str) -> Result<String>;
148
149/// The filesystem loader: the file at the path, relative to the working
150/// directory.
151///
152/// # Errors
153/// Fails if the file cannot be read.
154pub fn read_library(path: &str) -> Result<String> {
155    std::fs::read_to_string(path).with_context(|| format!("read {path}"))
156}
157
158#[derive(Debug, Clone, Deserialize, Serialize)]
159#[serde(deny_unknown_fields)]
160pub struct PanelSpec {
161    /// Name used for output files.
162    pub name: String,
163    /// Where the values came from and how far they are trusted.
164    #[serde(default)]
165    pub meta: Meta,
166    pub module: Module,
167    pub screen: Screen,
168    pub chip: Chip,
169    #[serde(default)]
170    pub color: Color,
171    #[serde(default)]
172    pub current: Current,
173    #[serde(default)]
174    pub timing: Timing,
175    #[serde(default)]
176    pub mapping: Mapping,
177    #[serde(default)]
178    pub boot: Boot,
179    /// Raw record 0x01 byte overrides (`"0x043" = 0x20`), applied last. The
180    /// bench spec's `+0x02F = 1` lives here; nothing displays without it.
181    #[serde(
182        default,
183        deserialize_with = "chips::record01_offsets",
184        serialize_with = "chips::hex_offsets",
185        skip_serializing_if = "BTreeMap::is_empty"
186    )]
187    pub record01_overrides: BTreeMap<usize, u8>,
188}
189
190/// An `f32` written as its shortest decimal (`2.8`, not the f64 expansion
191/// of the nearest binary value); it parses back to the same `f32`.
192struct Short(f32);
193
194impl Serialize for Short {
195    fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
196        let text = self.0.to_string();
197        s.serialize_f64(text.parse().unwrap_or_else(|_| f64::from(self.0)))
198    }
199}
200
201// serde's `serialize_with` passes the field by reference.
202#[allow(clippy::trivially_copy_pass_by_ref)]
203fn short<S: Serializer>(v: &f32, s: S) -> std::result::Result<S::Ok, S::Error> {
204    Short(*v).serialize(s)
205}
206
207fn shorts<S: Serializer>(v: &[f32], s: S) -> std::result::Result<S::Ok, S::Error> {
208    s.collect_seq(v.iter().map(|&x| Short(x)))
209}
210
211/// The `[meta]` table: provenance and trust. Every field has a default, so
212/// a spec without the table is a mined one no file agreed with yet.
213#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
214#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
215#[serde(default, deny_unknown_fields)]
216pub struct Meta {
217    /// Pixel pitch in millimetres, when the spec describes one physical module.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    #[cfg_attr(feature = "ts", ts(optional))]
220    pub pitch_mm: Option<f32>,
221    pub status: Status,
222    pub origin: Origin,
223    /// Vendor files the values were taken from.
224    pub sources: u32,
225    /// Share (0..1) of the files for this module class that agree with the
226    /// values; absent when nothing was counted.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    #[cfg_attr(feature = "ts", ts(optional))]
229    pub agreement: Option<f32>,
230    /// A few of the source files by name.
231    pub examples: Vec<String>,
232    /// Control-system vendors whose config files the sources are (the
233    /// format the values were mined from), not who makes the panel.
234    pub vendors: Vec<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    #[cfg_attr(feature = "ts", ts(optional))]
237    pub notes: Option<String>,
238    /// Who makes the panel.
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    #[cfg_attr(feature = "ts", ts(optional))]
241    pub maker: Option<String>,
242    /// The maker's model name.
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    #[cfg_attr(feature = "ts", ts(optional))]
245    pub product: Option<String>,
246    /// Product page.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    #[cfg_attr(feature = "ts", ts(optional))]
249    pub url: Option<String>,
250    /// Specification sheet.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    #[cfg_attr(feature = "ts", ts(optional))]
253    pub datasheet: Option<String>,
254    /// Photo.
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    #[cfg_attr(feature = "ts", ts(optional))]
257    pub image: Option<String>,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    #[cfg_attr(feature = "ts", ts(optional))]
260    pub image_source: Option<String>,
261}
262
263/// How far a spec has been shown to work.
264#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
265#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
266#[serde(rename_all = "lowercase")]
267pub enum Status {
268    /// Driven on a bench from flash.
269    Tested,
270    /// The configuration generates; never driven.
271    #[default]
272    Generates,
273}
274
275/// Where a spec's values came from.
276#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
277#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
278#[serde(rename_all = "lowercase")]
279pub enum Origin {
280    /// Measured against a panel.
281    Bench,
282    /// The vendor default for the module class, from the config corpus.
283    #[default]
284    Mined,
285}
286
287#[derive(Debug, Clone, Deserialize, Serialize)]
288#[serde(deny_unknown_fields)]
289pub struct Module {
290    /// Pixels across one module.
291    pub width: u16,
292    /// Pixels down one module. The record stores half of this.
293    pub height: u16,
294    /// Scan denominator, e.g. 16 for 1/16.
295    pub scan: u8,
296    /// Serial (data) clock setting, the vendor's SetSerialClockFrequency unit;
297    /// the chip library's default when absent.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub serial_clock: Option<u16>,
300    /// Grayscale depth override; derived from the chip registers when absent.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub gray_bits: Option<u8>,
303    /// Data line direction: 0/1 vertical, 2/3 horizontal (vendor GetLineDir).
304    #[serde(default)]
305    pub line_dir: u8,
306    /// Data-group / output code (record +0x044 low nibble).
307    #[serde(default = "default_data_groups")]
308    pub data_groups: u8,
309}
310
311#[derive(Debug, Clone, Deserialize, Serialize)]
312#[serde(deny_unknown_fields)]
313pub struct Screen {
314    /// Whole screen this card drives, in pixels (MaxWidth/MaxHeight).
315    pub width: u16,
316    pub height: u16,
317}
318
319#[derive(Debug, Clone, Deserialize, Serialize)]
320#[serde(deny_unknown_fields)]
321pub struct Chip {
322    /// Chip library (`config/chips/*.toml`): ids, register defaults, chip control.
323    pub library: String,
324}
325
326#[derive(Debug, Clone, Deserialize, Serialize)]
327#[serde(deny_unknown_fields)]
328pub struct Color {
329    /// Colour-swap index (record +0x02B).
330    pub swap: u8,
331    /// R/G/B source indices (record +0x02C..0x02E); (2,1,0) = no exchange.
332    pub source: [u8; 3],
333}
334
335impl Default for Color {
336    fn default() -> Self {
337        Self {
338            swap: 3,
339            source: [2, 1, 0],
340        }
341    }
342}
343
344#[derive(Debug, Clone, Deserialize, Serialize)]
345#[serde(deny_unknown_fields)]
346pub struct Current {
347    /// Red, green, blue, virtual-red current gain, 0-63.
348    pub gains: [u8; 4],
349    /// Per-channel current percent (record +0x0B4/B8/BC, f32).
350    #[serde(serialize_with = "shorts")]
351    pub percent: [f32; 3],
352}
353
354impl Default for Current {
355    fn default() -> Self {
356        Self {
357            gains: [43; 4],
358            percent: [0.1; 3],
359        }
360    }
361}
362
363#[derive(Debug, Clone, Deserialize, Serialize)]
364#[serde(deny_unknown_fields)]
365pub struct Timing {
366    #[serde(serialize_with = "short")]
367    pub gamma: f32,
368    #[serde(serialize_with = "short")]
369    pub refresh_hz: f32,
370    /// GCLK setting (record +0x031); vendor default 0x14.
371    pub gclock: u8,
372    /// Minimum OE time (record +0x0AE); the PWM bit-time solver's floor.
373    #[serde(serialize_with = "short")]
374    pub min_oe: f32,
375    /// Luminance level (record +0x026), split across the colour percents.
376    pub luminance_level: u16,
377    /// 8 ns OE enable (record +0x050 bit 0).
378    pub oe_8ns: bool,
379}
380
381impl Default for Timing {
382    fn default() -> Self {
383        Self {
384            gamma: 2.8,
385            refresh_hz: 60.0,
386            gclock: 0x14,
387            min_oe: 1e-4,
388            luminance_level: 188,
389            oe_8ns: true,
390        }
391    }
392}
393
394/// How the module's pixels are wired into the card's scan-line buffer
395/// (record 0x03). The vendor corpus shows two knobs beyond geometry.
396#[derive(Debug, Clone, Deserialize, Serialize)]
397#[serde(deny_unknown_fields)]
398pub struct Mapping {
399    /// Data groups (`stored height / scan` of them) in reverse order in the
400    /// buffer, the vendor default (234 of 241 two-group configs).
401    pub reversed_groups: bool,
402    /// Scan lines addressed bottom-up (`scan-1-row`) instead of top-down.
403    pub reversed_lines: bool,
404    /// Columns per run of the shift chain before it switches data group:
405    /// `[lower 0..b][upper 0..b][lower b..2b]...`. Default (module width) gives
406    /// each group one contiguous half; the bench panel's own file uses 64.
407    #[serde(default, skip_serializing_if = "Option::is_none")]
408    pub block: Option<u16>,
409    /// Displace line positions `width..2*width` off the chain via the void-line
410    /// column table; otherwise the card drives them with a fixed pattern that
411    /// shows as a floor at black (docs/rendering.md). Off reproduces the factory image.
412    #[serde(default = "default_true")]
413    pub gate_phantom_positions: bool,
414}
415
416const fn default_true() -> bool {
417    true
418}
419
420impl Default for Mapping {
421    fn default() -> Self {
422        Self {
423            reversed_groups: true,
424            reversed_lines: false,
425            block: None,
426            gate_phantom_positions: true,
427        }
428    }
429}
430
431#[derive(Debug, Clone, Default, Deserialize, Serialize)]
432#[serde(deny_unknown_fields)]
433pub struct Boot {
434    /// Install the chip-register page so the card arms the drivers at
435    /// power-on. Until the config boots dark this rails the supply.
436    pub arm_at_boot: bool,
437}
438
439const fn default_data_groups() -> u8 {
440    1
441}
442
443impl PanelSpec {
444    /// Read a spec from a TOML file.
445    ///
446    /// # Errors
447    /// Fails on a missing or malformed file.
448    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
449        let path = path.as_ref();
450        let text =
451            std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
452        Self::parse(&text).with_context(|| format!("parse {}", path.display()))
453    }
454
455    /// Read a spec from TOML text.
456    ///
457    /// # Errors
458    /// Fails on malformed TOML or an unknown field.
459    pub fn parse(text: &str) -> Result<Self> {
460        Ok(toml::from_str(text)?)
461    }
462
463    /// The spec as TOML, tables in the order `config/panels/*.toml` use;
464    /// `parse` reads it back to the same values.
465    ///
466    /// # Errors
467    /// Fails when a value cannot be written as TOML.
468    pub fn to_toml(&self) -> Result<String> {
469        toml::to_string(self).context("write spec")
470    }
471
472    /// The spec's chip library, with `load` mapping `[chip].library` to TOML
473    /// text.
474    ///
475    /// # Errors
476    /// Fails when `load` does, or on a malformed library.
477    pub fn chip_library(&self, load: Loader) -> Result<ChipLibrary> {
478        let path = &self.chip.library;
479        let text = load(path)?;
480        ChipLibrary::parse(&text).with_context(|| format!("parse {path}"))
481    }
482
483    /// Check the spec against what the record can express.
484    ///
485    /// # Errors
486    /// Rejects geometry the record cannot hold.
487    pub fn validate(&self) -> Result<()> {
488        if !self.module.height.is_multiple_of(2) {
489            bail!("module height must be even (the record stores height/2)");
490        }
491        if self.module.width > 255 || self.module.height / 2 > 255 {
492            bail!("module dimensions exceed the record's byte fields");
493        }
494        if !self.screen.width.is_multiple_of(self.module.width)
495            || !self.screen.height.is_multiple_of(self.module.height)
496        {
497            bail!("screen size must be a whole number of modules");
498        }
499        if self.module.scan == 0 || u16::from(self.module.scan) > self.module.height {
500            bail!("scan denominator must be 1..=module height");
501        }
502        if !(self.module.height / 2).is_multiple_of(u16::from(self.module.scan)) {
503            bail!("stored module height (height/2) must be a whole number of scan groups");
504        }
505        Ok(())
506    }
507
508    /// The serial clock (record +0x021): the spec's, else the chip's default.
509    #[must_use]
510    pub fn serial_clock(&self, chip: &ChipLibrary) -> u16 {
511        self.module.serial_clock.unwrap_or(chip.serial_clock)
512    }
513
514    /// Grayscale depth: the spec's override, else the vendor's derivation
515    /// from the chip registers.
516    ///
517    /// # Errors
518    /// Fails if the library lacks the registers the derivation reads.
519    pub fn gray_bits(&self, chip: &ChipLibrary) -> Result<u8> {
520        match self.module.gray_bits {
521            Some(g) => Ok(g),
522            None => chip.gray_bits(),
523        }
524    }
525
526    /// The screen extent along the data-line direction (vendor
527    /// `GetMaxInLineDir`, before void adjustments).
528    #[must_use]
529    pub fn screen_extent_in_line_dir(&self) -> u16 {
530        if self.module.line_dir >= 2 {
531            self.screen.height
532        } else {
533            self.screen.width
534        }
535    }
536
537    /// Vendor `GetModuleInputCount`: the 16-pixel grid unit over the module
538    /// dimension along the line direction, at least 1.
539    #[must_use]
540    pub fn module_input_count(&self) -> u8 {
541        let unit = 16u16;
542        let dim = if self.module.line_dir >= 2 {
543            self.module.width
544        } else {
545            self.module.height / 2
546        };
547        (unit / dim.max(1)).max(1) as u8
548    }
549
550    /// Modules chained along the data-line direction (vendor
551    /// `GetModuleCountInLineDir`): screen extent / module extent on that axis.
552    #[must_use]
553    pub fn modules_in_line_dir(&self) -> u16 {
554        if self.module.line_dir >= 2 {
555            self.screen.height.div_ceil(self.module.height)
556        } else {
557            self.screen.width.div_ceil(self.module.width)
558        }
559    }
560
561    /// Clocks in one scan line (vendor `GetOneScanLen`): W x stored H / scan.
562    #[must_use]
563    pub fn one_scan_len(&self) -> u16 {
564        let v = u32::from(self.module.width) * u32::from(self.module.height / 2)
565            / u32::from(self.module.scan);
566        v.max(1) as u16
567    }
568
569    /// Clocks in one card scan line (vendor `GetCardScanLen`): OneScanLen
570    /// scaled by the modules along the line direction.
571    #[must_use]
572    pub fn card_scan_len(&self) -> u16 {
573        self.one_scan_len() * self.modules_in_line_dir()
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580
581    fn spec() -> PanelSpec {
582        PanelSpec::parse(
583            r#"
584            name = "t"
585            [module]
586            width = 128
587            height = 64
588            scan = 16
589            [screen]
590            width = 256
591            height = 64
592            [chip]
593            library = "x.toml"
594            "#,
595        )
596        .unwrap()
597    }
598
599    #[test]
600    fn geometry_helpers_follow_the_vendor_formulas() {
601        let s = spec();
602        assert!(s.validate().is_ok());
603        assert_eq!(s.modules_in_line_dir(), 2);
604        assert_eq!(s.one_scan_len(), 256);
605        assert_eq!(s.card_scan_len(), 512);
606        assert_eq!(s.screen_extent_in_line_dir(), 256);
607        assert_eq!(s.module_input_count(), 1);
608    }
609
610    #[test]
611    fn a_scan_that_does_not_divide_the_module_is_refused() {
612        let mut s = spec();
613        s.module.scan = 12;
614        assert!(s.validate().is_err());
615    }
616
617    #[test]
618    fn unknown_fields_are_refused() {
619        assert!(PanelSpec::parse("name = \"t\"\nextra = 1\n").is_err());
620    }
621
622    #[test]
623    fn a_spec_written_as_toml_reads_back_to_the_same_values() {
624        let text = std::fs::read_to_string(concat!(
625            env!("CARGO_MANIFEST_DIR"),
626            "/config/panels/p25-128x64-sm16269s.toml"
627        ))
628        .unwrap();
629        let spec = PanelSpec::parse(&text).unwrap();
630        let out = spec.to_toml().unwrap();
631        assert!(out.starts_with("name = \"p25-128x64-sm16269s\"\n\n[meta]\n"), "{out}");
632        assert!(out.contains("\n[record01_overrides]\n0x02F = 1\n"), "{out}");
633        assert!(out.contains("gamma = 2.8\n") && out.contains("min_oe = 0.0001\n"), "{out}");
634        let back = PanelSpec::parse(&out).unwrap();
635        assert_eq!(back.to_toml().unwrap(), out);
636        assert_eq!(back.record01_overrides, spec.record01_overrides);
637        assert_eq!(back.timing.min_oe.to_bits(), spec.timing.min_oe.to_bits());
638        assert_eq!(back.module.serial_clock, Some(8));
639
640        let mut bare = spec;
641        bare.record01_overrides.clear();
642        bare.mapping.block = None;
643        let out = bare.to_toml().unwrap();
644        assert!(!out.contains("record01_overrides") && !out.contains("block"), "{out}");
645    }
646}