aboutsummaryrefslogtreecommitdiff
path: root/crates/render-core
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/render-core
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz
fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'crates/render-core')
-rw-r--r--crates/render-core/Cargo.toml11
-rw-r--r--crates/render-core/README.md14
-rw-r--r--crates/render-core/src/lib.rs146
-rw-r--r--crates/render-core/src/tests.rs256
4 files changed, 0 insertions, 427 deletions
diff --git a/crates/render-core/Cargo.toml b/crates/render-core/Cargo.toml
deleted file mode 100644
index c93d624..0000000
--- a/crates/render-core/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "render-core"
-version = "0.1.0"
-edition = "2021"
-
-[dependencies]
-msh-core = { path = "../msh-core" }
-
-[dev-dependencies]
-common = { path = "../common" }
-nres = { path = "../nres" }
diff --git a/crates/render-core/README.md b/crates/render-core/README.md
deleted file mode 100644
index a58f64f..0000000
--- a/crates/render-core/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# render-core
-
-CPU-подготовка draw-данных для моделей `MSH`.
-
-Покрывает:
-
-- обход `node -> slot -> batch`;
-- раскрытие индексов в triangle-list (`position + uv0`);
-- расчёт bounds по вершинам.
-
-Тесты:
-
-- построение рендер-сеток на реальных `.msh` из `testdata`;
-- unit-test bounds.
diff --git a/crates/render-core/src/lib.rs b/crates/render-core/src/lib.rs
deleted file mode 100644
index c7a69d6..0000000
--- a/crates/render-core/src/lib.rs
+++ /dev/null
@@ -1,146 +0,0 @@
-use msh_core::Model;
-use std::collections::HashMap;
-
-pub const DEFAULT_UV_SCALE: f32 = 1024.0;
-
-#[derive(Clone, Debug)]
-pub struct RenderVertex {
- pub position: [f32; 3],
- pub uv0: [f32; 2],
-}
-
-#[derive(Clone, Debug)]
-pub struct RenderMesh {
- pub vertices: Vec<RenderVertex>,
- pub indices: Vec<u16>,
- pub batch_count: usize,
- pub index_overflow: bool,
-}
-
-impl RenderMesh {
- pub fn triangle_count(&self) -> usize {
- self.indices.len() / 3
- }
-}
-
-/// Builds an indexed triangle mesh for a specific LOD/group pair.
-pub fn build_render_mesh(model: &Model, lod: usize, group: usize) -> RenderMesh {
- let mut vertices = Vec::new();
- let mut indices = Vec::new();
- let mut index_remap: HashMap<usize, u16> = HashMap::new();
- let mut batch_count = 0usize;
- let mut index_overflow = false;
- let uv0 = model.uv0.as_ref();
-
- for node_index in 0..model.node_count {
- let Some(slot_idx) = model.slot_index(node_index, lod, group) else {
- continue;
- };
- let Some(slot) = model.slots.get(slot_idx) else {
- continue;
- };
- let batch_start = usize::from(slot.batch_start);
- let batch_end = batch_start.saturating_add(usize::from(slot.batch_count));
- if batch_end > model.batches.len() {
- continue;
- }
-
- for batch in &model.batches[batch_start..batch_end] {
- let index_start = usize::try_from(batch.index_start).unwrap_or(usize::MAX);
- let index_count = usize::from(batch.index_count);
- let index_end = index_start.saturating_add(index_count);
- if index_end > model.indices.len() || index_count < 3 {
- continue;
- }
-
- let batch_out_start = indices.len();
- let mut batch_valid = true;
- for &idx in &model.indices[index_start..index_end] {
- let final_idx_u64 = u64::from(batch.base_vertex).saturating_add(u64::from(idx));
- let Ok(final_idx) = usize::try_from(final_idx_u64) else {
- batch_valid = false;
- break;
- };
- let Some(pos) = model.positions.get(final_idx) else {
- batch_valid = false;
- break;
- };
-
- let local_index = if let Some(&mapped) = index_remap.get(&final_idx) {
- mapped
- } else {
- let Ok(mapped) = u16::try_from(vertices.len()) else {
- index_overflow = true;
- batch_valid = false;
- break;
- };
- let uv = uv0
- .and_then(|uvs| uvs.get(final_idx))
- .copied()
- .map(|packed| {
- [
- packed[0] as f32 / DEFAULT_UV_SCALE,
- packed[1] as f32 / DEFAULT_UV_SCALE,
- ]
- })
- .unwrap_or([0.0, 0.0]);
- vertices.push(RenderVertex {
- position: *pos,
- uv0: uv,
- });
- index_remap.insert(final_idx, mapped);
- mapped
- };
-
- indices.push(local_index);
- }
-
- if !batch_valid {
- indices.truncate(batch_out_start);
- continue;
- }
-
- batch_count += 1;
- }
- }
-
- RenderMesh {
- vertices,
- indices,
- batch_count,
- index_overflow,
- }
-}
-
-pub fn compute_bounds(vertices: &[[f32; 3]]) -> Option<([f32; 3], [f32; 3])> {
- compute_bounds_impl(vertices.iter().copied())
-}
-
-pub fn compute_bounds_for_mesh(vertices: &[RenderVertex]) -> Option<([f32; 3], [f32; 3])> {
- compute_bounds_impl(vertices.iter().map(|v| v.position))
-}
-
-fn compute_bounds_impl<I>(mut positions: I) -> Option<([f32; 3], [f32; 3])>
-where
- I: Iterator<Item = [f32; 3]>,
-{
- let first = positions.next()?;
- let mut min_v = first;
- let mut max_v = first;
-
- for pos in positions {
- for i in 0..3 {
- if pos[i] < min_v[i] {
- min_v[i] = pos[i];
- }
- if pos[i] > max_v[i] {
- max_v[i] = pos[i];
- }
- }
- }
-
- Some((min_v, max_v))
-}
-
-#[cfg(test)]
-mod tests;
diff --git a/crates/render-core/src/tests.rs b/crates/render-core/src/tests.rs
deleted file mode 100644
index 1c5285e..0000000
--- a/crates/render-core/src/tests.rs
+++ /dev/null
@@ -1,256 +0,0 @@
-use super::*;
-use common::collect_files_recursive;
-use msh_core::parse_model_payload;
-use nres::Archive;
-use std::fs;
-use std::path::{Path, PathBuf};
-
-fn nres_test_files() -> Vec<PathBuf> {
- let root = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("..")
- .join("..")
- .join("testdata");
- let mut files = Vec::new();
- collect_files_recursive(&root, &mut files);
- files.sort();
- files
- .into_iter()
- .filter(|path| {
- fs::read(path)
- .map(|bytes| bytes.get(0..4) == Some(b"NRes"))
- .unwrap_or(false)
- })
- .collect()
-}
-
-#[test]
-fn build_render_mesh_for_real_models() {
- let archives = nres_test_files();
- if archives.is_empty() {
- eprintln!("skipping build_render_mesh_for_real_models: no NRes files in testdata");
- return;
- }
-
- let mut models_checked = 0usize;
- let mut meshes_non_empty = 0usize;
- let mut bounds_non_empty = 0usize;
-
- for archive_path in archives {
- let archive = Archive::open_path(&archive_path)
- .unwrap_or_else(|err| panic!("failed to open {}: {err}", archive_path.display()));
- for entry in archive.entries() {
- if !entry.meta.name.to_ascii_lowercase().ends_with(".msh") {
- continue;
- }
- models_checked += 1;
- let payload = archive.read(entry.id).unwrap_or_else(|err| {
- panic!(
- "failed to read model '{}' from {}: {err}",
- entry.meta.name,
- archive_path.display()
- )
- });
- let model = parse_model_payload(payload.as_slice()).unwrap_or_else(|err| {
- panic!(
- "failed to parse model '{}' from {}: {err}",
- entry.meta.name,
- archive_path.display()
- )
- });
- let mesh = build_render_mesh(&model, 0, 0);
- if !mesh.indices.is_empty() {
- meshes_non_empty += 1;
- }
- if compute_bounds_for_mesh(&mesh.vertices).is_some() {
- bounds_non_empty += 1;
- }
- for &index in &mesh.indices {
- assert!(
- usize::from(index) < mesh.vertices.len(),
- "index out of bounds for '{}' in {}",
- entry.meta.name,
- archive_path.display()
- );
- }
- for vertex in &mesh.vertices {
- assert!(
- vertex.uv0[0].is_finite() && vertex.uv0[1].is_finite(),
- "UV must be finite for '{}' in {}",
- entry.meta.name,
- archive_path.display()
- );
- }
- }
- }
-
- assert!(models_checked > 0, "no MSH models found");
- assert!(
- meshes_non_empty > 0,
- "all generated render meshes are empty"
- );
- assert_eq!(
- meshes_non_empty, bounds_non_empty,
- "bounds must be available for every non-empty mesh"
- );
-}
-
-#[test]
-fn compute_bounds_handles_empty_and_non_empty() {
- assert!(compute_bounds(&[]).is_none());
- let bounds = compute_bounds(&[[1.0, 2.0, 3.0], [-2.0, 5.0, 0.5], [0.0, -1.0, 9.0]])
- .expect("bounds expected");
- assert_eq!(bounds.0, [-2.0, -1.0, 0.5]);
- assert_eq!(bounds.1, [1.0, 5.0, 9.0]);
-}
-
-#[test]
-fn compute_bounds_for_mesh_handles_empty_and_non_empty() {
- assert!(compute_bounds_for_mesh(&[]).is_none());
- let bounds = compute_bounds_for_mesh(&[
- RenderVertex {
- position: [1.0, 2.0, 3.0],
- uv0: [0.0, 0.0],
- },
- RenderVertex {
- position: [-2.0, 5.0, 0.5],
- uv0: [0.2, 0.3],
- },
- RenderVertex {
- position: [0.0, -1.0, 9.0],
- uv0: [1.0, 1.0],
- },
- ])
- .expect("bounds expected");
- assert_eq!(bounds.0, [-2.0, -1.0, 0.5]);
- assert_eq!(bounds.1, [1.0, 5.0, 9.0]);
-}
-
-fn nodes_with_slot_refs(slot_ids: &[Option<u16>]) -> Vec<u8> {
- let mut out = vec![0u8; slot_ids.len().saturating_mul(38)];
- for (node_index, slot_id) in slot_ids.iter().copied().enumerate() {
- let node_off = node_index * 38;
- for i in 0..15 {
- let off = node_off + 8 + i * 2;
- out[off..off + 2].copy_from_slice(&u16::MAX.to_le_bytes());
- }
- if let Some(slot_id) = slot_id {
- out[node_off + 8..node_off + 10].copy_from_slice(&slot_id.to_le_bytes());
- }
- }
- out
-}
-
-fn slot(batch_start: u16, batch_count: u16) -> msh_core::Slot {
- msh_core::Slot {
- tri_start: 0,
- tri_count: 0,
- batch_start,
- batch_count,
- aabb_min: [0.0; 3],
- aabb_max: [0.0; 3],
- sphere_center: [0.0; 3],
- sphere_radius: 0.0,
- opaque: [0; 5],
- }
-}
-
-fn batch(index_start: u32, index_count: u16, base_vertex: u32) -> msh_core::Batch {
- msh_core::Batch {
- batch_flags: 0,
- material_index: 0,
- opaque4: 0,
- opaque6: 0,
- index_count,
- index_start,
- opaque14: 0,
- base_vertex,
- }
-}
-
-#[test]
-fn build_render_mesh_handles_empty_slot_model() {
- let model = msh_core::Model {
- node_stride: 38,
- node_count: 1,
- nodes_raw: nodes_with_slot_refs(&[None]),
- slots: Vec::new(),
- positions: vec![[0.0, 0.0, 0.0]],
- normals: None,
- uv0: None,
- indices: Vec::new(),
- batches: Vec::new(),
- node_names: None,
- };
-
- let mesh = build_render_mesh(&model, 0, 0);
- assert!(mesh.vertices.is_empty());
- assert!(mesh.indices.is_empty());
- assert_eq!(mesh.batch_count, 0);
- assert_eq!(mesh.triangle_count(), 0);
-}
-
-#[test]
-fn build_render_mesh_supports_multi_node_and_uv_scaling() {
- let model = msh_core::Model {
- node_stride: 38,
- node_count: 2,
- nodes_raw: nodes_with_slot_refs(&[Some(0), Some(1)]),
- slots: vec![slot(0, 1), slot(1, 1)],
- positions: vec![
- [0.0, 0.0, 0.0],
- [1.0, 0.0, 0.0],
- [0.0, 1.0, 0.0],
- [2.0, 0.0, 0.0],
- [3.0, 0.0, 0.0],
- [2.0, 1.0, 0.0],
- ],
- normals: None,
- uv0: Some(vec![
- [1024, -1024],
- [512, 256],
- [0, 0],
- [1024, 1024],
- [2048, 1024],
- [1024, 0],
- ]),
- indices: vec![0, 1, 2, 0, 1, 2],
- batches: vec![batch(0, 3, 0), batch(3, 3, 3)],
- node_names: None,
- };
-
- let mesh = build_render_mesh(&model, 0, 0);
- assert_eq!(mesh.batch_count, 2);
- assert_eq!(mesh.vertices.len(), 6);
- assert_eq!(mesh.indices, vec![0, 1, 2, 3, 4, 5]);
- assert_eq!(mesh.triangle_count(), 2);
- assert_eq!(mesh.vertices[0].uv0, [1.0, -1.0]);
- assert_eq!(mesh.vertices[1].uv0, [0.5, 0.25]);
- assert_eq!(mesh.vertices[2].uv0, [0.0, 0.0]);
- assert_eq!(mesh.vertices[3].uv0, [1.0, 1.0]);
-}
-
-#[test]
-fn build_render_mesh_deduplicates_shared_vertices() {
- let model = msh_core::Model {
- node_stride: 38,
- node_count: 1,
- nodes_raw: nodes_with_slot_refs(&[Some(0)]),
- slots: vec![slot(0, 1)],
- positions: vec![
- [0.0, 0.0, 0.0],
- [1.0, 0.0, 0.0],
- [0.0, 1.0, 0.0],
- [1.0, 1.0, 0.0],
- ],
- normals: None,
- uv0: None,
- indices: vec![0, 1, 2, 2, 1, 3],
- batches: vec![batch(0, 6, 0)],
- node_names: None,
- };
-
- let mesh = build_render_mesh(&model, 0, 0);
- assert_eq!(mesh.vertices.len(), 4);
- assert_eq!(mesh.indices, vec![0, 1, 2, 2, 1, 3]);
- assert_eq!(mesh.triangle_count(), 2);
-}