From 7a7578d0db2a0f6bd0a440f5ef1f72432f8b38ae Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 17:25:33 +0400 Subject: [PATCH] feat(terrain): decode slot material pairs --- crates/fparkan-terrain-format/src/lib.rs | 68 ++++++++++++++++++++++++ docs/tomes/05-render.md | 7 ++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs index 0051b82..ce97d62 100644 --- a/crates/fparkan-terrain-format/src/lib.rs +++ b/crates/fparkan-terrain-format/src/lib.rs @@ -198,6 +198,19 @@ pub struct TerrainSlotRenderDispatch { pub pair_count: u16, } +/// One pair consumed by the original terrain material-batch dispatcher. +/// +/// These four bytes live in the type-11 stream. `material_lookup` is handed to +/// `GetShade`'s material lookup, while bit `0x10` of `flags` creates a batch +/// boundary. Other flag bits remain raw. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TerrainMaterialPair { + /// Lookup id supplied to the shade material manager. + pub material_lookup: u16, + /// Opaque pair flags; bit `0x10` is a proven batch boundary. + pub flags: u16, +} + impl TerrainSlotTable { /// Returns the render-dispatch header for one decoded slot record. #[must_use] @@ -238,6 +251,29 @@ pub struct LandMeshDocument { pub faces: Vec, } +impl LandMeshDocument { + /// Resolves the type-11 material pairs addressed by one 68-byte slot. + /// + /// Returns `None` when the slot is absent or its pair range does not fit + /// the retained type-11 stream. + #[must_use] + pub fn slot_material_pairs(&self, slot_index: usize) -> Option> { + let dispatch = self.slots.render_dispatch(slot_index)?; + let start = usize::from(dispatch.pair_table_index); + let end = start.checked_add(usize::from(dispatch.pair_count))?; + let pairs = self.accelerator.get(start..end)?; + Some( + pairs + .iter() + .map(|raw| TerrainMaterialPair { + material_lookup: u16::from_le_bytes([raw[0], raw[1]]), + flags: u16::from_le_bytes([raw[2], raw[3]]), + }) + .collect(), + ) + } +} + /// Decoded `Land.map` document. #[derive(Clone, Debug, PartialEq)] pub struct LandMapDocument { @@ -1507,6 +1543,38 @@ mod tests { assert_eq!(slots.render_dispatch(1), None); } + #[test] + fn slot_material_pairs_use_type11_pair_table_and_preserve_flags() { + let nres = + decode_nres(&minimal_land_msh(&face([0, 1, 2], [None, None, None]))).expect("nres"); + let mut document = decode_land_msh(&nres).expect("land mesh"); + document.slots = TerrainSlotTable { + header_raw: SLOT_HEADER_ZERO.to_vec(), + slots_raw: vec![[0_u8; SLOT_STRIDE]], + }; + document.slots.slots_raw[0][..2].copy_from_slice(&1_u16.to_le_bytes()); + document.slots.slots_raw[0][2..4].copy_from_slice(&2_u16.to_le_bytes()); + document.accelerator = vec![[0, 0, 0, 0], [2, 1, 0, 0], [3, 2, 0x10, 0]]; + + assert_eq!( + document.slot_material_pairs(0), + Some(vec![ + TerrainMaterialPair { + material_lookup: 0x0102, + flags: 0, + }, + TerrainMaterialPair { + material_lookup: 0x0203, + flags: 0x0010, + }, + ]) + ); + assert_eq!(document.slot_material_pairs(1), None); + + document.slots.slots_raw[0][2..4].copy_from_slice(&3_u16.to_le_bytes()); + assert_eq!(document.slot_material_pairs(0), None); + } + #[test] fn terrain_material_tag_decodes_two_sidecar_selectors() { let mut raw_face = face([0, 1, 2], [None, None, None]); diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 29319ed..5dd2e0c 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1582,8 +1582,11 @@ preceding slot `+60` call sets its visibility mode. Slot `+16` consumes the second word as the count of adjacent `u16` pairs and the first as the pair-table index, then groups the pairs by material lookup before constructing draw work. `TerrainSlotTable::render_dispatch` exposes exactly those two proven disk -fields. The pair payload, its material key, and the final blend operation remain -unassigned. +fields, while `LandMeshDocument::slot_material_pairs` decodes the selected +type-11 entries as `{ material_lookup: u16, flags: u16 }`. Flag `0x0010` is a +proven batch boundary in `GetShade`; the lookup's mapping to the two WEAR +tables, the meanings of its remaining flags, and the final blend operation +remain unassigned. The static Vulkan bridge now carries each contiguous face run as a separate draw range keyed by the original packed `material_tag`; it neither reorders