Skip to main content

rcvbp/image/
mod.rs

1//! The compiled parameter image: flash block 7, applied by the card at boot.
2//!
3//! A fixed-offset scatter of pack bodies with no framing or checksums
4//! (`docs/compiled-image-format.md`); the builder starts from erased flash
5//! and reports every page it wrote.
6
7pub mod anti_void;
8pub mod data_swap;
9pub mod module_pos;
10pub mod scan_table;
11
12use crate::record01::View;
13use crate::spec::Generated;
14use crate::Rcvbp;
15use anyhow::{bail, Context, Result};
16use panelspec::PanelSpec;
17pub use receivers::BootImage;
18
19pub const IMAGE_LEN: usize = 0x1_0000;
20
21// The E120's region offsets (`config/cards/e120.toml`), pinned by
22// `tests/factory.rs`; the builder itself reads them from a `BootImage`.
23pub const BASIC_PACK_OFFSET: usize = 0x0000;
24pub const DATA_SWAP_OFFSET: usize = 0x0500;
25pub const MODULE_POS_OFFSET: usize = 0x0600;
26pub const CHIP_PAGE_OFFSET: usize = 0x0900;
27/// The void-line packs (zeroed for this chip; `send_params` slices them).
28pub const VOID_LINE_OFFSET: usize = 0x1000;
29pub const VOID_LINE_COLUMNS_OFFSET: usize = 0x1400;
30pub const ANTI_VOID_OFFSET: usize = 0x1800;
31pub const MAPPING_OFFSET: usize = 0x3000;
32pub const SCAN_TABLE_OFFSET: usize = 0x6000;
33pub const RCVBP_OFFSET: usize = 0x8000;
34
35/// Regions the vendor zeroes for this chip because a builder gate fails: void
36/// table (mode 0), current segment (chip id outside the table), current
37/// exchange, void-line packs (empty table), anti-void packs 4-7 (no large-load).
38const ZERO_REGIONS: [(usize, usize); 6] = [
39    (0x0100, 0x0500),
40    (0x0A00, 0x0C00),
41    (0x0C00, 0x0D00),
42    (0x1000, 0x1800),
43    (0x6800, 0x7000),
44    (0x7000, 0x8000),
45];
46
47pub struct Block7Builder {
48    map: BootImage,
49    img: Vec<u8>,
50    notes: Vec<String>,
51}
52
53impl Block7Builder {
54    /// Erased flash (all 0xFF), so every other byte is one the builder placed;
55    /// `map` says where each region goes.
56    #[must_use]
57    pub fn erased(map: &BootImage) -> Self {
58        Self {
59            map: map.clone(),
60            img: vec![0xFF; IMAGE_LEN],
61            notes: Vec::new(),
62        }
63    }
64
65    /// The raster-state regions. `void_line_columns` must follow `zero_regions`,
66    /// which clears 0x1000..0x1800 (docs/rendering.md). The chip page and the
67    /// embedded `.rcvbp` are left to the caller: RAM pushes must not send them.
68    ///
69    /// # Errors
70    /// Fails if the generated config lacks a record or a region builder
71    /// refuses the spec.
72    pub fn from_generated(map: &BootImage, spec: &PanelSpec, g: &Generated) -> Result<Self> {
73        let rec01 = &g.rcvbp.record_01().context("generated config has no record 0x01")?.payload;
74        let mut b = Self::erased(map);
75        b.zero_regions();
76        b.basic_pack(&g.basic_pack)?;
77        b.data_swap_from(rec01)?;
78        b.module_positions_from(rec01)?;
79        b.anti_void_lines();
80        if spec.mapping.gate_phantom_positions {
81            b.void_line_columns(spec.module.width, spec.module.width * 2);
82        }
83        b.mapping_from(&g.rcvbp)?;
84        b.scan_table_from(rec01, spec.card_scan_len())?;
85        Ok(b)
86    }
87
88    fn place(&mut self, at: usize, bytes: &[u8], note: impl Into<String>) {
89        self.img[at..at + bytes.len()].copy_from_slice(bytes);
90        self.notes.push(note.into());
91    }
92
93    /// The gated-off regions, as the vendor's zeroed buffers leave them.
94    pub fn zero_regions(&mut self) {
95        for (lo, hi) in ZERO_REGIONS {
96            self.img[lo..hi].fill(0);
97        }
98        self.notes
99            .push("0x100/0xA00/0xC00/0x1000/0x6800/0x7000: zeros (builders gated off)".into());
100    }
101
102    /// 0x1400: the void-line column table, one byte per line position
103    /// (`physical = a + table[a]`, `GetVoidLineInfoPacks` @ 0x1e58c0). 0xFF
104    /// pushes `from..to` past the end of the chain; for this wiring
105    /// `width..2*width` carried a fixed pattern instead (docs/rendering.md).
106    pub fn void_line_columns(&mut self, from: u16, to: u16) {
107        let at = self.map.void_line_columns;
108        self.img[at + usize::from(from)..at + usize::from(to)].fill(0xFF);
109        self.notes.push(format!(
110            "0x1400: void-line column table, positions {from}..{to} displaced off the chain"
111        ));
112    }
113
114    /// Page 0: the basic-parameter pack body.
115    ///
116    /// # Errors
117    /// Rejects a body that is not exactly one page.
118    pub fn basic_pack(&mut self, body: &[u8]) -> Result<()> {
119        if body.len() != 0x100 {
120            bail!("basic pack body is {} bytes, need 256", body.len());
121        }
122        self.place(self.map.basic_pack, body, "page 0x00: basic-parameter pack");
123        Ok(())
124    }
125
126    /// Page 0x09: record 0x84 verbatim; the card arms the drivers at boot only
127    /// when this page is written. No record 0x84 leaves the page erased.
128    ///
129    /// # Errors
130    /// Fails if record 0x84 is present but not one page.
131    pub fn chip_registers_from(&mut self, cfg: &Rcvbp) -> Result<()> {
132        let Some(rec) = cfg.find_by_id(0x84) else {
133            return Ok(());
134        };
135        if rec.payload.len() != 0x100 {
136            bail!("record 0x84 is {} bytes, need 256", rec.payload.len());
137        }
138        self.place(self.map.chip_page, &rec.payload, "page 0x09: chip registers");
139        Ok(())
140    }
141
142    /// 0x500: the data-swap pack body.
143    ///
144    /// # Errors
145    /// Fails on a short record.
146    pub fn data_swap_from(&mut self, rec01: &[u8]) -> Result<()> {
147        let body = data_swap::body(View::new(rec01)?);
148        self.place(self.map.data_swap, &body, "0x500: data-swap (lane map + deseam 1.0 x3)");
149        Ok(())
150    }
151
152    /// 0x600: the module-position table.
153    ///
154    /// # Errors
155    /// Fails on a short record or an unimplemented split layout.
156    pub fn module_positions_from(&mut self, rec01: &[u8]) -> Result<()> {
157        let (region, note) = module_pos::region(View::new(rec01)?)?;
158        self.place(self.map.module_positions, &region, note);
159        Ok(())
160    }
161
162    /// 0x1800: the anti-void-line counters.
163    pub fn anti_void_lines(&mut self) {
164        let region = anti_void::region();
165        self.place(self.map.anti_void, &region, "0x1800: anti-void-line counters");
166    }
167
168    /// 0x3000: record 0x03 with each entry's u16 flipped LE to BE; the
169    /// vendor's 16 pixel-sequence packs are this table, sliced.
170    ///
171    /// # Errors
172    /// Fails if the record is missing, malformed, or too large.
173    pub fn mapping_from(&mut self, cfg: &Rcvbp) -> Result<()> {
174        let rec = record(cfg, 0x03)?;
175        let body = &rec[2..];
176        if !body.len().is_multiple_of(3) {
177            bail!("mapping record body is {} bytes, not a multiple of 3", body.len());
178        }
179        let len = self.map.mapping_len();
180        if body.len() > len {
181            bail!("mapping record ({} entries) exceeds the card's {} entries", body.len() / 3, self.map.map_entries);
182        }
183        let dst = &mut self.img[self.map.mapping..self.map.mapping + len];
184        dst.fill(0);
185        let (dst3, _) = dst.as_chunks_mut::<3>();
186        let (src3, _) = body.as_chunks::<3>();
187        for (d, e) in dst3.iter_mut().zip(src3) {
188            d.copy_from_slice(&[e[0], e[2], e[1]]);
189        }
190        let note = if body.len() < len {
191            format!(
192                "pages 0x30-0x5f: mapping ({} entries, zero-padded — padding UNVERIFIED)",
193                body.len() / 3
194            )
195        } else {
196            format!("pages 0x30-0x5f: mapping ({} entries)", body.len() / 3)
197        };
198        self.notes.push(note);
199        Ok(())
200    }
201
202    /// 0x6000: the scan table from the vendor's bit-time solver.
203    ///
204    /// # Errors
205    /// Fails for solver inputs outside the transcribed cases.
206    pub fn scan_table_from(&mut self, rec01: &[u8], card_scan_len: u16) -> Result<()> {
207        let table = scan_table::body(View::new(rec01)?, card_scan_len)?;
208        self.place(self.map.scan_table, &table, "0x6000: scan table (bit-time solver)");
209        Ok(())
210    }
211
212    /// 0x8000: the length-prefixed `.rcvbp`, erased flash after it.
213    ///
214    /// # Errors
215    /// Rejects a file over the vendor's clamp.
216    pub fn rcvbp(&mut self, file: &[u8]) -> Result<()> {
217        let max = self.map.rcvbp_max;
218        if file.len() > max {
219            bail!("rcvbp is {} bytes; the vendor clamps at {max}", file.len());
220        }
221        let at = self.map.rcvbp;
222        self.img[at..at + 4].copy_from_slice(&(file.len() as u32).to_le_bytes());
223        self.img[at + 4..at + 4 + file.len()].copy_from_slice(file);
224        self.img[at + 4 + file.len()..].fill(0xFF);
225        self.notes.push(format!("+0x8000: embedded .rcvbp ({} bytes)", file.len()));
226        Ok(())
227    }
228
229    /// The image, what was placed, and which pages are no longer erased.
230    #[must_use]
231    pub fn finish(self) -> Block7 {
232        let changed_pages = self
233            .img
234            .as_chunks::<0x100>()
235            .0
236            .iter()
237            .enumerate()
238            .filter(|(_, page)| page.iter().any(|&b| b != 0xFF))
239            .map(|(i, _)| i as u8)
240            .collect();
241        Block7 {
242            image: self.img,
243            notes: self.notes,
244            changed_pages,
245        }
246    }
247}
248
249/// The whole image for a generated config: the raster regions, the chip page
250/// when the spec arms at boot, and the embedded `.rcvbp`. What `rxp config
251/// gen` writes as `<name>-block7.bin`.
252///
253/// # Errors
254/// Fails where the region builders do, or if the `.rcvbp` cannot be encoded.
255pub fn compile(map: &BootImage, spec: &PanelSpec, g: &Generated) -> Result<Block7> {
256    let mut b = Block7Builder::from_generated(map, spec, g)?;
257    if spec.boot.arm_at_boot {
258        b.chip_registers_from(&g.rcvbp)?;
259    }
260    b.rcvbp(&g.rcvbp.to_file_bytes()?)?;
261    Ok(b.finish())
262}
263
264/// A finished block-7 image.
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct Block7 {
267    pub image: Vec<u8>,
268    /// One line per region placed.
269    pub notes: Vec<String>,
270    /// Pages (256 B) that are no longer erased flash.
271    pub changed_pages: Vec<u8>,
272}
273
274fn record(cfg: &Rcvbp, id: u8) -> Result<&[u8]> {
275    cfg.find_by_id(id)
276        .map(|r| r.payload.as_slice())
277        .with_context(|| format!("config has no record 0x{id:02x}"))
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn the_e120_model_carries_the_pinned_offsets() {
286        let m = &receivers::by_name("E120").unwrap().memory.boot_image;
287        assert_eq!(
288            [m.basic_pack, m.data_swap, m.module_positions, m.chip_page, m.void_line, m.void_line_columns, m.anti_void, m.mapping, m.scan_table, m.rcvbp],
289            [BASIC_PACK_OFFSET, DATA_SWAP_OFFSET, MODULE_POS_OFFSET, CHIP_PAGE_OFFSET, VOID_LINE_OFFSET, VOID_LINE_COLUMNS_OFFSET, ANTI_VOID_OFFSET, MAPPING_OFFSET, SCAN_TABLE_OFFSET, RCVBP_OFFSET]
290        );
291        assert_eq!((m.map_entries, m.rcvbp_max), (4096, 0x6FFC));
292    }
293}