Implement native SimpleRenderer reference pipeline (#76)
Some checks failed
Native code generation / deterministic (push) Failing after 2m7s
Imaging and meshing gate / native (push) Failing after 4m9s
JPEG 2000 feature / linux (push) Successful in 2m55s
Native Rust workspace compile / compile (push) Failing after 6m15s
Skia feature / linux (push) Successful in 30m59s

This commit is contained in:
2026-08-10 21:50:59 +00:00
parent fdda05b53f
commit 611db567a0
16 changed files with 1306 additions and 37 deletions

View File

@@ -7,6 +7,7 @@ on:
- "tools/install_openjpeg_2_5_4.sh"
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "tools/check_milestone_10_issue_76.py"
- "**/*.rs"
- "**/Cargo.toml"
- "Cargo.lock"
@@ -16,6 +17,7 @@ on:
- "tools/install_openjpeg_2_5_4.sh"
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "tools/check_milestone_10_issue_76.py"
- "**/*.rs"
- "**/Cargo.toml"
- "Cargo.lock"
@@ -51,6 +53,7 @@ jobs:
python3 tools/check_milestone_09_issue_65.py
python3 tools/check_milestone_09_issue_66.py
python3 tools/check_milestone_09.py
python3 tools/check_milestone_10_issue_76.py
- name: Test the complete native world milestone
run: python3 tools/test_milestone_09.py
- name: Compile every workspace target with bounded memory

1
Cargo.lock generated
View File

@@ -970,6 +970,7 @@ version = "0.0.1"
dependencies = [
"libremetaverse",
"libremetaverse-imaging",
"libremetaverse-prim-mesher",
"libremetaverse-types",
]

View File

@@ -7,7 +7,7 @@ host, bind to, invoke, or ship the .NET implementation. The current
stage is a compiling structural shell: crate boundaries mirror the .NET library
projects, public C# type names have Rust declarations, every upstream NUnit
invocation has a traceable catalog entry, and every sample/tool project has a
Rust binary target. Types, StructuredData, Imaging, PrimMesher, both rendering adapters, and
Rust binary target. Types, StructuredData, Imaging, PrimMesher, the MeshFoundry rendering adapter, and
the main assembly's packet/message/asset/primitive wire-data, core
runtime/networking, avatar-facing manager, world/social/service manager, RLV,
LSL tools, Utilities, Vivox, and WebRTC slices now have complete callable,
@@ -441,3 +441,15 @@ application forms with stable escaping, while interest-list modes and unknown
simulator features are preserved through LLSD capabilities. The cache,
pagination, cancellation, and compatibility contracts are documented in
[`docs/discovery.md`](docs/discovery.md).
### Milestone 10
The native `SimpleRenderer` is the milestone's deterministic reference
pipeline for legacy prim and decoded sculpt-map geometry. It converts checked
PrimMesher output into shared faceted and simple meshes, preserves prim-face
texture/material metadata, supplies finite normals and UVs, applies default or
planar texture transforms, and reports bounded failures with the source
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).

View File

