Implement MeshFoundry pipeline (#77)
Some checks failed
Native code generation / deterministic (push) Failing after 1m58s
Imaging and meshing gate / native (push) Failing after 4m10s
JPEG 2000 feature / linux (push) Successful in 3m0s
Native Rust workspace compile / compile (push) Failing after 6m12s
Skia feature / linux (push) Has been cancelled

This commit is contained in:
2026-08-10 22:37:43 +00:00
parent 611db567a0
commit 88408c9680
17 changed files with 2402 additions and 50 deletions

View File

@@ -8,6 +8,7 @@ on:
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "tools/check_milestone_10_issue_76.py"
- "tools/check_milestone_10_issue_77.py"
- "**/*.rs"
- "**/Cargo.toml"
- "Cargo.lock"
@@ -18,6 +19,7 @@ on:
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "tools/check_milestone_10_issue_76.py"
- "tools/check_milestone_10_issue_77.py"
- "**/*.rs"
- "**/Cargo.toml"
- "Cargo.lock"
@@ -42,7 +44,7 @@ jobs:
tools/install_openjpeg_2_5_4.sh "$OPENJPEG_PREFIX"
- name: Check formatting
run: cargo fmt --all -- --check
- name: Audit native milestone 09 boundaries
- name: Audit native implementation boundaries
run: |
python3 tools/check_milestone_09_issue_59.py
python3 tools/check_milestone_09_issue_60.py
@@ -54,6 +56,7 @@ jobs:
python3 tools/check_milestone_09_issue_66.py
python3 tools/check_milestone_09.py
python3 tools/check_milestone_10_issue_76.py
python3 tools/check_milestone_10_issue_77.py
- name: Test the complete native world milestone
run: python3 tools/test_milestone_09.py
- name: Compile every workspace target with bounded memory

3
Cargo.lock generated
View File

@@ -957,11 +957,14 @@ dependencies = [
name = "libremetaverse-rendering-mesh-foundry"
version = "0.0.1"
dependencies = [
"flate2",
"libremetaverse",
"libremetaverse-imaging",
"libremetaverse-prim-mesher",
"libremetaverse-rendering-simple",
"libremetaverse-structured-data",
"libremetaverse-types",
"stats_alloc",
]
[[package]]

View File

@@ -33,3 +33,13 @@ unsafe_code = "forbid"
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
# The generated core API is intentionally very large; full release LTO-style
# optimization can exceed small Gitea worker memory before diagnostic fixture
# benchmarks run. This stable profile retains optimization without debug or
# incremental artifacts and is used only when explicitly selected.
[profile.benchmark]
inherits = "dev"
opt-level = 1
debug = 0
incremental = false

View File

@@ -453,3 +453,12 @@ primitive UUID. It deliberately does not fetch or decode mesh assets; that
surface remains with MeshFoundry. Supported topology, 16-bit geometry budgets,
determinism, and the asset boundary are documented in the
[`SimpleRenderer` guide](crates/libremetaverse-rendering-simple/README.md).
The native `MeshFoundry` pipeline builds on that reference path for prims and
sculpts, and adds bounded packed-LLSD mesh assets, deterministic LOD fallback,
terrain and convex hulls, independent UV domains, generated or stored normals
and tangents, inherited material metadata, mirror/invert transforms, and
rigged-mesh skin matrices and normalized weights. A large executable fixture
reports decode allocations and timing without replacing correctness gates. The
format, ownership, error, and tangent-output contracts are documented in the
[`MeshFoundry` guide](crates/libremetaverse-rendering-mesh-foundry/README.md).

View File

@@ -10,7 +10,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |
| `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims |
| `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim |
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | native implementation: 1 type / 14 members; no generated shims remain |
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | native implementation: 1 type / 6 members; no generated shims remain |
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 16 types / 295 members; no generated shims remain |
| `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |

View File

@@ -5,14 +5,23 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "MeshFoundry renderer shims for the MetaCrate LibreMetaverse rewrite"
description = "Bounded native MeshFoundry renderer for the MetaCrate LibreMetaverse rewrite"
[dependencies]
flate2 = "1.1.2"
libremetaverse = { path = "../libremetaverse" }
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
libremetaverse-prim-mesher = { path = "../libremetaverse-prim-mesher" }
libremetaverse-rendering-simple = { path = "../libremetaverse-rendering-simple" }
libremetaverse-structured-data = { path = "../libremetaverse-structured-data" }
libremetaverse-types = { path = "../libremetaverse-types" }
[dev-dependencies]
stats_alloc = "0.1.10"
[[bench]]
name = "mesh_decode"
harness = false
[lints]
workspace = true

View File

@@ -0,0 +1,85 @@
# LibreMetaverse MeshFoundry renderer
`libremetaverse-rendering-mesh-foundry` is the bounded native renderer for
legacy prims, sculpt maps, terrain, and Second Life LLSD mesh assets. It uses
the checked SimpleRenderer/PrimMesher path for legacy geometry and the native
StructuredData binary codec for mesh containers. It does not wrap, load, or
invoke the C# renderer or duplicate the LLSD and image codecs.
## Pipelines
The mapped `MeshFoundry` methods cover:
- simple and face-grouped legacy prim geometry at every `DetailLevel`, always
retaining finite normals and UVs;
- plane, cylinder, sphere, and torus sculpt maps;
- rectangular height-map terrain with generated normals and UVs;
- packed mesh-asset headers and individually zlib-compressed LLSD sections;
- requested visual LOD selection (`high_lod`, `medium_lod`, `low_lod`, and
`lowest_lod`) with deterministic highest-to-lowest fallback;
- single-section simple-mesh extraction and physics convex/bounding hulls; and
- default or planar texture transforms, repeats, offsets, rotation, and prim
scale through the common `IRendering` contract.
Mesh vertices remain in prim-local coordinates. Quantized positions use each
submesh's `PositionDomain`; normals and tangent components use the format's
fixed `[-1, 1]` domain; UV0 and optional UV1 use their independent domains.
Triangle and face order is preserved, while flattened multi-face output rebases
indices without reordering vertices. Missing normals and tangents are generated
deterministically from triangles and UVs. Degenerate UV gradients receive a
stable normal-orthogonal fallback tangent.
The mapped `Face` type has no tangent field. Mesh tangents are therefore kept
without changing the public C# layout in a public `MeshFaceAux` value stored in
`Face.user_data`; recover it with
`face.user_data.downcast_ref::<MeshFaceAux>()`. Its `Vector4.w` is the bitangent
handedness. UV1 remains in `Face.tex_coords1`.
## Rigged meshes and materials
The `skin` section decodes bounded joint names, inverse-bind matrices,
bind-shape matrix, alternative inverse-bind matrices, pelvis offset, and the
joint-position scale lock into the shared `MeshSkinData`. Per-vertex weight
streams accept the protocol's sentinel form and its exactly-four-influence
form, validate every joint index, clamp protocol weights, normalize four
influences, and bind missing vertices to joint zero. The output is directly
consumable by `RiggedSkinMath` and `AnimeshSkinning`.
Each submesh retains its original face ID and the effective inherited
`PrimitiveTextureEntryFace`, including texture, legacy material, and render
material overrides. `NormalizedScale`, face bounds, centers, optional UV1,
weights, normals, tangents, and winding are kept together. Mesh sculpt mirror
and invert flags apply the viewer-compatible reflection, normal inversion, and
XOR winding rule to every face and tangent domain.
## Bounds and failure contract
Assets and compressed/decompressed sections are capped at 64 MiB. Header
sections and submeshes are capped at 256, each face remains within the 16-bit
65,536-vertex index domain, aggregate visual geometry is capped at 1,000,000
vertices and 6,000,000 indices, skins at 512 joints, and convex data at 65,536
hulls and 1,000,000 vertices. All sizes, offsets, index rebasing, buffer strides,
and allocation products use checked arithmetic before slicing or allocation.
Malformed LLSD, zlib data, domains, parallel buffers, matrices, weights,
offsets, and indices return `Error::Rendering` with the source primitive UUID
and a stable operation context. `UnpackMesh` has no primitive parameter and
uses the zero UUID. This crate performs no asset download, scene traversal,
world transform, GPU upload, material shading, texture fetch, or drawing.
## Reproducible gates and benchmark
```console
python3 tools/check_milestone_10_issue_77.py
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics --locked -j1
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-rendering-mesh-foundry --locked -j1
CARGO_BUILD_JOBS=1 cargo bench -p libremetaverse-rendering-mesh-foundry --bench mesh_decode --profile benchmark --locked -j1
```
The named `benchmark` profile uses bounded-memory moderate optimization because
full release optimization of the generated core surface can exceed small
Gitea-worker memory before the fixture runs. The non-statistical benchmark
builds and decodes a deterministic 57,600-vertex,
342,726-index mesh asset and reports elapsed time plus allocation counts. It is
diagnostic evidence for regressions; correctness and malformed-input tests
remain the release gates.

View File

@@ -0,0 +1,82 @@
#![allow(clippy::cast_precision_loss)] // The benchmark grid is fixed at 240×240.
use std::alloc::System;
use std::hint::black_box;
use std::time::Instant;
use libremetaverse::Primitive;
use libremetaverse::import_export::{ModelFace, ModelPrim};
use libremetaverse::rendering::{DetailLevel, Vertex};
use libremetaverse_rendering_mesh_foundry::MeshFoundry;
use libremetaverse_types::{UUID, Vector2, Vector3};
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, StatsAlloc};
#[global_allocator]
static ALLOCATOR: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
fn fixture(side: usize) -> Vec<u8> {
let mut model = ModelPrim::new().expect("model prim");
let mut face = ModelFace::new().expect("model face");
face.vertices.reserve(side * side);
for y in 0..side {
for x in 0..side {
let u = x as f32 / (side - 1) as f32;
let v = y as f32 / (side - 1) as f32;
face.vertices.push(Vertex {
position: Vector3 {
x: u - 0.5,
y: v - 0.5,
z: (u * std::f32::consts::TAU).sin() * 0.05,
},
normal: Vector3::unit_z(),
tex_coord: Vector2 { x: u, y: v },
});
}
}
for y in 0..side - 1 {
for x in 0..side - 1 {
let a = u32::try_from(y * side + x).expect("bounded benchmark index");
let b = a + 1;
let c = a + u32::try_from(side).expect("bounded side");
let d = c + 1;
face.indices.extend_from_slice(&[a, b, c, b, d, c]);
}
}
model.faces.push(face);
model
.create_asset(UUID::zero())
.expect("bounded benchmark mesh asset");
model.asset
}
fn main() {
let asset = fixture(240);
let renderer = MeshFoundry::new().expect("MeshFoundry constructor");
let primitive = Primitive::new_with_constructor().expect("Primitive constructor");
let allocation_region = Region::new(ALLOCATOR);
let started = Instant::now();
let mesh = renderer
.generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
primitive,
asset.clone(),
DetailLevel::Highest,
)
.expect("large fixture decode")
.expect("large fixture geometry");
let elapsed = started.elapsed();
let allocation_stats = allocation_region.change();
assert_eq!(mesh.faces.len(), 1);
assert_eq!(mesh.faces[0].vertices.len(), 57_600);
assert_eq!(mesh.faces[0].indices.len(), 342_726);
black_box((&asset, &mesh));
println!(
"mesh decode: {} compressed bytes, {} vertices, {} indices in {elapsed:?}, {} allocations, {} reallocations, {} bytes allocated",
asset.len(),
mesh.faces[0].vertices.len(),
mesh.faces[0].indices.len(),
allocation_stats.allocations,
allocation_stats.reallocations,
allocation_stats.bytes_allocated,
);
}

View File

@@ -6,11 +6,11 @@
#![allow(non_snake_case)]
/// C# type: `T:LibreMetaverse.Rendering.MeshFoundry`.
pub struct MeshFoundry;
pub use crate::mesh_foundry::MeshFoundry;
impl MeshFoundry {
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.#ctor`.
pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Rendering.MeshFoundry.#ctor")
Self::native_new()
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_mesh(
@@ -18,9 +18,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_mesh(prim, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])`.
pub fn generate_faceted_mesh_mesh_with_primitive_bytes(
@@ -28,8 +26,10 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
mesh_data: Vec<u8>,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])",
self.native_generate_faceted_mesh_mesh(
prim,
mesh_data,
libremetaverse::rendering::DetailLevel::Highest,
)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)`.
@@ -39,9 +39,7 @@ impl MeshFoundry {
mesh_data: Vec<u8>,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_mesh_mesh(prim, mesh_data, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_sculpt_mesh(
@@ -50,9 +48,7 @@ impl MeshFoundry {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::FacetedMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_mesh(
@@ -60,9 +56,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_mesh(prim, lod, false)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_mesh_with_normals(
@@ -70,9 +64,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_mesh(prim, lod, true)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_sculpt_mesh(
@@ -81,9 +73,7 @@ impl MeshFoundry {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])`.
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(
@@ -91,9 +81,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
compressed_mesh_data: Vec<u8>,
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])",
)
self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, None)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)`.
pub fn mesh_sub_mesh_as_convex_hulls_with_primitive_bytes_list(
@@ -102,9 +90,7 @@ impl MeshFoundry {
compressed_mesh_data: Vec<u8>,
bounding_hull: &mut Vec<libremetaverse_types::Vector3>,
) -> Result<Vec<Vec<libremetaverse_types::Vector3>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)",
)
self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, Some(bounding_hull))
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])`.
pub fn mesh_sub_mesh_as_simple_mesh(
@@ -112,9 +98,7 @@ impl MeshFoundry {
prim: libremetaverse::Primitive,
compressed_mesh_data: Vec<u8>,
) -> Result<Option<libremetaverse::rendering::SimpleMesh>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])",
)
self.native_mesh_sub_mesh_as_simple_mesh(prim, compressed_mesh_data)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)`.
pub fn terrain_mesh(
@@ -125,9 +109,7 @@ impl MeshFoundry {
y_begin: f32,
y_end: f32,
) -> Result<libremetaverse::rendering::Face, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)",
)
self.native_terrain_mesh(z_map, x_begin, x_end, y_begin, y_end)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)`.
pub fn transform_tex_coords(
@@ -137,18 +119,13 @@ impl MeshFoundry {
te_face: libremetaverse::PrimitiveTextureEntryFace,
prim_scale: libremetaverse_types::Vector3,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
)
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
}
/// C# member: `M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])`.
pub fn unpack_mesh(
&self,
asset_data: Vec<u8>,
) -> Result<Option<libremetaverse_structured_data::OSDMap>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])",
)
self.native_unpack_mesh(asset_data).map(Some)
}
}
impl libremetaverse::rendering::IRendering for MeshFoundry {}

View File

@@ -1,8 +1,9 @@
//! `MeshFoundry` renderer corresponding to `LibreMetaverse.Rendering.MeshFoundry`.
//! Bounded native prim, sculpt, terrain, and LLSD mesh-asset rendering.
extern crate self as libremetaverse_rendering_mesh_foundry;
mod generated;
mod mesh_foundry;
pub use generated::*;
pub use libremetaverse as core;
@@ -11,3 +12,4 @@ pub use libremetaverse_prim_mesher as prim_mesher;
pub use libremetaverse_structured_data as structured_data;
pub use libremetaverse_types as types;
pub use libremetaverse_types::Error;
pub use mesh_foundry::MeshFaceAux;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,570 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::float_cmp,
clippy::too_many_lines
)] // Fixture quantization is explicitly clamped to the u16 domain.
use std::collections::{BTreeMap, HashMap};
use std::io::Write as _;
use flate2::{Compression, write::ZlibEncoder};
use libremetaverse::import_export::{ModelFace, ModelPrim};
use libremetaverse::rendering::{DetailLevel, RiggedSkinMath, SimpleMesh, Vertex};
use libremetaverse::{Primitive, PrimitiveSculptData, PrimitiveTextureEntry};
use libremetaverse_rendering_mesh_foundry::{MeshFaceAux, MeshFoundry};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::{Error, SculptType, UUID, Vector2, Vector3};
fn compress(value: OSD) -> Vec<u8> {
let bytes = OSDParser::serialize_llsd_binary_with_osd_boolean(value, false)
.expect("serialize fixture section");
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&bytes).expect("compress fixture section");
encoder.finish().expect("finish fixture section")
}
fn pack(sections: BTreeMap<&str, OSD>) -> Vec<u8> {
let mut payload = Vec::new();
let mut header = HashMap::new();
for (name, value) in sections {
let section = compress(value);
header.insert(
name.to_owned(),
OSD::Map(HashMap::from([
(
"offset".to_owned(),
OSD::Integer(i32::try_from(payload.len()).expect("fixture offset")),
),
(
"size".to_owned(),
OSD::Integer(i32::try_from(section.len()).expect("fixture size")),
),
])),
);
payload.extend(section);
}
header.insert("version".to_owned(), OSD::Integer(1));
let mut asset = OSDParser::serialize_llsd_binary_with_osd_boolean(OSD::Map(header), false)
.expect("serialize fixture header");
asset.extend(payload);
asset
}
fn quantized3(values: &[[f32; 3]], min: [f32; 3], max: [f32; 3]) -> Vec<u8> {
values
.iter()
.flat_map(|value| {
(0..3).flat_map(|axis| quantize(value[axis], min[axis], max[axis]).to_le_bytes())
})
.collect()
}
fn quantized2(values: &[[f32; 2]], min: [f32; 2], max: [f32; 2]) -> Vec<u8> {
values
.iter()
.flat_map(|value| {
(0..2).flat_map(|axis| quantize(value[axis], min[axis], max[axis]).to_le_bytes())
})
.collect()
}
fn quantize(value: f32, min: f32, max: f32) -> u16 {
(((value - min) / (max - min)).clamp(0.0, 1.0) * 65_535.0).round() as u16
}
fn domain2(min: Vector2, max: Vector2) -> OSD {
OSD::Map(HashMap::from([
(
"Min".to_owned(),
OSD::from_vector2(min).expect("domain min"),
),
(
"Max".to_owned(),
OSD::from_vector2(max).expect("domain max"),
),
]))
}
fn domain3(min: Vector3, max: Vector3) -> OSD {
OSD::Map(HashMap::from([
(
"Min".to_owned(),
OSD::from_vector3(min).expect("domain min"),
),
(
"Max".to_owned(),
OSD::from_vector3(max).expect("domain max"),
),
]))
}
fn identity() -> OSD {
OSD::Array(
[
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
.into_iter()
.map(OSD::Real)
.collect(),
)
}
fn rigged_submesh(triangle: [u16; 3], weights: Vec<u8>) -> OSD {
let positions = [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [-0.5, 0.5, 0.0]];
let normals = [[0.0, 0.0, 1.0]; 3];
let uv0 = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
let uv1 = [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75]];
let tangents = [[1.0, 0.0, 0.0, -1.0]; 3];
let tangent_bytes: Vec<u8> = tangents
.iter()
.flat_map(|value| {
value
.iter()
.flat_map(|component| quantize(*component, -1.0, 1.0).to_le_bytes())
})
.collect();
let triangle_bytes: Vec<u8> = triangle.into_iter().flat_map(u16::to_le_bytes).collect();
let mut map = HashMap::from([
(
"PositionDomain".to_owned(),
domain3(
Vector3 {
x: -0.5,
y: -0.5,
z: -0.5,
},
Vector3 {
x: 0.5,
y: 0.5,
z: 0.5,
},
),
),
(
"Position".to_owned(),
OSD::Binary(quantized3(&positions, [-0.5; 3], [0.5; 3])),
),
(
"Normal".to_owned(),
OSD::Binary(quantized3(&normals, [-1.0; 3], [1.0; 3])),
),
(
"TexCoord0Domain".to_owned(),
domain2(Vector2::zero(), Vector2 { x: 1.0, y: 1.0 }),
),
(
"TexCoord0".to_owned(),
OSD::Binary(quantized2(&uv0, [0.0; 2], [1.0; 2])),
),
(
"TexCoord1Domain".to_owned(),
domain2(Vector2::zero(), Vector2 { x: 1.0, y: 1.0 }),
),
(
"TexCoord1".to_owned(),
OSD::Binary(quantized2(&uv1, [0.0; 2], [1.0; 2])),
),
("Tangent".to_owned(), OSD::Binary(tangent_bytes)),
("TriangleList".to_owned(), OSD::Binary(triangle_bytes)),
(
"NormalizedScale".to_owned(),
OSD::from_vector3(Vector3 {
x: 2.0,
y: 3.0,
z: 4.0,
})
.expect("normalized scale"),
),
]);
if !weights.is_empty() {
map.insert("Weights".to_owned(), OSD::Binary(weights));
}
OSD::Map(map)
}
fn skin() -> OSD {
OSD::Map(HashMap::from([
(
"joint_names".to_owned(),
OSD::Array(
[
OSD::String("mPelvis".to_owned()),
OSD::String("mTorso".to_owned()),
]
.into(),
),
),
(
"inverse_bind_matrix".to_owned(),
OSD::Array(vec![identity(), identity()]),
),
(
"alt_inverse_bind_matrix".to_owned(),
OSD::Array(vec![identity(), identity()]),
),
("bind_shape_matrix".to_owned(), identity()),
("pelvis_offset".to_owned(), OSD::Real(0.25)),
(
"lock_scale_if_joint_position".to_owned(),
OSD::Boolean(true),
),
]))
}
fn weights() -> Vec<u8> {
let quarter = (0.25_f32 * 65_535.0).round() as u16;
let three_quarters = (0.75_f32 * 65_535.0).round() as u16;
let half = (0.5_f32 * 65_535.0).round() as u16;
let mut bytes = vec![0];
bytes.extend(quarter.to_le_bytes());
bytes.push(1);
bytes.extend(three_quarters.to_le_bytes());
bytes.push(0xff);
bytes.push(0);
bytes.extend(half.to_le_bytes());
bytes.push(1);
bytes.extend(half.to_le_bytes());
bytes.push(0xff);
bytes.push(0xff);
bytes
}
fn rigged_asset(triangle: [u16; 3], weight_bytes: Vec<u8>) -> Vec<u8> {
pack(BTreeMap::from([
(
"high_lod",
OSD::Array(vec![rigged_submesh(triangle, weight_bytes)]),
),
("skin", skin()),
]))
}
fn mesh_hash(mesh: &SimpleMesh) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for vertex in &mesh.vertices {
for value in [
vertex.position.x,
vertex.position.y,
vertex.position.z,
vertex.normal.x,
vertex.normal.y,
vertex.normal.z,
vertex.tex_coord.x,
vertex.tex_coord.y,
] {
hash ^= u64::from(value.to_bits());
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
}
for index in &mesh.indices {
hash ^= u64::from(*index);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[test]
fn model_workflow_asset_round_trips_and_falls_back_deterministically() {
let mut model = ModelPrim::new().expect("model prim");
let mut face = ModelFace::new().expect("model face");
face.vertices = vec![
Vertex {
position: Vector3 {
x: -0.5,
y: -0.5,
z: 0.0,
},
normal: Vector3::unit_z(),
tex_coord: Vector2::zero(),
},
Vertex {
position: Vector3 {
x: 0.5,
y: -0.5,
z: 0.0,
},
normal: Vector3::unit_z(),
tex_coord: Vector2 { x: 1.0, y: 0.0 },
},
Vertex {
position: Vector3 {
x: -0.5,
y: 0.5,
z: 0.0,
},
normal: Vector3::unit_z(),
tex_coord: Vector2 { x: 0.0, y: 1.0 },
},
];
face.indices = vec![0, 1, 2];
model.faces.push(face);
model.create_asset(UUID::zero()).expect("mesh asset");
let renderer = MeshFoundry::new().expect("renderer");
let primitive = Primitive::new_with_constructor().expect("primitive");
let highest = renderer
.generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
primitive.clone(),
model.asset.clone(),
DetailLevel::Highest,
)
.expect("highest decode")
.expect("highest mesh");
let fallback = renderer
.generate_faceted_mesh_mesh_with_primitive_bytes_detail_level(
primitive,
model.asset.clone(),
DetailLevel::Low,
)
.expect("fallback decode")
.expect("fallback mesh");
assert_eq!(highest.faces.len(), 1);
assert_eq!(highest.faces[0].vertices.len(), 3);
assert_eq!(highest.faces[0].indices, [0, 1, 2]);
assert_eq!(fallback.faces[0].indices, highest.faces[0].indices);
let aux = highest.faces[0]
.user_data
.downcast_ref::<MeshFaceAux>()
.expect("generated tangent domain");
assert_eq!(aux.tangents.len(), 3);
assert!(aux.tangents.iter().all(|tangent| tangent.w.abs() == 1.0));
let unpacked = renderer
.unpack_mesh(model.asset)
.expect("unpack call")
.expect("unpacked map");
assert!(matches!(unpacked.get("high_lod"), Some(OSD::Array(_))));
assert!(matches!(unpacked.get("physics_convex"), Some(OSD::Map(_))));
}
#[test]
fn rigged_mesh_preserves_skin_uv_tangent_material_and_ordering_domains() {
let texture =
UUID::new_with_string("11111111-2222-3333-4444-555555555555".into()).expect("texture UUID");
let material = UUID::new_with_string("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into())
.expect("material UUID");
let mut textures = PrimitiveTextureEntry::new_with_uuid(texture).expect("texture entry");
textures
.create_face(0)
.expect("face override")
.set_render_material_id(material);
let mut primitive = Primitive::new_with_constructor().expect("primitive");
primitive.textures = Some(textures);
let renderer = MeshFoundry::new().expect("renderer");
let mesh = renderer
.generate_faceted_mesh_mesh_with_primitive_bytes(
primitive,
rigged_asset([0, 1, 2], weights()),
)
.expect("rigged decode")
.expect("rigged mesh");
let skin = mesh.skin_data.as_ref().expect("skin data");
assert_eq!(skin.joint_names, ["mPelvis", "mTorso"]);
assert_eq!(skin.inverse_bind_matrices.len(), 32);
assert_eq!(skin.alt_inverse_bind_matrices.len(), 32);
assert_eq!(skin.bind_shape_matrix.len(), 16);
assert_eq!(skin.pelvis_offset, 0.25);
assert!(skin.lock_scale_if_joint_position);
assert_eq!(
RiggedSkinMath::build_inv_bind_matrices(skin.clone())
.expect("decoded inverse bind matrices")
.len(),
2
);
assert_eq!(
RiggedSkinMath::extract_joint_position_overrides(skin.clone())
.expect("decoded joint overrides")
.len(),
2
);
let face = &mesh.faces[0];
assert_eq!(face.indices, [0, 1, 2]);
assert_eq!(
face.normalized_scale,
Vector3 {
x: 2.0,
y: 3.0,
z: 4.0
}
);
assert_eq!(face.texture_face.texture_id(), texture);
assert_eq!(face.texture_face.render_material_id(), material);
assert_eq!(face.tex_coords1.as_ref().expect("UV1").len(), 3);
let weights = face.weights.as_ref().expect("weights");
assert_eq!(weights.len(), 3);
for weight in weights {
let total = weight.weight0 + weight.weight1 + weight.weight2 + weight.weight3;
assert!((total - 1.0).abs() < 1.0e-6);
}
assert_eq!(weights[0].joint0, 0);
assert_eq!(weights[0].joint1, 1);
assert_eq!(weights[2].weight0, 1.0);
let tangents = &face
.user_data
.downcast_ref::<MeshFaceAux>()
.expect("stored tangents")
.tangents;
assert_eq!(tangents.len(), 3);
assert!(
tangents
.iter()
.all(|tangent| tangent.x > 0.999 && tangent.w == -1.0)
);
}
#[test]
fn compressed_submesh_decode_has_stable_golden_output() {
let section = compress(OSD::Array(vec![rigged_submesh([0, 1, 2], Vec::new())]));
let renderer = MeshFoundry::new().expect("renderer");
let mesh = renderer
.mesh_sub_mesh_as_simple_mesh(
Primitive::new_with_constructor().expect("primitive"),
section,
)
.expect("submesh decode")
.expect("simple mesh");
assert_eq!(mesh_hash(&mesh), 0xcb32_07be_6dea_73c8);
}
#[test]
fn compressed_submesh_decode_rebases_indices_in_face_order() {
let section = compress(OSD::Array(vec![
rigged_submesh([0, 1, 2], Vec::new()),
rigged_submesh([2, 1, 0], Vec::new()),
]));
let mesh = MeshFoundry::new()
.expect("renderer")
.mesh_sub_mesh_as_simple_mesh(
Primitive::new_with_constructor().expect("primitive"),
section,
)
.expect("submesh decode")
.expect("simple mesh");
assert_eq!(mesh.vertices.len(), 6);
assert_eq!(mesh.indices, [0, 1, 2, 5, 4, 3]);
}
#[test]
fn convex_hulls_decode_bounding_and_decomposed_domains() {
let positions = quantized3(
&[[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, 0.5]],
[-0.5; 3],
[0.5; 3],
);
let section = compress(OSD::Map(HashMap::from([
(
"Min".to_owned(),
OSD::from_vector3(Vector3 {
x: -0.5,
y: -0.5,
z: -0.5,
})
.expect("min"),
),
(
"Max".to_owned(),
OSD::from_vector3(Vector3 {
x: 0.5,
y: 0.5,
z: 0.5,
})
.expect("max"),
),
("BoundingVerts".to_owned(), OSD::Binary(positions.clone())),
("HullList".to_owned(), OSD::Binary(vec![3])),
("Positions".to_owned(), OSD::Binary(positions)),
])));
let renderer = MeshFoundry::new().expect("renderer");
let primitive = Primitive::new_with_constructor().expect("primitive");
let mut bounding = Vec::new();
let hulls = renderer
.mesh_sub_mesh_as_convex_hulls_with_primitive_bytes_list(
primitive.clone(),
section.clone(),
&mut bounding,
)
.expect("convex decode");
assert_eq!(bounding.len(), 3);
assert_eq!(hulls.len(), 1);
assert_eq!(hulls[0], bounding);
assert_eq!(
renderer
.mesh_sub_mesh_as_convex_hulls_with_primitive_bytes(primitive, section)
.expect("convex overload"),
hulls
);
}
#[test]
fn malformed_offsets_indices_and_weights_return_source_context() {
let source =
UUID::new_with_string("12345678-1234-5678-9abc-def012345678".into()).expect("source UUID");
let mut primitive = Primitive::new_with_constructor().expect("primitive");
primitive.id = source;
let renderer = MeshFoundry::new().expect("renderer");
let invalid_index = renderer.generate_faceted_mesh_mesh_with_primitive_bytes(
primitive.clone(),
rigged_asset([0, 1, 3], weights()),
);
assert!(matches!(
invalid_index,
Err(Error::Rendering { source: actual, context: "mesh triangle index is out of range" })
if actual == source
));
let invalid_weights = renderer.generate_faceted_mesh_mesh_with_primitive_bytes(
primitive.clone(),
rigged_asset([0, 1, 2], vec![2, 0, 128, 0xff, 0xff, 0xff]),
);
assert!(matches!(
invalid_weights,
Err(Error::Rendering { source: actual, context: "mesh skin joint index is out of range" })
if actual == source
));
let header = OSD::Map(HashMap::from([(
"high_lod".to_owned(),
OSD::Map(HashMap::from([
("offset".to_owned(), OSD::Integer(i32::MAX)),
("size".to_owned(), OSD::Integer(64)),
])),
)]));
let invalid_offset = OSDParser::serialize_llsd_binary_with_osd_boolean(header, false)
.expect("invalid fixture header");
assert!(matches!(
renderer.generate_faceted_mesh_mesh_with_primitive_bytes(primitive, invalid_offset),
Err(Error::Rendering { source: actual, context: "mesh section is outside the asset" })
if actual == source
));
}
#[test]
fn mesh_sculpt_mirror_and_invert_update_coordinates_normals_and_tangents() {
let mut primitive = Primitive::new_with_constructor().expect("primitive");
let mut bytes = UUID::zero().get_bytes().expect("UUID bytes");
bytes.push(SculptType::Mesh as u8 | SculptType::Mirror as u8 | SculptType::Invert as u8);
primitive.sculpt =
Some(PrimitiveSculptData::new_with_bytes_int32(bytes, 0).expect("sculpt flags"));
let mesh = MeshFoundry::new()
.expect("renderer")
.generate_faceted_mesh_mesh_with_primitive_bytes(
primitive,
rigged_asset([0, 1, 2], weights()),
)
.expect("modified decode")
.expect("modified mesh");
let face = &mesh.faces[0];
// Mirror and invert each reverse winding, so applying both preserves it.
assert_eq!(face.indices, [0, 1, 2]);
assert_eq!(face.vertices[0].position.x, 0.5);
assert!(face.vertices[0].normal.z < -0.999);
let tangents = &face
.user_data
.downcast_ref::<MeshFaceAux>()
.expect("tangents")
.tangents;
assert!(tangents[0].x < -0.999);
assert_eq!(tangents[0].w, -1.0);
}

View File

@@ -21,10 +21,14 @@ const fn is_binary_whitespace(byte: u8) -> bool {
}
pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
deserialize_prefix(&data).map(|(value, _)| value)
}
pub(crate) fn deserialize_prefix(data: &[u8]) -> Result<(OSD, usize), Error> {
if data.len() > MAX_INPUT_BYTES {
return Err(parse_error(0, "binary LLSD input exceeds allocation limit"));
}
let mut parser = Parser::new(&data);
let mut parser = Parser::new(data);
parser.skip_whitespace();
if parser.consume_prefix_ignore_ascii_case(ALT_HEADER)
|| parser.consume_prefix_ignore_ascii_case(HEADER)
@@ -37,7 +41,7 @@ pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
OSD::DEFAULT_MAX_NODES,
OSD::DEFAULT_MAX_BINARY_BYTES,
)?;
Ok(value)
Ok((value, parser.position))
}
pub(crate) fn deserialize_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<OSD, Error> {
@@ -596,6 +600,18 @@ mod tests {
);
}
#[test]
fn prefix_decode_reports_exact_payload_boundary() {
let value = OSD::Map(HashMap::from([("offset".into(), OSD::Integer(7))]));
let encoded = serialize_with_header(value.clone(), false).unwrap();
let mut container = encoded.clone();
container.extend_from_slice(b"\x78\x9c appended payload");
let (decoded, consumed) = deserialize_prefix(&container).unwrap();
assert_eq!(decoded, value);
assert_eq!(consumed, encoded.len());
assert_eq!(&container[consumed..], b"\x78\x9c appended payload");
}
#[test]
fn malformed_seed_corpus_is_rejected_with_positions() {
let expected: &[(&str, &str)] = &[

View File

@@ -54,3 +54,15 @@ pub mod xml {
pub use generated::*;
pub use libremetaverse_types as types;
pub use libremetaverse_types::Error;
/// Decodes one bounded binary LLSD value and returns the number of consumed
/// bytes, allowing native container formats to locate payloads appended after
/// an LLSD header without duplicating the codec.
///
/// # Errors
///
/// Returns a positioned parse error when the prefix is malformed, truncated,
/// or exceeds the shared LLSD depth, node, input, or allocation bounds.
pub fn deserialize_llsd_binary_prefix(data: &[u8]) -> Result<(OSD, usize), Error> {
binary::deserialize_prefix(data)
}

View File

@@ -5,10 +5,11 @@ Milestone 06 owns the native implementations in `libremetaverse-imaging`,
imaging and meshing crates contain no CLR bridge, subprocess adapter, foreign
RPC, or generated failure-only shim. The rendering crates consume the checked
mesh data but remain a separate milestone: `SimpleRenderer` now provides the
reference prim/sculpt conversion pipeline, while mesh-asset decoding and
rigging remain owned by the separate `MeshFoundry` issue. See the
reference prim/sculpt conversion pipeline. `MeshFoundry` now adds bounded mesh
asset, rigging, tangent, secondary-UV, terrain, and convex-hull handling without
changing the milestone 06 ownership boundary. See the
[`SimpleRenderer` guide](../crates/libremetaverse-rendering-simple/README.md)
for that boundary.
and [`MeshFoundry` guide](../crates/libremetaverse-rendering-mesh-foundry/README.md).
## Feature boundary

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Audit issue 77's native MeshFoundry ownership and evidence boundary."""
from __future__ import annotations
from pathlib import Path
import re
import generate_api_shims
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "src" / "mesh_foundry.rs"
GENERATED = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "src" / "generated.rs"
MANIFEST = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "Cargo.toml"
ASSET_TESTS = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "tests" / "mesh_assets.rs"
COMPAT = ROOT / "tests" / "compat" / "tests" / "imaging_meshing_semantics.rs"
BENCH = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "benches" / "mesh_decode.rs"
WORKSPACE = ROOT / "Cargo.toml"
DOC = ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "README.md"
STRUCTURED = ROOT / "crates" / "libremetaverse-structured-data" / "src" / "lib.rs"
WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml"
STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(")
TYPE = "T:LibreMetaverse.Rendering.MeshFoundry"
MEMBERS = {
"M:LibreMetaverse.Rendering.MeshFoundry.#ctor",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])",
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)",
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])",
"M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)",
"M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])",
}
def require_markers(path: Path, markers: tuple[str, ...]) -> None:
text = path.read_text()
missing = [marker for marker in markers if marker not in text]
if missing:
raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing))
def main() -> None:
if generate_api_shims.NATIVE_DECLARATIONS.get(TYPE) != "crate::mesh_foundry::MeshFoundry":
raise SystemExit("issue 77 MeshFoundry native declaration is missing")
missing = sorted(MEMBERS - set(generate_api_shims.NATIVE_MEMBER_BODIES))
if missing:
raise SystemExit("issue 77 native members missing: " + ", ".join(missing))
for source in (SOURCE, GENERATED):
if STUB_RE.search(source.read_text()):
raise SystemExit(f"issue 77 owned Rust stubs remain in {source.name}")
require_markers(SOURCE, (
"impl IRendering for MeshFoundry", "SimpleRenderer", "SculptMesh::from_height_rows",
"deserialize_llsd_binary_prefix", "ZlibDecoder", "select_lod", "decode_submesh",
"TexCoord1", "Tangent", "generate_normals", "generate_tangents", "MeshFaceAux",
"decode_skin", "decode_weights", "apply_sculpt_modifiers", "texture_for_face",
"MAX_ASSET_BYTES", "MAX_TOTAL_VERTICES", "MAX_JOINTS", "checked_add",
"mesh triangle index is out of range", "Error::Rendering",
))
manifest = MANIFEST.read_text()
required_deps = (
"flate2", "libremetaverse-prim-mesher", "libremetaverse-rendering-simple",
"libremetaverse-structured-data", "stats_alloc",
)
missing_deps = [dependency for dependency in required_deps if dependency not in manifest]
if missing_deps:
raise SystemExit("issue 77 dependencies/evidence missing: " + ", ".join(missing_deps))
require_markers(ASSET_TESTS, (
"model_workflow_asset_round_trips_and_falls_back_deterministically",
"rigged_mesh_preserves_skin_uv_tangent_material_and_ordering_domains",
"RiggedSkinMath::build_inv_bind_matrices", "compressed_submesh_decode_has_stable_golden_output",
"0xcb32_07be_6dea_73c8", "rebases_indices_in_face_order",
"convex_hulls_decode_bounding_and_decomposed_domains",
"malformed_offsets_indices_and_weights_return_source_context",
"mirror_and_invert_update_coordinates_normals_and_tangents",
))
require_markers(COMPAT, (
"reviewed_meshfoundrytests_generatesimplemesh_box_returnsnonnull",
"reviewed_meshfoundrytests_generatefacetedmesh_box_has6faces",
"reviewed_meshfoundrytests_transformtexcoords_identitytransform_preservesuvs",
"reviewed_meshfoundrytests_terrainmesh_allindicesinrange",
))
require_markers(BENCH, ("fixture(240)", "57_600", "342_726", "Region::new"))
require_markers(WORKSPACE, ("[profile.benchmark]", 'opt-level = 1', "incremental = false"))
require_markers(STRUCTURED, ("pub fn deserialize_llsd_binary_prefix", "binary::deserialize_prefix"))
require_markers(DOC, (
"prim-local coordinates", "MeshFaceAux", "bitangent", "UV1", "RiggedSkinMath",
"exactly-four-influence", "XOR winding", "64 MiB", "65,536", "6,000,000",
"source primitive UUID", "57,600-vertex",
))
require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_77.py",))
print(
"issue 77 audit: native prim/sculpt/terrain and packed mesh pipelines, bounded LLSD/zlib, "
"LOD, normals/tangents/UVs, materials, rigging, convex data, tests, benchmark, and docs are present"
)
if __name__ == "__main__":
main()

View File

@@ -504,9 +504,37 @@ NATIVE_DECLARATIONS = {
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
"T:LibreMetaverse.Rendering.SimpleRenderer": "crate::simple_renderer::SimpleRenderer",
"T:LibreMetaverse.Rendering.MeshFoundry": "crate::mesh_foundry::MeshFoundry",
}
NATIVE_MEMBER_BODIES = {
"M:LibreMetaverse.Rendering.MeshFoundry.#ctor": "Self::native_new()",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_faceted_mesh(prim, lod)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[])":
"self.native_generate_faceted_mesh_mesh(prim, mesh_data, libremetaverse::rendering::DetailLevel::Highest)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedMeshMesh(LibreMetaverse.Primitive,System.Byte[],LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_faceted_mesh_mesh(prim, mesh_data, lod)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_simple_mesh(prim, lod, false)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleMeshWithNormals(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_simple_mesh(prim, lod, true)",
"M:LibreMetaverse.Rendering.MeshFoundry.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)",
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[])":
"self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, None)",
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsConvexHulls(LibreMetaverse.Primitive,System.Byte[],System.Collections.Generic.List{LibreMetaverse.Vector3}@)":
"self.native_mesh_sub_mesh_as_convex_hulls(prim, compressed_mesh_data, Some(bounding_hull))",
"M:LibreMetaverse.Rendering.MeshFoundry.MeshSubMeshAsSimpleMesh(LibreMetaverse.Primitive,System.Byte[])":
"self.native_mesh_sub_mesh_as_simple_mesh(prim, compressed_mesh_data)",
"M:LibreMetaverse.Rendering.MeshFoundry.TerrainMesh(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single)":
"self.native_terrain_mesh(z_map, x_begin, x_end, y_begin, y_end)",
"M:LibreMetaverse.Rendering.MeshFoundry.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)":
"self.native_transform_tex_coords(vertices, center, te_face, prim_scale)",
"M:LibreMetaverse.Rendering.MeshFoundry.UnpackMesh(System.Byte[])":
"self.native_unpack_mesh(asset_data).map(Some)",
"M:LibreMetaverse.Rendering.SimpleRenderer.#ctor": "Self::native_new()",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_faceted_mesh(prim, lod)",