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

@@ -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}")
}
}
}
}