@@ -11,7 +11,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| `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.Simple` | 1 | 6 | callable failure-only shim |
| `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 |
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |

View File

@@ -10,6 +10,7 @@ description = "Simple renderer shims for the MetaCrate LibreMetaverse rewrite"
[dependencies]
libremetaverse = { path = "../libremetaverse" }
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
libremetaverse-prim-mesher = { path = "../libremetaverse-prim-mesher" }
libremetaverse-types = { path = "../libremetaverse-types" }
[lints]

View File

@@ -0,0 +1,58 @@
# LibreMetaverse simple renderer
`libremetaverse-rendering-simple` is the small, deterministic reference
pipeline for geometry that is already represented by a `Primitive` or a
decoded sculpt map. It has no windowing, GPU, Skia, voice, RLV, or LSL
dependency. The only geometry backend is the workspace's checked native
`libremetaverse-prim-mesher` crate.
## Supported inputs and output
`SimpleRenderer` implements both its concrete C#-mapped methods and the shared
`IRendering` trait. It supports:
- linear and flexible paths, plus circular prim paths;
- square, circular, half-circle, and all three triangular profiles;
- profile/path cuts, hollow profiles, shear, taper, twist, skew, radius offset,
and revolutions at all four `DetailLevel` values;
- plane, cylinder, sphere, and torus sculpt maps supplied as `ManagedImage`;
- faceted output grouped by prim face, and flattened simple output that keeps
each face's independent vertex/normal/UV domain;
- per-face texture-entry inheritance, including texture, legacy material, and
render-material UUIDs; and
- default and planar texture transforms with repeat, offset, rotation, and
primitive scale.
Positions are emitted in prim-local coordinates. Every face contains finite
positions, normals, and UVs, in-range triangle indices, its local bounds and
center, and its effective texture-entry face. Identical inputs produce
byte-for-byte-identical floating-point and index sequences; the compatibility
suite pins a golden high-detail box.
## Bounds and errors
Each face and flattened mesh is limited to the 16-bit index domain (65,536
vertices), and aggregate vertex/index growth uses checked arithmetic before
allocation or conversion. Non-finite construction data, malformed mesher
domains, invalid indices, unsupported topology, and budget overflow return
`Error::Rendering`. That error includes the source primitive UUID and a static
operation context so callers can identify the object that failed. Invalid
standalone texture-transform arguments return `Error::Argument` because that
API has no primitive parameter from which to derive source context.
## Deliberate boundary
A sculpt whose type is `Mesh` names an encoded mesh asset rather than a sculpt
height map. `SimpleRenderer` does not fetch or decode assets, so that input
returns a typed `mesh sculpt requires a decoded mesh asset` error. Mesh-asset
decoding, skin weights, secondary UVs, and rigged-mesh metadata belong to the
separate MeshFoundry renderer. The simple renderer also performs no world
transform, scene traversal, material shading, texture download, or drawing.
Run the focused implementation gates with:
```console
python3 tools/check_milestone_10_issue_76.py
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rendering_shims --locked -j1
CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-rendering-simple --locked -j1
```

View File

@@ -6,11 +6,11 @@
#![allow(non_snake_case)]
/// C# type: `T:LibreMetaverse.Rendering.SimpleRenderer`.
pub struct SimpleRenderer;
pub use crate::simple_renderer::SimpleRenderer;
impl SimpleRenderer {
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.#ctor`.
pub fn new() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.Rendering.SimpleRenderer.#ctor")
Self::native_new()
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_mesh(
@@ -18,9 +18,7 @@ impl SimpleRenderer {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_mesh(prim, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_faceted_sculpt_mesh(
@@ -29,9 +27,7 @@ impl SimpleRenderer {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::FacetedMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_mesh(
@@ -39,9 +35,7 @@ impl SimpleRenderer {
prim: libremetaverse::Primitive,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_mesh(prim, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)`.
pub fn generate_simple_sculpt_mesh(
@@ -50,9 +44,7 @@ impl SimpleRenderer {
sculpt_texture: libremetaverse_imaging::ManagedImage,
lod: libremetaverse::rendering::DetailLevel,
) -> Result<libremetaverse::rendering::SimpleMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
)
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
}
/// C# member: `M:LibreMetaverse.Rendering.SimpleRenderer.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)`.
pub fn transform_tex_coords(
@@ -62,9 +54,6 @@ impl SimpleRenderer {
te_face: libremetaverse::PrimitiveTextureEntryFace,
prim_scale: libremetaverse_types::Vector3,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.Rendering.SimpleRenderer.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)
}
}
impl libremetaverse::rendering::IRendering for SimpleRenderer {}

View File

@@ -1,8 +1,13 @@
//! Simple renderer corresponding to `LibreMetaverse.Rendering.Simple`.
//! Deterministic reference rendering for legacy prims and decoded sculpt maps.
//!
//! [`SimpleRenderer`] converts checked `PrimMesher` geometry into the shared
//! rendering types without a graphics backend. See the crate README for the
//! supported topology, resource bounds, and mesh-asset boundary.
extern crate self as libremetaverse_rendering_simple;
mod generated;
mod simple_renderer;
pub use generated::*;
pub use libremetaverse as core;

View File

@@ -0,0 +1,665 @@
//! Deterministic reference renderer built on the checked native prim mesher.
#![allow(clippy::cast_possible_truncation)] // Checked C# float-to-int twist conversion.
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the API mapping.
#![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped surface.
#![allow(clippy::needless_pass_by_value)] // Mapped value parameters are owned.
#![allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible in the C# API.
#![allow(clippy::unused_self)] // IRendering exposes these operations as instance methods.
use libremetaverse::rendering::{
DetailLevel, Face, FaceMask, FacetedMesh, IRendering, SimpleMesh, Vertex,
};
use libremetaverse::{MappingType, Primitive, PrimitiveTextureEntryFace};
use libremetaverse_imaging::ManagedImage;
use libremetaverse_prim_mesher::{
PathType, PrimMesh, SculptMesh, SculptMeshSculptType, VertexIndexer,
};
use libremetaverse_types::compat::Object;
use libremetaverse_types::{
Error, HoleType, PathCurve, ProfileCurve, SculptType, Vector2, Vector3,
};
const MAX_FACE_VERTICES: usize = 65_536;
const MAX_TOTAL_VERTICES: usize = MAX_FACE_VERTICES;
const MAX_TOTAL_INDICES: usize = MAX_TOTAL_VERTICES * 6;
/// Small, deterministic renderer for prim and sculpt geometry.
pub struct SimpleRenderer;
impl SimpleRenderer {
pub(crate) fn native_new() -> Result<Self, Error> {
Ok(Self)
}
pub(crate) fn native_generate_simple_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<SimpleMesh, Error> {
let source = prim.id;
let faceted = self.native_generate_faceted_mesh(prim, lod)?;
flatten_faces(faceted.faces, source)
}
pub(crate) fn native_generate_simple_sculpt_mesh(
&self,
prim: Primitive,
sculpt_texture: ManagedImage,
lod: DetailLevel,
) -> Result<SimpleMesh, Error> {
let source = prim.id;
let faceted = self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)?;
flatten_faces(faceted.faces, source)
}
pub(crate) fn native_generate_faceted_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<FacetedMesh, Error> {
let source = prim.id;
let mesh = generate_prim_mesh(&prim, lod, source)?;
let indexer = mesh
.get_vertex_indexer()
.map_err(|_| render_error(source, "index primitive viewer faces"))?
.ok_or_else(|| render_error(source, "primitive viewer faces are unavailable"))?;
let faces = indexed_faces(&prim, indexer, source)?;
Ok(FacetedMesh {
faces,
skin_data: None,
})
}
pub(crate) fn native_generate_faceted_sculpt_mesh(
&self,
prim: Primitive,
sculpt_texture: ManagedImage,
lod: DetailLevel,
) -> Result<FacetedMesh, Error> {
let source = prim.id;
let sculpt = prim
.sculpt
.as_ref()
.ok_or_else(|| render_error(source, "primitive has no sculpt metadata"))?;
let sculpt_type = match sculpt.type_() {
SculptType::Cylinder => SculptMeshSculptType::Cylinder,
SculptType::Plane => SculptMeshSculptType::Plane,
SculptType::Sphere => SculptMeshSculptType::Sphere,
SculptType::Torus => SculptMeshSculptType::Torus,
SculptType::Mesh => {
return Err(render_error(
source,
"mesh sculpt requires a decoded mesh asset",
));
}
SculptType::None | SculptType::Invert | SculptType::Mirror => {
return Err(render_error(source, "unsupported sculpt topology"));
}
};
let mesher_lod = match lod {
DetailLevel::Highest | DetailLevel::High => 32,
DetailLevel::Medium => 16,
DetailLevel::Low => 8,
};
let mesh = SculptMesh::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
sculpt_texture,
sculpt_type,
mesher_lod,
true,
sculpt.mirror(),
sculpt.invert(),
)
.map_err(|_| render_error(source, "generate bounded sculpt geometry"))?;
if mesh.coords.len() > MAX_FACE_VERTICES
|| mesh.coords.len() != mesh.normals.len()
|| mesh.coords.len() != mesh.uvs.len()
{
return Err(render_error(source, "invalid sculpt vertex domains"));
}
let vertices = mesh
.coords
.iter()
.zip(&mesh.normals)
.zip(&mesh.uvs)
.map(|((position, normal), uv)| {
checked_vertex(
Vector3 {
x: position.x,
y: position.y,
z: position.z,
},
Vector3 {
x: normal.x,
y: normal.y,
z: normal.z,
},
Vector2 { x: uv.u, y: uv.v },
source,
)
})
.collect::<Result<Vec<_>, _>>()?;
let mut indices = Vec::with_capacity(
mesh.faces
.len()
.checked_mul(3)
.ok_or_else(|| render_error(source, "sculpt index count overflow"))?,
);
for face in mesh.faces {
push_triangle(
&mut indices,
face.v1,
face.v2,
face.v3,
vertices.len(),
source,
)?;
}
let texture = texture_for_face(&prim, 0, source)?;
let face = make_face(0, vertices, indices, texture, source)?;
Ok(FacetedMesh {
faces: vec![face],
skin_data: None,
})
}
pub(crate) fn native_transform_tex_coords(
&self,
vertices: &mut Vec<Vertex>,
center: Vector3,
te_face: PrimitiveTextureEntryFace,
prim_scale: Vector3,
) -> Result<(), Error> {
if !finite3(center)
|| !finite3(prim_scale)
|| ![
te_face.repeat_u(),
te_face.repeat_v(),
te_face.offset_u(),
te_face.offset_v(),
te_face.rotation(),
]
.into_iter()
.all(f32::is_finite)
{
return Err(Error::Argument);
}
let cosine = te_face.rotation().cos();
let sine = te_face.rotation().sin();
for vertex in vertices {
if !finite3(vertex.position) || !finite3(vertex.normal) || !finite2(vertex.tex_coord) {
return Err(Error::Argument);
}
if te_face.tex_map_type() == MappingType::Planar {
let mut binormal;
let normal_x = vertex.normal.x;
if (-0.5..0.5).contains(&normal_x) {
binormal = Vector3::unit_x();
if vertex.normal.y > 0.0 {
binormal.x = -1.0;
}
} else {
binormal = Vector3::unit_y();
if normal_x < 0.0 {
binormal.y = -1.0;
}
}
let tangent = cross(binormal, vertex.normal);
let scaled = Vector3 {
x: vertex.position.x * prim_scale.x,
y: vertex.position.y * prim_scale.y,
z: vertex.position.z * prim_scale.z,
};
vertex.tex_coord.x = 0.5 + dot(binormal, scaled) * 2.0;
vertex.tex_coord.y = 0.5 - dot(tangent, scaled) * 2.0;
}
let x = vertex.tex_coord.x - 0.5;
let y = vertex.tex_coord.y - 0.5;
vertex.tex_coord.x =
(x * cosine + y * sine) * te_face.repeat_u() + te_face.offset_u() + 0.5;
vertex.tex_coord.y =
(-x * sine + y * cosine) * te_face.repeat_v() + te_face.offset_v() + 0.5;
if !finite2(vertex.tex_coord) {
return Err(Error::Argument);
}
}
Ok(())
}
}
impl IRendering for SimpleRenderer {
fn generate_faceted_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<Option<FacetedMesh>, Error> {
self.native_generate_faceted_mesh(prim, lod).map(Some)
}
fn generate_faceted_sculpt_mesh(
&self,
prim: Primitive,
sculpt_texture: ManagedImage,
lod: DetailLevel,
) -> Result<Option<FacetedMesh>, Error> {
self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)
.map(Some)
}
fn generate_simple_mesh(
&self,
prim: Primitive,
lod: DetailLevel,
) -> Result<Option<SimpleMesh>, Error> {
self.native_generate_simple_mesh(prim, lod).map(Some)
}
fn generate_simple_sculpt_mesh(
&self,
prim: Primitive,
sculpt_texture: ManagedImage,
lod: DetailLevel,
) -> Result<Option<SimpleMesh>, Error> {
self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)
.map(Some)
}
fn transform_tex_coords(
&self,
vertices: &mut Vec<Vertex>,
center: Vector3,
te_face: PrimitiveTextureEntryFace,
prim_scale: Vector3,
) -> Result<(), Error> {
self.native_transform_tex_coords(vertices, center, te_face, prim_scale)
}
}
fn generate_prim_mesh(
prim: &Primitive,
lod: DetailLevel,
source: libremetaverse_types::UUID,
) -> Result<PrimMesh, Error> {
let data = &prim.prim_data;
let parameters = [
data.profile_begin,
data.profile_end,
data.profile_hollow,
data.path_scale_x,
data.path_scale_y,
data.path_begin,
data.path_end,
data.path_shear_x,
data.path_shear_y,
data.path_radius_offset,
data.path_revolutions,
data.path_skew,
data.path_taper_x,
data.path_taper_y,
data.path_twist_begin,
data.path_twist,
];
if !parameters.into_iter().all(f32::is_finite) {
return Err(render_error(
source,
"non-finite primitive construction data",
));
}
let (mut sides, mut profile_begin, mut profile_end, sphere_mode) =
match data.profile_curve_with_property() {
ProfileCurve::Circle => (lod_sides(lod), data.profile_begin, data.profile_end, false),
ProfileCurve::EqualTriangle
| ProfileCurve::IsoTriangle
| ProfileCurve::RightTriangle => (3, data.profile_begin, data.profile_end, false),
ProfileCurve::HalfCircle => (
lod_sides(lod),
data.profile_begin.mul_add(0.5, 0.5),
data.profile_end.mul_add(0.5, 0.5),
true,
),
ProfileCurve::Square => (4, data.profile_begin, data.profile_end, false),
};
if sides < 3 {
sides = 3;
}
profile_begin = profile_begin.clamp(0.0, 1.0);
profile_end = profile_end.clamp(0.0, 1.0);
let hollow_sides = if data.profile_hole() == HoleType::Circle {
lod_sides(lod)
} else if data.profile_hole() == HoleType::Triangle {
3
} else if data.profile_hole() == HoleType::Same {
sides
} else {
4
};
let mut mesh = PrimMesh::new(
sides,
profile_begin,
profile_end,
data.profile_hollow,
hollow_sides,
)
.map_err(|_| render_error(source, "initialize primitive profile"))?;
mesh.viewer_mode = true;
mesh.sphere_mode = sphere_mode;
mesh.hole_size_x = data.path_scale_x;
mesh.hole_size_y = data.path_scale_y;
mesh.path_cut_begin = data.path_begin;
mesh.path_cut_end = data.path_end;
mesh.top_shear_x = data.path_shear_x;
mesh.top_shear_y = data.path_shear_y;
mesh.radius = data.path_radius_offset;
mesh.revolutions = data.path_revolutions;
mesh.skew = data.path_skew;
mesh.steps_per_revolution = lod_sides(lod);
let path_type = if matches!(data.path_curve, PathCurve::Line | PathCurve::Flexible) {
mesh.taper_x = 1.0 - data.path_scale_x;
mesh.taper_y = 1.0 - data.path_scale_y;
mesh.twist_begin = checked_twist(data.path_twist_begin, 180.0, source)?;
mesh.twist_end = checked_twist(data.path_twist, 180.0, source)?;
PathType::Linear
} else {
mesh.taper_x = data.path_taper_x;
mesh.taper_y = data.path_taper_y;
mesh.twist_begin = checked_twist(data.path_twist_begin, 360.0, source)?;
mesh.twist_end = checked_twist(data.path_twist, 360.0, source)?;
PathType::Circular
};
mesh.extrude(path_type)
.map_err(|_| render_error(source, "extrude primitive geometry"))?;
if mesh.coords.len() > MAX_TOTAL_VERTICES || mesh.viewer_faces.len() > MAX_TOTAL_INDICES / 3 {
return Err(render_error(
source,
"primitive geometry exceeds renderer bounds",
));
}
Ok(mesh)
}
fn indexed_faces(
prim: &Primitive,
indexer: VertexIndexer,
source: libremetaverse_types::UUID,
) -> Result<Vec<Face>, Error> {
let count = usize::try_from(indexer.num_prim_faces)
.map_err(|_| render_error(source, "invalid primitive face count"))?;
if count != indexer.viewer_vertices.len() || count != indexer.viewer_polygons.len() {
return Err(render_error(source, "misaligned primitive face domains"));
}
let mut total_vertices = 0usize;
let mut total_indices = 0usize;
let mut faces = Vec::with_capacity(count);
for face_index in 0..count {
let source_vertices = &indexer.viewer_vertices[face_index];
let polygons = indexer.viewer_polygons[face_index]
.as_ref()
.ok_or_else(|| render_error(source, "primitive face has no polygon domain"))?;
if source_vertices.is_empty() {
if polygons.is_empty() {
continue;
}
return Err(render_error(
source,
"primitive face polygons have no vertex domain",
));
}
if source_vertices.len() > MAX_FACE_VERTICES {
return Err(render_error(
source,
"primitive face exceeds 16-bit vertex range",
));
}
total_vertices = total_vertices
.checked_add(source_vertices.len())
.ok_or_else(|| render_error(source, "primitive vertex count overflow"))?;
if total_vertices > MAX_TOTAL_VERTICES {
return Err(render_error(source, "primitive vertex budget exceeded"));
}
let vertices = source_vertices
.iter()
.map(|vertex| {
checked_vertex(
Vector3 {
x: vertex.v.x,
y: vertex.v.y,
z: vertex.v.z,
},
Vector3 {
x: vertex.n.x,
y: vertex.n.y,
z: vertex.n.z,
},
Vector2 {
x: vertex.uv.u,
y: 1.0 - vertex.uv.v,
},
source,
)
})
.collect::<Result<Vec<_>, _>>()?;
let mut indices = Vec::with_capacity(
polygons
.len()
.checked_mul(3)
.ok_or_else(|| render_error(source, "primitive index count overflow"))?,
);
for polygon in polygons {
if polygon.v1 == polygon.v2 || polygon.v1 == polygon.v3 || polygon.v2 == polygon.v3 {
continue;
}
push_triangle(
&mut indices,
polygon.v1,
polygon.v2,
polygon.v3,
vertices.len(),
source,
)?;
}
total_indices = total_indices
.checked_add(indices.len())
.ok_or_else(|| render_error(source, "primitive index count overflow"))?;
if total_indices > MAX_TOTAL_INDICES {
return Err(render_error(source, "primitive index budget exceeded"));
}
let texture = texture_for_face(prim, face_index, source)?;
faces.push(make_face(
i32::try_from(face_index)
.map_err(|_| render_error(source, "primitive face id overflow"))?,
vertices,
indices,
texture,
source,
)?);
}
Ok(faces)
}
fn flatten_faces(
faces: Vec<Face>,
source: libremetaverse_types::UUID,
) -> Result<SimpleMesh, Error> {
let mut vertices = Vec::new();
let mut indices = Vec::new();
for face in faces {
let base = vertices.len();
let combined = base
.checked_add(face.vertices.len())
.ok_or_else(|| render_error(source, "simple mesh vertex count overflow"))?;
if combined > MAX_TOTAL_VERTICES {
return Err(render_error(
source,
"simple mesh exceeds 16-bit vertex range",
));
}
for index in face.indices {
let global = base
.checked_add(usize::from(index))
.ok_or_else(|| render_error(source, "simple mesh index overflow"))?;
if global >= combined {
return Err(render_error(source, "simple mesh index is out of range"));
}
indices.push(
u16::try_from(global)
.map_err(|_| render_error(source, "simple mesh index exceeds 16 bits"))?,
);
}
vertices.extend(face.vertices);
}
if indices.len() > MAX_TOTAL_INDICES {
return Err(render_error(source, "simple mesh index budget exceeded"));
}
Ok(SimpleMesh { indices, vertices })
}
fn texture_for_face(
prim: &Primitive,
index: usize,
source: libremetaverse_types::UUID,
) -> Result<PrimitiveTextureEntryFace, Error> {
let Some(textures) = prim.textures.as_ref() else {
return Ok(PrimitiveTextureEntryFace::default());
};
let index =
u32::try_from(index).map_err(|_| render_error(source, "texture face index overflow"))?;
textures
.get_face(index)
.map_err(|_| render_error(source, "texture face index is out of range"))
.map(|face| face.cloned().unwrap_or_default())
}
fn make_face(
id: i32,
vertices: Vec<Vertex>,
indices: Vec<u16>,
texture_face: PrimitiveTextureEntryFace,
source: libremetaverse_types::UUID,
) -> Result<Face, Error> {
let first = vertices
.first()
.ok_or_else(|| render_error(source, "render face has no vertices"))?;
let mut min = first.position;
let mut max = first.position;
for vertex in &vertices[1..] {
min.x = min.x.min(vertex.position.x);
min.y = min.y.min(vertex.position.y);
min.z = min.z.min(vertex.position.z);
max.x = max.x.max(vertex.position.x);
max.y = max.y.max(vertex.position.y);
max.z = max.z.max(vertex.position.z);
}
let center = Vector3 {
x: (min.x + max.x) * 0.5,
y: (min.y + max.y) * 0.5,
z: (min.z + max.z) * 0.5,
};
Ok(Face {
begin_s: 0,
begin_t: 0,
center,
edge: Vec::new(),
id,
indices,
mask: FaceMask::SINGLE,
max_extent: max,
min_extent: min,
normalized_scale: Vector3 {
x: max.x - min.x,
y: max.y - min.y,
z: max.z - min.z,
},
num_s: 0,
num_t: 0,
tex_coords1: None,
texture_face,
user_data: Object::Undefined,
vertices,
weights: None,
})
}
fn checked_vertex(
position: Vector3,
normal: Vector3,
tex_coord: Vector2,
source: libremetaverse_types::UUID,
) -> Result<Vertex, Error> {
if !finite3(position) || !finite3(normal) || !finite2(tex_coord) {
return Err(render_error(
source,
"renderer produced a non-finite vertex",
));
}
Ok(Vertex {
position,
normal,
tex_coord,
})
}
fn push_triangle(
indices: &mut Vec<u16>,
v1: i32,
v2: i32,
v3: i32,
vertex_count: usize,
source: libremetaverse_types::UUID,
) -> Result<(), Error> {
for index in [v1, v2, v3] {
let index =
usize::try_from(index).map_err(|_| render_error(source, "negative renderer index"))?;
if index >= vertex_count {
return Err(render_error(source, "renderer index is out of range"));
}
indices.push(
u16::try_from(index)
.map_err(|_| render_error(source, "renderer index exceeds 16 bits"))?,
);
}
Ok(())
}
fn checked_twist(
value: f32,
multiplier: f32,
source: libremetaverse_types::UUID,
) -> Result<i32, Error> {
let degrees = value * multiplier;
if !degrees.is_finite() || degrees.abs() > 360_000.0 {
return Err(render_error(source, "primitive twist is out of range"));
}
Ok(degrees as i32)
}
const fn lod_sides(lod: DetailLevel) -> i32 {
match lod {
DetailLevel::Low => 6,
DetailLevel::Medium => 12,
DetailLevel::High | DetailLevel::Highest => 24,
}
}
const fn render_error(source: libremetaverse_types::UUID, context: &'static str) -> Error {
Error::Rendering { source, context }
}
const fn finite2(value: Vector2) -> bool {
value.x.is_finite() && value.y.is_finite()
}
const fn finite3(value: Vector3) -> bool {
value.x.is_finite() && value.y.is_finite() && value.z.is_finite()
}
const fn dot(left: Vector3, right: Vector3) -> f32 {
left.x * right.x + left.y * right.y + left.z * right.z
}
const fn cross(left: Vector3, right: Vector3) -> Vector3 {
Vector3 {
x: left.y * right.z - left.z * right.y,
y: left.z * right.x - left.x * right.z,
z: left.x * right.y - left.y * right.x,
}
}

View File

@@ -0,0 +1,117 @@
use libremetaverse::rendering::{DetailLevel, SimpleMesh};
use libremetaverse::{
Primitive, PrimitiveConstructionData, PrimitiveSculptData, PrimitiveTextureEntry,
};
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
use libremetaverse_rendering_simple::SimpleRenderer;
use libremetaverse_types::{HoleType, PCode, PathCurve, ProfileCurve, SculptType, UUID};
fn primitive(profile: ProfileCurve, path: PathCurve) -> Primitive {
let mut primitive = Primitive::new_with_constructor().expect("Primitive constructor");
let mut data =
PrimitiveConstructionData::new_with_constructor().expect("ConstructionData constructor");
data.p_code = PCode::Prim;
data.path_curve = path;
data.set_profile_curve_with_property(profile);
data.path_scale_x = 0.8;
data.path_scale_y = 0.7;
data.path_begin = 0.05;
data.path_end = 0.95;
data.profile_begin = 0.03;
data.profile_end = 0.97;
data.profile_hollow = 0.15;
data.set_profile_hole(HoleType::Circle);
data.path_shear_x = 0.1;
data.path_shear_y = -0.1;
data.path_revolutions = 1.25;
data.path_twist = 0.1;
primitive.prim_data = data;
primitive.textures = Some(
PrimitiveTextureEntry::new_with_uuid(UUID::zero()).expect("texture entry constructor"),
);
primitive
}
fn assert_valid(mesh: &SimpleMesh) {
assert!(!mesh.vertices.is_empty());
assert!(!mesh.indices.is_empty());
assert_eq!(mesh.indices.len() % 3, 0);
for index in &mesh.indices {
assert!(usize::from(*index) < mesh.vertices.len());
}
for vertex in &mesh.vertices {
assert!(vertex.position.x.is_finite());
assert!(vertex.position.y.is_finite());
assert!(vertex.position.z.is_finite());
assert!(vertex.normal.x.is_finite());
assert!(vertex.normal.y.is_finite());
assert!(vertex.normal.z.is_finite());
assert!(vertex.tex_coord.x.is_finite());
assert!(vertex.tex_coord.y.is_finite());
}
}
fn sculpt_image() -> ManagedImage {
let mut image = ManagedImage::new(32, 32, ManagedImageImageChannels::COLOR)
.expect("sculpt image constructor");
for (index, red) in image.red.iter_mut().enumerate() {
*red = u8::try_from(index % 256).expect("bounded sample");
image.green[index] = u8::try_from((index * 3) % 256).expect("bounded sample");
image.blue[index] = u8::try_from((index * 7) % 256).expect("bounded sample");
}
image
}
#[test]
fn every_profile_path_and_lod_produces_checked_geometry() {
let renderer = SimpleRenderer::new().expect("renderer");
for profile in [
ProfileCurve::Circle,
ProfileCurve::EqualTriangle,
ProfileCurve::HalfCircle,
ProfileCurve::IsoTriangle,
ProfileCurve::RightTriangle,
ProfileCurve::Square,
] {
for path in [PathCurve::Flexible, PathCurve::Line, PathCurve::Circle] {
for lod in [
DetailLevel::Low,
DetailLevel::Medium,
DetailLevel::High,
DetailLevel::Highest,
] {
let mesh = renderer
.generate_simple_mesh(primitive(profile, path), lod)
.expect("supported prim topology");
assert_valid(&mesh);
}
}
}
}
#[test]
fn every_sculpt_topology_produces_checked_geometry_at_each_lod() {
let renderer = SimpleRenderer::new().expect("renderer");
for sculpt_type in [
SculptType::Plane,
SculptType::Cylinder,
SculptType::Sphere,
SculptType::Torus,
] {
for lod in [
DetailLevel::Low,
DetailLevel::Medium,
DetailLevel::High,
DetailLevel::Highest,
] {
let mut prim = primitive(ProfileCurve::Square, PathCurve::Line);
let mut sculpt = PrimitiveSculptData::new_with_constructor().expect("sculpt metadata");
sculpt.set_type_(sculpt_type);
prim.sculpt = Some(sculpt);
let mesh = renderer
.generate_simple_sculpt_mesh(prim, sculpt_image(), lod)
.expect("supported sculpt topology");
assert_valid(&mesh);
}
}
}

View File

@@ -68,6 +68,13 @@ pub enum Error {
HttpRequest,
/// A host lookup or socket operation failed.
Socket,
/// Rendering failed while processing one source object at a named stage.
Rendering {
/// UUID of the primitive whose geometry could not be produced.
source: crate::UUID,
/// Stable stage suitable for diagnostics and fixture assertions.
context: &'static str,
},
}
impl Error {
@@ -83,7 +90,8 @@ impl Error {
| Self::Parse { .. }
| Self::Cancelled
| Self::HttpRequest
| Self::Socket => None,
| Self::Socket
| Self::Rendering { .. } => None,
}
}
}
@@ -110,6 +118,9 @@ impl fmt::Display for Error {
Self::Cancelled => formatter.write_str("operation was cancelled"),
Self::HttpRequest => formatter.write_str("HTTP request failed"),
Self::Socket => formatter.write_str("socket operation failed"),
Self::Rendering { source, context } => {
write!(formatter, "rendering object {source} failed: {context}")
}
}
}
}

View File

@@ -4,8 +4,11 @@ Milestone 06 owns the native implementations in `libremetaverse-imaging`,
`libremetaverse-imaging-skia`, and `libremetaverse-prim-mesher`. The core
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: this boundary does not hide or
reclassify their future `MeshFoundry` and `SimpleRenderer` work.
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
[`SimpleRenderer` guide](../crates/libremetaverse-rendering-simple/README.md)
for that boundary.
## Feature boundary

View File

@@ -1,8 +1,115 @@
use libremetaverse::Primitive;
use libremetaverse::rendering::{DetailLevel, SimpleMesh};
#![allow(clippy::float_cmp, clippy::too_many_lines)]
use libremetaverse::rendering::{DetailLevel, IRendering, SimpleMesh, Vertex};
use libremetaverse::{
MappingType, Primitive, PrimitiveConstructionData, PrimitiveSculptData, PrimitiveTextureEntry,
PrimitiveTextureEntryFace,
};
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
use libremetaverse_prim_mesher::PrimMesh;
use libremetaverse_rendering_mesh_foundry::MeshFoundry;
use libremetaverse_rendering_simple::SimpleRenderer;
use libremetaverse_types::{
Error, HoleType, PCode, PathCurve, ProfileCurve, SculptType, UUID, Vector2, Vector3,
};
fn uuid(value: &str) -> UUID {
UUID::new_with_string(value.into()).expect("fixture UUID")
}
fn primitive(profile: ProfileCurve, path: PathCurve, path_scale_y: f32) -> Primitive {
let mut primitive = Primitive::new_with_constructor().expect("Primitive constructor");
let mut data =
PrimitiveConstructionData::new_with_constructor().expect("ConstructionData constructor");
data.p_code = PCode::Prim;
data.path_curve = path;
data.set_profile_curve_with_property(profile);
data.path_scale_x = 1.0;
data.path_scale_y = path_scale_y;
data.path_begin = 0.0;
data.path_end = 1.0;
data.profile_begin = 0.0;
data.profile_end = 1.0;
data.profile_hollow = 0.0;
data.set_profile_hole(HoleType::Same);
data.path_revolutions = 1.0;
primitive.prim_data = data;
primitive
}
fn box_primitive() -> Primitive {
primitive(ProfileCurve::Square, PathCurve::Line, 1.0)
}
fn mesh_hash(mesh: &SimpleMesh) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
let mut mix = |value: u32| {
hash ^= u64::from(value);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
};
mix(u32::try_from(mesh.vertices.len()).expect("fixture vertex count"));
mix(u32::try_from(mesh.indices.len()).expect("fixture index count"));
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,
] {
mix(value.to_bits());
}
}
for index in &mesh.indices {
mix(u32::from(*index));
}
hash
}
fn assert_valid_mesh(mesh: &SimpleMesh) {
assert!(!mesh.vertices.is_empty());
assert!(!mesh.indices.is_empty());
assert_eq!(mesh.indices.len() % 3, 0);
for index in &mesh.indices {
assert!(usize::from(*index) < mesh.vertices.len());
}
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,
] {
assert!(value.is_finite());
}
}
}
fn sculpt_image() -> ManagedImage {
let mut image = ManagedImage::new(8, 8, ManagedImageImageChannels::COLOR)
.expect("sculpt image constructor");
for index in 0_u8..64 {
image.red[usize::from(index)] = index.wrapping_mul(3);
image.green[usize::from(index)] = index.wrapping_mul(5);
image.blue[usize::from(index)] = index.wrapping_mul(7);
}
image
}
fn vertex(x: f32, y: f32, normal: Vector3, position: Vector3) -> Vertex {
Vertex {
position,
normal,
tex_coord: Vector2 { x, y },
}
}
#[test]
fn mesh_and_renderer_calls_compile_with_typed_elements() {
@@ -23,17 +130,205 @@ fn mesh_and_renderer_calls_compile_with_typed_elements() {
}
#[test]
fn native_mesher_and_remaining_shims_report_their_current_status() {
let mesh = PrimMesh::new(4, 0.0, 1.0, 0.0, 4)
.expect("native PrimMesh constructor should create a checked mesh");
assert!(mesh.coords.is_empty());
assert!(mesh.faces.is_empty());
let Err(renderer_error) = SimpleRenderer::new() else {
panic!("failure-only renderer constructor unexpectedly returned a value");
};
fn simple_renderer_box_pipeline_is_deterministic_and_matches_golden_mesh() {
let renderer = SimpleRenderer::new().expect("SimpleRenderer constructor");
let faceted = renderer
.generate_faceted_mesh(box_primitive(), DetailLevel::High)
.expect("faceted box");
assert_eq!(faceted.faces.len(), 6);
assert_eq!(
renderer_error.csharp_member(),
Some("M:LibreMetaverse.Rendering.SimpleRenderer.#ctor")
faceted
.faces
.iter()
.map(|face| face.vertices.len())
.sum::<usize>(),
24
);
assert_eq!(
faceted
.faces
.iter()
.map(|face| face.indices.len())
.sum::<usize>(),
36
);
for face in &faceted.faces {
assert!(face.min_extent.x <= face.max_extent.x);
assert!(face.min_extent.y <= face.max_extent.y);
assert!(face.min_extent.z <= face.max_extent.z);
}
let first = renderer
.generate_simple_mesh(box_primitive(), DetailLevel::High)
.expect("first simple box");
let second = renderer
.generate_simple_mesh(box_primitive(), DetailLevel::High)
.expect("second simple box");
assert_valid_mesh(&first);
assert_eq!(first.vertices.len(), 24);
assert_eq!(first.indices.len(), 36);
assert_eq!(mesh_hash(&first), mesh_hash(&second));
assert_eq!(mesh_hash(&first), 0x3d2b_10e3_7da0_3d2e);
}
#[test]
fn simple_renderer_maps_default_and_per_face_texture_metadata() {
let default_texture = uuid("11111111-1111-1111-1111-111111111111");
let override_texture = uuid("22222222-2222-2222-2222-222222222222");
let material = uuid("33333333-3333-3333-3333-333333333333");
let render_material = uuid("44444444-4444-4444-4444-444444444444");
let mut textures =
PrimitiveTextureEntry::new_with_uuid(default_texture).expect("texture entry constructor");
let face = textures.create_face(0).expect("face override");
face.set_texture_id(override_texture);
face.set_material_id(material);
face.set_render_material_id(render_material);
let mut prim = box_primitive();
prim.textures = Some(textures);
let mesh = SimpleRenderer::new()
.expect("renderer")
.generate_faceted_mesh(prim, DetailLevel::Medium)
.expect("textured box");
assert_eq!(mesh.faces[0].texture_face.texture_id(), override_texture);
assert_eq!(mesh.faces[0].texture_face.material_id(), material);
assert_eq!(
mesh.faces[0].texture_face.render_material_id(),
render_material
);
assert_eq!(mesh.faces[1].texture_face.texture_id(), default_texture);
}
#[test]
fn simple_renderer_transforms_default_and_planar_texture_coordinates() {
let renderer = SimpleRenderer::new().expect("renderer");
let mut face = PrimitiveTextureEntryFace::new(None).expect("texture face");
face.set_repeat_u(2.0);
face.set_repeat_v(3.0);
face.set_offset_u(0.1);
face.set_offset_v(-0.2);
face.set_rotation(0.0);
face.set_tex_map_type(MappingType::Default);
let mut vertices = vec![vertex(0.25, 0.75, Vector3::unit_z(), Vector3::zero())];
renderer
.transform_tex_coords(
&mut vertices,
Vector3::zero(),
face,
Vector3 {
x: 1.0,
y: 1.0,
z: 1.0,
},
)
.expect("default texture transform");
assert!((vertices[0].tex_coord.x - 0.1).abs() < 1.0e-6);
assert!((vertices[0].tex_coord.y - 1.05).abs() < 1.0e-6);
let mut planar = PrimitiveTextureEntryFace::new(None).expect("planar face");
planar.set_tex_map_type(MappingType::Planar);
let mut vertices = vec![vertex(
99.0,
99.0,
Vector3::unit_z(),
Vector3 {
x: 0.25,
y: -0.25,
z: 0.5,
},
)];
renderer
.transform_tex_coords(
&mut vertices,
Vector3::zero(),
planar,
Vector3 {
x: 2.0,
y: 4.0,
z: 1.0,
},
)
.expect("planar texture transform");
assert!((vertices[0].tex_coord.x - 1.5).abs() < 1.0e-6);
assert!((vertices[0].tex_coord.y - -1.5).abs() < 1.0e-6);
}
#[test]
fn simple_renderer_generates_deterministic_sculpt_geometry() {
let mut prim = box_primitive();
let mut sculpt = PrimitiveSculptData::new_with_constructor().expect("sculpt metadata");
sculpt.set_type_(SculptType::Plane);
prim.sculpt = Some(sculpt);
let renderer = SimpleRenderer::new().expect("renderer");
let first = renderer
.generate_simple_sculpt_mesh(prim.clone(), sculpt_image(), DetailLevel::Low)
.expect("first sculpt mesh");
let second = renderer
.generate_simple_sculpt_mesh(prim, sculpt_image(), DetailLevel::Low)
.expect("second sculpt mesh");
assert_valid_mesh(&first);
assert_eq!(mesh_hash(&first), mesh_hash(&second));
}
#[test]
fn simple_renderer_trait_dispatch_returns_geometry() {
let renderer = SimpleRenderer::new().expect("renderer");
let mesh = IRendering::generate_simple_mesh(&renderer, box_primitive(), DetailLevel::Low)
.expect("trait call")
.expect("renderer returned geometry");
assert_valid_mesh(&mesh);
}
#[test]
fn simple_renderer_reports_source_context_for_invalid_and_mesh_sculpt_inputs() {
let renderer = SimpleRenderer::new().expect("renderer");
let source = uuid("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
let mut invalid = box_primitive();
invalid.id = source;
invalid.prim_data.path_end = f32::NAN;
assert!(matches!(
renderer.generate_simple_mesh(invalid, DetailLevel::High),
Err(Error::Rendering {
source: actual,
context: "non-finite primitive construction data"
}) if actual == source
));
let mut mesh_sculpt = box_primitive();
mesh_sculpt.id = source;
let mut sculpt = PrimitiveSculptData::new_with_constructor().expect("sculpt metadata");
sculpt.set_type_(SculptType::Mesh);
mesh_sculpt.sculpt = Some(sculpt);
assert!(matches!(
renderer.generate_simple_sculpt_mesh(
mesh_sculpt,
sculpt_image(),
DetailLevel::High,
),
Err(Error::Rendering {
source: actual,
context: "mesh sculpt requires a decoded mesh asset"
}) if actual == source
));
}
#[test]
fn simple_renderer_rejects_non_finite_texture_inputs() {
let renderer = SimpleRenderer::new().expect("renderer");
let mut face = PrimitiveTextureEntryFace::new(None).expect("texture face");
face.set_repeat_u(f32::INFINITY);
let mut vertices = vec![vertex(0.0, 0.0, Vector3::unit_z(), Vector3::zero())];
assert_eq!(
renderer.transform_tex_coords(
&mut vertices,
Vector3::zero(),
face,
Vector3 {
x: 1.0,
y: 1.0,
z: 1.0
},
),
Err(Error::Argument)
);
}

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Audit issue 76's native SimpleRenderer 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-simple" / "src" / "simple_renderer.rs"
GENERATED = ROOT / "crates" / "libremetaverse-rendering-simple" / "src" / "generated.rs"
MANIFEST = ROOT / "crates" / "libremetaverse-rendering-simple" / "Cargo.toml"
COMPAT = ROOT / "tests" / "compat" / "tests" / "rendering_shims.rs"
REFERENCE = (
ROOT
/ "crates"
/ "libremetaverse-rendering-simple"
/ "tests"
/ "reference_pipeline.rs"
)
DOC = ROOT / "crates" / "libremetaverse-rendering-simple" / "README.md"
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.SimpleRenderer"
MEMBERS = {
"M:LibreMetaverse.Rendering.SimpleRenderer.#ctor",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)",
"M:LibreMetaverse.Rendering.SimpleRenderer.TransformTexCoords(System.Collections.Generic.List{LibreMetaverse.Rendering.Vertex},LibreMetaverse.Vector3,LibreMetaverse.Primitive.TextureEntryFace,LibreMetaverse.Vector3)",
}
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::simple_renderer::SimpleRenderer"
):
raise SystemExit("issue 76 SimpleRenderer native declaration is missing")
missing = sorted(MEMBERS - set(generate_api_shims.NATIVE_MEMBER_BODIES))
if missing:
raise SystemExit("issue 76 native members missing: " + ", ".join(missing))
for source in (SOURCE, GENERATED):
if STUB_RE.search(source.read_text()):
raise SystemExit(f"issue 76 owned Rust stubs remain in {source.name}")
require_markers(SOURCE, (
"impl IRendering for SimpleRenderer", "PrimMesh", "SculptMesh",
"MAX_FACE_VERTICES", "checked_add", "u16::try_from", "checked_vertex",
"texture_for_face", "MappingType::Planar", "Error::Rendering",
"mesh sculpt requires a decoded mesh asset",
))
manifest = MANIFEST.read_text()
if "libremetaverse-prim-mesher" not in manifest:
raise SystemExit("issue 76 must depend on the checked PrimMesher crate")
forbidden = ("skia", "voice", "libremetaverse-rlv", "libremetaverse-lsl")
leaked = [name for name in forbidden if name in manifest.lower()]
if leaked:
raise SystemExit("issue 76 dependency boundary leaked: " + ", ".join(leaked))
require_markers(COMPAT, (
"matches_golden_mesh", "0x3d2b_10e3_7da0_3d2e",
"maps_default_and_per_face_texture_metadata", "transforms_default_and_planar",
"generates_deterministic_sculpt_geometry", "trait_dispatch_returns_geometry",
"reports_source_context", "rejects_non_finite_texture_inputs",
))
require_markers(REFERENCE, (
"every_profile_path_and_lod_produces_checked_geometry",
"ProfileCurve::RightTriangle", "PathCurve::Flexible", "DetailLevel::Highest",
"every_sculpt_topology_produces_checked_geometry_at_each_lod",
"SculptType::Cylinder", "SculptType::Sphere", "SculptType::Torus",
))
require_markers(DOC, (
"all four `DetailLevel`", "65,536", "Error::Rendering",
"source primitive UUID", "MeshFoundry", "prim-local coordinates",
))
require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_76.py",))
print(
"issue 76 audit: native prim/sculpt conversion, face metadata, texture mapping, "
"bounded errors, deterministic evidence, dependency isolation, and docs are present"
)
if __name__ == "__main__":
main()

View File

@@ -503,9 +503,21 @@ NATIVE_DECLARATIONS = {
"T:LibreMetaverse.Settings": "crate::client_core::Settings",
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
"T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser",
"T:LibreMetaverse.Rendering.SimpleRenderer": "crate::simple_renderer::SimpleRenderer",
}
NATIVE_MEMBER_BODIES = {
"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)",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateFacetedSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_faceted_sculpt_mesh(prim, sculpt_texture, lod)",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleMesh(LibreMetaverse.Primitive,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_simple_mesh(prim, lod)",
"M:LibreMetaverse.Rendering.SimpleRenderer.GenerateSimpleSculptMesh(LibreMetaverse.Primitive,LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.Rendering.DetailLevel)":
"self.native_generate_simple_sculpt_mesh(prim, sculpt_texture, lod)",
"M:LibreMetaverse.Rendering.SimpleRenderer.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)",
"F:LibreMetaverse.Assets.ArchiveConstants.ASSET_TYPE_TO_EXTENSION":
"crate::asset_archive::asset_type_to_extension()",
"F:LibreMetaverse.Assets.ArchiveConstants.EXTENSION_TO_ASSET_TYPE":

View File

@@ -1029,6 +1029,7 @@ def validate_generated_shims() -> None:
"crate::transfers::",
"crate::xml_codec::",
"stored_visual_params",
"Self::native_",
"self.native_",
"left.native_equals(",
)