From 5577a04a4093a529d2c64a24e1ab4c72941b5ff1 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 17:20:37 +0400 Subject: [PATCH] feat(terrain): expose slot render dispatch --- crates/fparkan-terrain-format/src/lib.rs | 47 ++++++++++++++++++++++++ docs/tomes/05-render.md | 15 +++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs index 6e018c0..0051b82 100644 --- a/crates/fparkan-terrain-format/src/lib.rs +++ b/crates/fparkan-terrain-format/src/lib.rs @@ -183,6 +183,33 @@ pub struct TerrainSlotTable { pub slots_raw: Vec<[u8; SLOT_STRIDE]>, } +/// The proven front fields of one 68-byte terrain slot record. +/// +/// `Terrain.dll!CLandscape` supplies these fields to `GetShade`'s terrain +/// batch dispatcher: `pair_table_index` selects the pointer-table entry and +/// `pair_count` is the number of adjacent `u16` pairs to process. The pair +/// payload and the later material/blend interpretation remain external to the +/// disk record. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TerrainSlotRenderDispatch { + /// Index into the runtime pair-pointer table. + pub pair_table_index: u16, + /// Number of adjacent `u16` pairs in that selected payload. + pub pair_count: u16, +} + +impl TerrainSlotTable { + /// Returns the render-dispatch header for one decoded slot record. + #[must_use] + pub fn render_dispatch(&self, slot_index: usize) -> Option { + let raw = self.slots_raw.get(slot_index)?; + Some(TerrainSlotRenderDispatch { + pair_table_index: u16::from_le_bytes([raw[0], raw[1]]), + pair_count: u16::from_le_bytes([raw[2], raw[3]]), + }) + } +} + /// Land mesh document. #[derive(Clone, Debug, PartialEq)] pub struct LandMeshDocument { @@ -1460,6 +1487,26 @@ mod tests { ); } + #[test] + fn slot_render_dispatch_retains_pair_table_index_and_count() { + let mut raw = [0_u8; SLOT_STRIDE]; + raw[..2].copy_from_slice(&7_u16.to_le_bytes()); + raw[2..4].copy_from_slice(&3_u16.to_le_bytes()); + let slots = TerrainSlotTable { + header_raw: SLOT_HEADER_ZERO.to_vec(), + slots_raw: vec![raw], + }; + + assert_eq!( + slots.render_dispatch(0), + Some(TerrainSlotRenderDispatch { + pair_table_index: 7, + pair_count: 3, + }) + ); + assert_eq!(slots.render_dispatch(1), 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 749d073..29319ed 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1575,12 +1575,15 @@ an overlay, blend, or microtexture shader. The terrain render traversal is now located at `Terrain.dll` RVA `0x11340`. After visibility/cell work, it derives a 68-byte source-material record and -dispatches renderer-context slot `+16` with four arguments: zero, the record's -16-bit word at `+2`, a pointer selected from a four-byte table by its word at -`+0`, and zero. A preceding renderer-context slot `+60` receives the current -visibility mode. This is the concrete terrain-to-renderer handoff; it does not -call `GetMaterialPhase` directly and does not yet identify either word, the -four-byte payload, or the final blend operation. +dispatches `Terrain!GetShade()` slot `+16` with four arguments: zero, the +record's word at `+2`, a pointer selected from a four-byte table by its word at +`+0`, and zero. `GetShade` is a Terrain singleton, not `niGet3DRender`; the +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. 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