diff --git a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs index 2466cfc..70f4549 100644 --- a/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs +++ b/crates/polymodel-renderer-worker/src/bin/ldraw_preview.rs @@ -974,10 +974,10 @@ fn run_visible_preview( let (x, y) = (select_x, select_y); let active_camera = camera .as_ref() - .ok_or_else(|| RendererError::invalid("missing-camera"))?; + .ok_or(RendererError::ParseAdaptation("missing-camera"))?; let picker = scene_picker .as_ref() - .ok_or_else(|| RendererError::invalid("missing-picker"))?; + .ok_or(RendererError::ParseAdaptation("missing-picker"))?; let viewport = active_camera.viewport(); let pixel = three_d::vec2( x as f32, diff --git a/crates/polymodel-renderer-worker/src/lib.rs b/crates/polymodel-renderer-worker/src/lib.rs index d10cd5d..c7cdcb4 100644 --- a/crates/polymodel-renderer-worker/src/lib.rs +++ b/crates/polymodel-renderer-worker/src/lib.rs @@ -152,8 +152,24 @@ pub fn basis_determinant() -> f64 { #[derive(Debug, PartialEq, Eq, thiserror::Error)] pub enum RendererError { - #[error("invalid scene data: {0}")] - InvalidScene(String), + #[error("missing geometry {0:?}")] + MissingGeometry(GeometryId), + #[error("missing material {0:?}")] + MissingMaterial(MaterialId), + #[error("missing texture {0:?}")] + MissingTexture(TextureId), + #[error("missing node {0:?}")] + MissingNode(NodeId), + #[error("duplicate geometry {0:?}")] + DuplicateGeometry(GeometryId), + #[error("index overflow: {context}")] + IndexOverflow { context: &'static str }, + #[error("invalid range: {context} (index {index}, length {len})")] + InvalidRange { + context: &'static str, + index: usize, + len: usize, + }, #[error("non-finite transform or coordinate")] NonFiniteTransform, #[error("GPU/resource failure: {0}")] @@ -161,7 +177,7 @@ pub enum RendererError { #[error("texture decode failure: {0}")] TextureDecode(String), #[error("parse/adaptation failure: {0}")] - ParseAdaptation(String), + ParseAdaptation(&'static str), } impl From for String { @@ -171,12 +187,8 @@ impl From for String { } impl RendererError { - pub fn invalid(message: impl Into) -> Self { - Self::InvalidScene(message.into()) - } - - fn parse(message: impl Into) -> Self { - Self::ParseAdaptation(message.into()) + fn parse(message: &'static str) -> Self { + Self::ParseAdaptation(message) } } @@ -357,10 +369,13 @@ where batch_index }; let batch = &mut batches[batch_index].1; - let base = u32::try_from(batch.points.len()) - .map_err(|_| RendererError::invalid("geometry index overflow"))?; - let point_count = u32::try_from(primitive.vertices.len()) - .map_err(|_| RendererError::invalid("primitive point count overflow"))?; + let base = u32::try_from(batch.points.len()).map_err(|_| RendererError::IndexOverflow { + context: "geometry index", + })?; + let point_count = + u32::try_from(primitive.vertices.len()).map_err(|_| RendererError::IndexOverflow { + context: "primitive point count", + })?; batch.primitives.push(ScenePrimitiveRange { point_start: base, point_count, @@ -496,38 +511,36 @@ fn adapt_ldraw_assembly( let mut provenance = Vec::with_capacity(models.len()); let mut texture_ids = HashMap::<(RootId, String, String), TextureId>::new(); let mut textures = Vec::::new(); - let mut texture_for = - |root: RootId, reference: &str, role: &str| -> Result { - let path = NormalizedPath::new(reference).map_err(|_| { - RendererError::invalid("TEXMAP texture reference is not a valid canonical path") - })?; - let key = (root, path.as_str().to_owned(), role.to_owned()); - if let Some(id) = texture_ids.get(&key) { - return Ok(*id); - } - let resource = resources - .and_then(|resources| texture_resource_for(resources, root, &path)) - .ok_or_else(|| { - RendererError::invalid( - "TEXMAP texture is not present in the referencing part's canonical root", - ) - })?; - resource.validate().map_err(|_| { - RendererError::TextureDecode("TEXMAP texture resource failed verification".into()) - })?; - let id = TextureId((textures.len() + 1) as u64); - textures.push(SceneTexture { - id, - provenance: ProvenanceId(1), - uri: reference.into(), - resource_key: Some(resource.key.as_str().into()), - sha256: Some(resource.sha256.clone()), - byte_length: Some(resource.byte_length), - role: Some(role.into()), - }); - texture_ids.insert(key, id); - Ok(id) - }; + let mut texture_for = |root: RootId, + reference: &str, + role: &str| + -> Result { + let path = NormalizedPath::new(reference).map_err(|_| { + RendererError::ParseAdaptation("TEXMAP texture reference is not a valid canonical path") + })?; + let key = (root, path.as_str().to_owned(), role.to_owned()); + if let Some(id) = texture_ids.get(&key) { + return Ok(*id); + } + let resource = resources + .and_then(|resources| texture_resource_for(resources, root, &path)) + .ok_or_else(|| RendererError::MissingTexture(TextureId((textures.len() + 1) as u64)))?; + resource.validate().map_err(|_| { + RendererError::TextureDecode("TEXMAP texture resource failed verification".into()) + })?; + let id = TextureId((textures.len() + 1) as u64); + textures.push(SceneTexture { + id, + provenance: ProvenanceId(1), + uri: reference.into(), + resource_key: Some(resource.key.as_str().into()), + sha256: Some(resource.sha256.clone()), + byte_length: Some(resource.byte_length), + role: Some(role.into()), + }); + texture_ids.insert(key, id); + Ok(id) + }; let texmap_available = |root: RootId, texmap: &SceneTexmapAssociation| { let Some(resources) = resources else { return false; @@ -691,10 +704,10 @@ fn adapt_ldraw_assembly( .collect::>(); let root_batches = nodes_by_model .get(&models[0].model) - .ok_or_else(|| RendererError::invalid("root model has no geometry batches"))?; + .ok_or_else(|| RendererError::ParseAdaptation("root model has no geometry batches"))?; let root_metadata = metadata_by_model .get(&models[0].model) - .ok_or_else(|| RendererError::invalid("root model has no definition metadata"))?; + .ok_or_else(|| RendererError::ParseAdaptation("root model has no definition metadata"))?; let mut occurrences = Vec::new(); for (node, primitive) in root_batches { let inherited_colour = u32::from(polymodel_ldraw_core::DEFAULT_COLOUR); @@ -742,14 +755,14 @@ fn adapt_ldraw_assembly( .cloned() .flatten(); let target_key = occurrence.target_key.as_ref().ok_or_else(|| { - RendererError::invalid("renderable occurrence missing target identity") + RendererError::ParseAdaptation("renderable occurrence missing target identity") + })?; + let batches = nodes_by_model.get(&occurrence.model).ok_or_else(|| { + RendererError::ParseAdaptation("occurrence model was not materialized") + })?; + let metadata = metadata_by_model.get(&occurrence.model).ok_or_else(|| { + RendererError::ParseAdaptation("occurrence model has no definition metadata") })?; - let batches = nodes_by_model - .get(&occurrence.model) - .ok_or_else(|| RendererError::invalid("occurrence model was not materialized"))?; - let metadata = metadata_by_model - .get(&occurrence.model) - .ok_or_else(|| RendererError::invalid("occurrence model has no definition metadata"))?; let logical_part = inherited_logical_part.or_else(|| { meaningful_part(target_key.canonical_path.as_str()).then(|| { ( @@ -770,7 +783,7 @@ fn adapt_ldraw_assembly( let source_index = geometry_indices .get(&GeometryId(node.0)) .copied() - .ok_or_else(|| RendererError::invalid("occurrence geometry is missing"))?; + .ok_or_else(|| RendererError::MissingGeometry(GeometryId(node.0)))?; let occurrence_uvs = occurrence_texmap_uvs(texmap, transform, &geometries[source_index].points)?; (*node, Some(occurrence_uvs)) @@ -992,7 +1005,7 @@ fn adapt_ldraw_assembly( }; scene .recompute_stats() - .map_err(|error| RendererError::invalid(error.to_string()))?; + .map_err(|_| RendererError::ParseAdaptation("scene statistics are invalid"))?; Ok(scene) } @@ -2434,10 +2447,7 @@ fn scene_mesh_topology_for_view_kind( if converted.iter().all(|coordinate| coordinate.is_finite()) { Ok(converted) } else { - Err(RendererError::invalid(format!( - "geometry {} contains a non-finite position", - geometry.id.0 - ))) + Err(RendererError::NonFiniteTransform) } }) .collect::, _>>()?; @@ -2456,10 +2466,11 @@ fn scene_mesh_topology_for_view_kind( .flatten() .any(|value| !value.is_finite()) { - return Err(RendererError::invalid(format!( - "geometry {} has invalid UV alignment", - geometry.id.0 - ))); + return Err(RendererError::InvalidRange { + context: "geometry UV alignment", + index: geometry.uvs.len(), + len: geometry.points.len(), + }); } else { geometry.uvs.clone() } @@ -2483,20 +2494,18 @@ fn scene_mesh_topology_for_view_kind( let start = source_positions .get(line[0] as usize) .copied() - .ok_or_else(|| { - RendererError::invalid(format!( - "geometry {} line start is outside its position domain", - geometry.id.0 - )) + .ok_or_else(|| RendererError::InvalidRange { + context: "geometry line start", + index: line[0] as usize, + len: source_positions.len(), })?; let end = source_positions .get(line[1] as usize) .copied() - .ok_or_else(|| { - RendererError::invalid(format!( - "geometry {} line end is outside its position domain", - geometry.id.0 - )) + .ok_or_else(|| RendererError::InvalidRange { + context: "geometry line end", + index: line[1] as usize, + len: source_positions.len(), })?; let direction = [ f64::from(end[0] - start[0]), @@ -2551,8 +2560,9 @@ fn scene_mesh_topology_for_view_kind( (side[1] / side_length * radius) as f32, (side[2] / side_length * radius) as f32, ]; - let base = u32::try_from(positions.len()) - .map_err(|_| RendererError::invalid("line vertex range overflow"))?; + let base = u32::try_from(positions.len()).map_err(|_| RendererError::IndexOverflow { + context: "line vertex range", + })?; positions.extend([ [ start[0] - offset[0], @@ -2578,17 +2588,19 @@ fn scene_mesh_topology_for_view_kind( if include_conditional { for conditional in &geometry.conditional_lines { let points = conditional - .iter() - .map(|index| { - geometry.points.get(*index as usize).copied().ok_or_else(|| { - RendererError::invalid(format!( - "geometry {} conditional-line index {index} is outside its local {}-position domain", - geometry.id.0, - geometry.points.len() - )) + .iter() + .map(|index| { + geometry + .points + .get(*index as usize) + .copied() + .ok_or_else(|| RendererError::InvalidRange { + context: "geometry conditional-line index", + index: *index as usize, + len: geometry.points.len(), + }) }) - }) - .collect::, _>>()?; + .collect::, _>>()?; let endpoints = [points[0], points[1]]; let controls = [points[2], points[3]]; if conditional_line_visible(endpoints, controls, view) { @@ -2597,21 +2609,22 @@ fn scene_mesh_topology_for_view_kind( } } if indices.len() % 3 != 0 { - return Err(RendererError::invalid(format!( - "geometry {} has an incomplete triangle index stream", - geometry.id.0 - ))); + return Err(RendererError::InvalidRange { + context: "geometry triangle index stream", + index: indices.len(), + len: indices.len() + (3 - indices.len() % 3) % 3, + }); } if let Some(index) = indices .iter() .copied() .find(|index| *index as usize >= positions.len()) { - return Err(RendererError::invalid(format!( - "geometry {} index {index} is outside its local {}-position domain", - geometry.id.0, - positions.len() - ))); + return Err(RendererError::InvalidRange { + context: "geometry index", + index: index as usize, + len: positions.len(), + }); } Ok(SceneMeshTopology { positions, @@ -2690,25 +2703,23 @@ impl SceneIndex { let mut geometries = std::collections::HashMap::with_capacity(scene.geometries.len()); for (index, geometry) in scene.geometries.iter().enumerate() { if geometry.id.0 == 0 { - return Err(RendererError::invalid("geometry id zero is invalid")); + return Err(RendererError::ParseAdaptation( + "geometry id zero is invalid", + )); } if geometries.insert(geometry.id, index).is_some() { - return Err(RendererError::invalid(format!( - "duplicate geometry id {}", - geometry.id.0 - ))); + return Err(RendererError::DuplicateGeometry(geometry.id)); } } let mut materials = std::collections::HashMap::with_capacity(scene.materials.len()); for (index, material) in scene.materials.iter().enumerate() { if material.id.0 == 0 { - return Err(RendererError::invalid("material id zero is invalid")); + return Err(RendererError::ParseAdaptation( + "material id zero is invalid", + )); } if materials.insert(material.id, index).is_some() { - return Err(RendererError::invalid(format!( - "duplicate material id {}", - material.id.0 - ))); + return Err(RendererError::ParseAdaptation("duplicate material id")); } } let index = Self { @@ -2727,10 +2738,7 @@ impl SceneIndex { .flatten() .any(|value| !value.is_finite()) { - return Err(RendererError::invalid(format!( - "occurrence {} has a non-finite transform", - occurrence.id.0 - ))); + return Err(RendererError::NonFiniteTransform); } } Ok(index) @@ -2745,14 +2753,14 @@ impl SceneIndex { self.geometries .get(&id) .and_then(|index| scene.geometries.get(*index)) - .ok_or_else(|| RendererError::invalid(format!("unknown geometry id {}", node.0))) + .ok_or(RendererError::MissingGeometry(id)) } fn geometry_index(&self, node: NodeId) -> Result { self.geometries .get(&GeometryId(node.0)) .copied() - .ok_or_else(|| RendererError::invalid(format!("unknown geometry id {}", node.0))) + .ok_or(RendererError::MissingGeometry(GeometryId(node.0))) } fn material<'a>( @@ -2763,7 +2771,7 @@ impl SceneIndex { self.materials .get(&id) .and_then(|index| scene.materials.get(*index)) - .ok_or_else(|| RendererError::invalid(format!("unknown material id {}", id.0))) + .ok_or(RendererError::MissingMaterial(id)) } } @@ -2784,7 +2792,11 @@ fn finalized_upload_batches( let geometry_index = index.geometry_index(batch.node)?; let topology = topologies .get(geometry_index) - .ok_or_else(|| RendererError::invalid("render plan is missing geometry topology"))?; + .ok_or(RendererError::InvalidRange { + context: "render plan geometry topology", + index: geometry_index, + len: topologies.len(), + })?; if topology.indices.is_empty() { continue; } @@ -2806,17 +2818,30 @@ fn occurrence_gpu_ranges_from_batches( let geometry_index = index.geometry_index(batch.node)?; let topology = topologies .get(geometry_index) - .ok_or_else(|| RendererError::invalid("render plan is missing geometry topology"))?; - let vertex_count = u32::try_from(topology.positions.len()) - .map_err(|_| RendererError::invalid("vertex range overflow"))?; - let index_count = u32::try_from(topology.indices.len()) - .map_err(|_| RendererError::invalid("index range overflow"))?; + .ok_or(RendererError::InvalidRange { + context: "render plan geometry topology", + index: geometry_index, + len: topologies.len(), + })?; + let vertex_count = + u32::try_from(topology.positions.len()).map_err(|_| RendererError::IndexOverflow { + context: "vertex range", + })?; + let index_count = + u32::try_from(topology.indices.len()).map_err(|_| RendererError::IndexOverflow { + context: "index range", + })?; for (instance_index, instance) in batch.instances.iter().enumerate() { - let range = ranges - .get_mut(instance.occurrence_index) - .ok_or_else(|| RendererError::invalid("batch references an unknown occurrence"))?; + let range = + ranges + .get_mut(instance.occurrence_index) + .ok_or(RendererError::InvalidRange { + context: "batch occurrence", + index: instance.occurrence_index, + len: scene.occurrences.len(), + })?; if range.is_some() { - return Err(RendererError::invalid( + return Err(RendererError::ParseAdaptation( "occurrence appears in multiple native GPU batches", )); } @@ -3154,9 +3179,11 @@ pub struct ScenePicker { impl ScenePicker { pub fn new(scene: &Scene, plan: SceneRenderPlan) -> Result { if plan.planned_occurrences.len() != scene.occurrences.len() { - return Err(RendererError::invalid( - "render plan pick occurrences are incomplete", - )); + return Err(RendererError::InvalidRange { + context: "render plan pick occurrences", + index: plan.planned_occurrences.len(), + len: scene.occurrences.len(), + }); } let bvh = PickBvh::new(&plan.logical_bounds); let logical_part_indices = plan @@ -3192,16 +3219,21 @@ impl ScenePicker { } pub fn selection(&self, raw_index: usize) -> Result { - let raw = self.occurrences.get(raw_index).ok_or_else(|| { - RendererError::invalid("raw occurrence is outside the render-plan cache") - })?; + let raw = self + .occurrences + .get(raw_index) + .ok_or_else(|| RendererError::InvalidRange { + context: "raw occurrence", + index: raw_index, + len: self.occurrences.len(), + })?; let logical_part_occurrence_id = raw.logical_part_occurrence_id.clone(); let group_index = self .logical_part_indices .get(&logical_part_occurrence_id) .copied() .ok_or_else(|| { - RendererError::invalid("logical part is missing from the render-plan cache") + RendererError::ParseAdaptation("logical part is missing from the render-plan cache") })?; let occurrence_indices = self.logical_parts[group_index].clone(); let transforms = occurrence_indices @@ -3276,7 +3308,6 @@ impl ScenePicker { f64::from(point[2]), ], ) - .map_err(|error| RendererError::invalid(error.to_string())) }; ray_triangle_distance( ray, @@ -3307,7 +3338,7 @@ pub fn build_scene_occurrence_models( context: &three_d::Context, scene: &Scene, occurrence_indices: &[usize], -) -> Result { +) -> Result { let mut selected = scene.clone(); selected.occurrences = occurrence_indices .iter() @@ -3316,10 +3347,14 @@ pub fn build_scene_occurrence_models( .occurrences .get(*index) .cloned() - .ok_or_else(|| "selected occurrence is outside the scene table".to_owned()) + .ok_or(RendererError::InvalidRange { + context: "selected occurrence", + index: *index, + len: scene.occurrences.len(), + }) }) .collect::>()?; - build_scene_model(context, &selected) + build_scene_model(context, &selected).map_err(|error| RendererError::GpuResource(error)) } #[cfg(not(target_arch = "wasm32"))] @@ -3327,7 +3362,7 @@ pub fn build_scene_occurrence_model( context: &three_d::Context, scene: &Scene, occurrence_index: usize, -) -> Result { +) -> Result { build_scene_occurrence_models(context, scene, &[occurrence_index]) } @@ -3426,10 +3461,11 @@ pub fn scene_render_plan(scene: &Scene) -> Result Result Result, _>>()?; let local_bounds_complete = started.elapsed(); @@ -3545,7 +3576,7 @@ fn scene_instance_batches_for_mode( let local = local_bounds[geometry_index]; let bounds = local .transformed(occurrence.transform) - .ok_or_else(|| RendererError::invalid("occurrence has no finite render bounds"))?; + .ok_or_else(|| RendererError::NonFiniteTransform)?; let logical_part_occurrence_id = occurrence .logical_part_occurrence_id .clone() @@ -3597,9 +3628,8 @@ fn scene_instance_batches_for_mode( }); let material = occurrence.material.unwrap_or(default_material); let reverse_winding = occurrence.bfc_inverted ^ !occurrence.bfc_ccw; - let reflection = scene_transform_reflection(occurrence.transform).ok_or_else(|| { - RendererError::invalid("occurrence has a degenerate or non-finite transform") - })?; + let reflection = scene_transform_reflection(occurrence.transform) + .ok_or_else(|| RendererError::NonFiniteTransform)?; let key = BatchKey { node: occurrence.node, material, @@ -3806,7 +3836,7 @@ pub fn build_scene_model_for_view_with_resources( scene: &Scene, resources: Option<&[ModelResource]>, ) -> Result { - let plan = scene_render_plan(scene)?; + let plan = scene_render_plan(scene).map_err(|error| error.to_string())?; build_scene_model_from_batches( context, scene, @@ -3815,6 +3845,7 @@ pub fn build_scene_model_for_view_with_resources( &plan.topologies, resources, ) + .map_err(|error| error.to_string()) } pub type EdgeModel = Gm; @@ -3826,7 +3857,10 @@ pub fn update_edge_model_culling_bounds( viewer: &dyn Viewer, ) -> Result<(), String> { for model in models { - model.geometry.update_culling_aabb(viewer)?; + model + .geometry + .update_culling_aabb(viewer) + .map_err(|error| error.to_string())?; } Ok(()) } @@ -3914,19 +3948,27 @@ pub struct CompactSegmentGeometry { fn compact_segment_aabb( endpoints: &[three_d::Vec3], transforms: &[Mat4], -) -> Result { +) -> Result { if endpoints.is_empty() { - return Err("compact segment geometry has no endpoints"); + return Err(RendererError::InvalidRange { + context: "compact segment endpoints", + index: 0, + len: endpoints.len(), + }); } if transforms.is_empty() { - return Err("compact segment geometry has no instance transforms"); + return Err(RendererError::InvalidRange { + context: "compact segment transforms", + index: 0, + len: transforms.len(), + }); } if endpoints.iter().any(|point| { [point.x, point.y, point.z] .into_iter() .any(|value| !value.is_finite()) }) { - return Err("compact segment geometry has non-finite endpoints"); + return Err(RendererError::NonFiniteTransform); } let local = AxisAlignedBoundingBox::new_with_positions(endpoints); let mut world = AxisAlignedBoundingBox::EMPTY; @@ -3945,7 +3987,7 @@ fn compact_segment_aabb( ]) .all(|value| value.is_finite()) { - return Err("compact segment geometry transform overflowed its bounds"); + return Err(RendererError::NonFiniteTransform); } world.expand_with_aabb(transformed); } @@ -3956,7 +3998,7 @@ fn compact_segment_aabb( { Ok(world) } else { - Err("compact segment geometry world bounds are non-finite") + Err(RendererError::NonFiniteTransform) } } @@ -3965,22 +4007,34 @@ fn validate_compact_segment_cardinality( control_count: Option, transform_count: u64, local_view_count: Option, -) -> Result<(u64, u32), String> { +) -> Result<(u64, u32), RendererError> { let vertex_count = segment_count .checked_mul(6) - .ok_or_else(|| "compact segment vertex count overflow".to_owned())?; + .ok_or(RendererError::IndexOverflow { + context: "compact segment vertex count", + })?; if let Some(control_count) = control_count && control_count != segment_count { - return Err("compact segment controls must match segment count".to_owned()); + return Err(RendererError::InvalidRange { + context: "compact segment controls", + index: control_count as usize, + len: segment_count as usize, + }); } if let Some(local_view_count) = local_view_count && local_view_count != transform_count { - return Err("compact segment local views must match transform count".to_owned()); + return Err(RendererError::InvalidRange { + context: "compact segment local views", + index: local_view_count as usize, + len: transform_count as usize, + }); } - let instance_count = u32::try_from(transform_count) - .map_err(|_| "compact segment instance count exceeds GPU range".to_owned())?; + let instance_count = + u32::try_from(transform_count).map_err(|_| RendererError::IndexOverflow { + context: "compact segment instance count", + })?; Ok((vertex_count, instance_count)) } @@ -3992,25 +4046,33 @@ impl CompactSegmentGeometry { transforms: &[Mat4], local_views: Option<&[[f32; 3]]>, ribbon_width: f32, - ) -> Result { + ) -> Result { let (vertex_count, instance_count) = validate_compact_segment_cardinality( - u64::try_from(segments.len()) - .map_err(|_| "compact segment count exceeds logical range".to_owned())?, + u64::try_from(segments.len()).map_err(|_| RendererError::IndexOverflow { + context: "compact segment count", + })?, controls .map(<[_]>::len) .map(u64::try_from) .transpose() - .map_err(|_| "compact segment control count exceeds logical range".to_owned())?, - u64::try_from(transforms.len()) - .map_err(|_| "compact segment transform count exceeds logical range".to_owned())?, + .map_err(|_| RendererError::IndexOverflow { + context: "compact segment control count", + })?, + u64::try_from(transforms.len()).map_err(|_| RendererError::IndexOverflow { + context: "compact segment transform count", + })?, local_views .map(<[_]>::len) .map(u64::try_from) .transpose() - .map_err(|_| "compact segment local-view count exceeds logical range".to_owned())?, + .map_err(|_| RendererError::IndexOverflow { + context: "compact segment local-view count", + })?, )?; - let vertex_count = usize::try_from(vertex_count) - .map_err(|_| "compact segment vertex count exceeds addressable memory".to_owned())?; + let vertex_count = + usize::try_from(vertex_count).map_err(|_| RendererError::IndexOverflow { + context: "compact segment vertex address", + })?; let mut endpoints = Vec::with_capacity(vertex_count); let mut paired_endpoints = Vec::with_capacity(vertex_count); let mut control1s = Vec::with_capacity(vertex_count); @@ -4042,7 +4104,7 @@ impl CompactSegmentGeometry { sides.push(side); } } - let base_aabb = compact_segment_aabb(&endpoints, transforms).map_err(str::to_owned)?; + let base_aabb = compact_segment_aabb(&endpoints, transforms)?; let row1: Vec = transforms.iter().map(|m| m.row(0)).collect(); let row2: Vec = transforms.iter().map(|m| m.row(1)).collect(); let row3: Vec = transforms.iter().map(|m| m.row(2)).collect(); @@ -4162,14 +4224,18 @@ void main() { } impl CompactSegmentGeometry { - fn update_culling_aabb(&mut self, viewer: &dyn Viewer) -> Result<(), String> { + fn update_culling_aabb(&mut self, viewer: &dyn Viewer) -> Result<(), RendererError> { if self.base_aabb.is_empty() { self.aabb = self.base_aabb; return Ok(()); } let viewport = viewer.viewport(); if viewport.width == 0 || viewport.height == 0 { - return Err("ribbon culling requires a non-empty viewport".to_owned()); + return Err(RendererError::InvalidRange { + context: "ribbon culling viewport", + index: 0, + len: 0, + }); } let view_projection = viewer.projection() * viewer.view(); let corners = [ @@ -4218,14 +4284,14 @@ impl CompactSegmentGeometry { || projection_x_scale <= 0.0 || projection_y_scale <= 0.0 { - return Err("ribbon culling projection scale is not finite".to_owned()); + return Err(RendererError::NonFiniteTransform); } let mut radius = 0.0_f32; let mut minimum_w = f32::INFINITY; for corner in corners { let clip = view_projection * corner.extend(1.0); if !clip.x.is_finite() || !clip.y.is_finite() || !clip.w.is_finite() || clip.w <= 0.0 { - return Err("ribbon culling cannot represent a finite clip bound".to_owned()); + return Err(RendererError::NonFiniteTransform); } minimum_w = minimum_w.min(clip.w); let radius_x = @@ -4238,7 +4304,7 @@ impl CompactSegmentGeometry { || radius > f32::MAX / 4.0 || minimum_w <= radius * projection_w_scale { - return Err("ribbon culling bound cannot remain finite near the clip plane".to_owned()); + return Err(RendererError::NonFiniteTransform); } let delta = three_d::vec3(radius, radius, radius); self.aabb = AxisAlignedBoundingBox::new_with_positions(&[ @@ -4254,7 +4320,7 @@ impl CompactSegmentGeometry { { Ok(()) } else { - Err("ribbon culling bound overflow".to_owned()) + Err(RendererError::NonFiniteTransform) } } } @@ -4404,7 +4470,7 @@ pub fn build_edge_models_for_view( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], -) -> Result { +) -> Result { let topology_started = Instant::now(); let (explicit_inputs, conditional_inputs) = compact_edge_inputs_for_view(scene, plan, view)?; let stats = explicit_inputs.stats; @@ -4436,7 +4502,7 @@ pub fn build_conditional_edge_models_for_view( _scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], -) -> Result>, String> { +) -> Result>, RendererError> { let conditional_inputs = compact_conditional_edge_inputs_for_membership(plan, view, None)?; let models = build_compact_edge_models(context, conditional_inputs.batches, false)?; Ok((!models.is_empty()).then_some(models)) @@ -4458,7 +4524,7 @@ pub fn build_selected_edge_models_for_view( plan: &SceneRenderPlan, selection: &SelectionIdentity, view: [f64; 3], -) -> Result { +) -> Result { let (explicit_inputs, _) = compact_edge_inputs_for_membership(plan, view, Some(&selection.occurrence_indices))?; let conditional_inputs = compact_conditional_edge_inputs_for_membership( @@ -4482,7 +4548,7 @@ pub fn build_selected_conditional_edge_models_for_view( plan: &SceneRenderPlan, selection: &SelectionIdentity, view: [f64; 3], -) -> Result>, String> { +) -> Result>, RendererError> { let inputs = compact_conditional_edge_inputs_for_membership( plan, view, @@ -4515,7 +4581,7 @@ pub fn build_scene_model_parts_for_style_with_resources( resources: Option<&[ModelResource]>, ) -> Result { let started = Instant::now(); - let plan = scene_render_plan(scene)?; + let plan = scene_render_plan(scene).map_err(|error| error.to_string())?; let plan_elapsed = started.elapsed(); tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={} cumulative_ms={}", @@ -4530,7 +4596,8 @@ pub fn build_scene_model_parts_for_style_with_resources( &plan.upload_batches, &plan.topologies, resources, - )?; + ) + .map_err(|error| error.to_string())?; let static_elapsed = static_started.elapsed(); tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={} cumulative_ms={}", @@ -4539,7 +4606,8 @@ pub fn build_scene_model_parts_for_style_with_resources( ); let (explicit_model, conditional_model) = if style == RenderStyle::Edges { let edge_started = Instant::now(); - let models = build_edge_models_for_view(context, scene, &plan, view)?; + let models = build_edge_models_for_view(context, scene, &plan, view) + .map_err(|error| error.to_string())?; tracing::debug!( "[ldraw-preview] native-build-stage=edge-output elapsed_ms={} cumulative_ms={}", edge_started.elapsed().as_millis(), @@ -4549,7 +4617,7 @@ pub fn build_scene_model_parts_for_style_with_resources( } else { (None, None) }; - let picker = ScenePicker::new(scene, plan.clone())?; + let picker = ScenePicker::new(scene, plan.clone()).map_err(|error| error.to_string())?; tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() @@ -4567,7 +4635,7 @@ pub fn build_scene_faces_and_picker( scene: &Scene, ) -> Result<(NativeSceneModel, ScenePicker), String> { let started = Instant::now(); - let plan = scene_render_plan(scene)?; + let plan = scene_render_plan(scene).map_err(|error| error.to_string())?; tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={}", started.elapsed().as_millis() @@ -4579,12 +4647,13 @@ pub fn build_scene_faces_and_picker( &plan.upload_batches, &plan.topologies, None, - )?; + ) + .map_err(|error| error.to_string())?; tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={}", started.elapsed().as_millis() ); - let picker = ScenePicker::new(scene, plan)?; + let picker = ScenePicker::new(scene, plan).map_err(|error| error.to_string())?; tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() @@ -4627,7 +4696,7 @@ fn compact_edge_inputs_for_view( _scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], -) -> Result<(CompactEdgeInputs, CompactEdgeInputs), String> { +) -> Result<(CompactEdgeInputs, CompactEdgeInputs), RendererError> { compact_edge_inputs_for_membership(plan, view, None) } @@ -4635,7 +4704,7 @@ fn compact_conditional_edge_inputs_for_membership( plan: &SceneRenderPlan, view: [f64; 3], selected: Option<&[usize]>, -) -> Result { +) -> Result { let indices = selected.unwrap_or(&[]); let occurrences = if selected.is_some() { indices @@ -4643,7 +4712,11 @@ fn compact_conditional_edge_inputs_for_membership( .map(|index| { plan.planned_occurrences .get(*index) - .ok_or("selected occurrence is outside the render-plan cache") + .ok_or(RendererError::InvalidRange { + context: "selected occurrence", + index: *index, + len: plan.planned_occurrences.len(), + }) }) .collect::, _>>()? } else { @@ -4684,13 +4757,13 @@ fn compact_conditional_edge_inputs_for_membership( local_views: Some(Vec::new()), transforms: Vec::new(), }); - batch.transforms.push( - scene_transform_to_mat4(occurrence.transform).map_err(|error| error.to_string())?, - ); + batch + .transforms + .push(scene_transform_to_mat4(occurrence.transform)?); batch.local_views.as_mut().unwrap().push([ - finite_f32(local_view[0]).map_err(|error| error.to_string())?, - finite_f32(local_view[1]).map_err(|error| error.to_string())?, - finite_f32(local_view[2]).map_err(|error| error.to_string())?, + finite_f32(local_view[0])?, + finite_f32(local_view[1])?, + finite_f32(local_view[2])?, ]); stats.visible_conditional_segments += source.conditional.len(); } @@ -4711,7 +4784,7 @@ fn compact_edge_inputs_for_membership( plan: &SceneRenderPlan, view: [f64; 3], selected: Option<&[usize]>, -) -> Result<(CompactEdgeInputs, CompactEdgeInputs), String> { +) -> Result<(CompactEdgeInputs, CompactEdgeInputs), RendererError> { let mut stats = EdgeBuildStats { source_geometry_count: plan.edge_sources.len(), planned_occurrences: plan.planned_occurrences.len(), @@ -4724,7 +4797,11 @@ fn compact_edge_inputs_for_membership( .map(|index| { plan.planned_occurrences .get(*index) - .ok_or("selected occurrence is outside the render-plan cache") + .ok_or(RendererError::InvalidRange { + context: "selected occurrence", + index: *index, + len: plan.planned_occurrences.len(), + }) }) .collect::, _>>() }) @@ -4749,9 +4826,9 @@ fn compact_edge_inputs_for_membership( }); if !explicit_batch.segments.is_empty() { stats.explicit_segments += source.explicit.len(); - explicit_batch.transforms.push( - scene_transform_to_mat4(occurrence.transform).map_err(|error| error.to_string())?, - ); + explicit_batch + .transforms + .push(scene_transform_to_mat4(occurrence.transform)?); } if !source.conditional.is_empty() { let batch = conditional @@ -4775,13 +4852,13 @@ fn compact_edge_inputs_for_membership( local_views: Some(Vec::new()), transforms: Vec::new(), }); - batch.transforms.push( - scene_transform_to_mat4(occurrence.transform).map_err(|error| error.to_string())?, - ); + batch + .transforms + .push(scene_transform_to_mat4(occurrence.transform)?); batch.local_views.as_mut().unwrap().push([ - finite_f32(local_view[0]).map_err(|error| error.to_string())?, - finite_f32(local_view[1]).map_err(|error| error.to_string())?, - finite_f32(local_view[2]).map_err(|error| error.to_string())?, + finite_f32(local_view[0])?, + finite_f32(local_view[1])?, + finite_f32(local_view[2])?, ]); stats.visible_conditional_segments += source.conditional.len(); } @@ -4841,11 +4918,11 @@ fn compact_edge_inputs_for_membership( )) } -fn finite_f32_vec3(point: [f64; 3]) -> Result<[f32; 3], String> { +fn finite_f32_vec3(point: [f64; 3]) -> Result<[f32; 3], RendererError> { Ok([ - finite_f32(point[0]).map_err(|error| error.to_string())?, - finite_f32(point[1]).map_err(|error| error.to_string())?, - finite_f32(point[2]).map_err(|error| error.to_string())?, + finite_f32(point[0])?, + finite_f32(point[1])?, + finite_f32(point[2])?, ]) } @@ -4853,7 +4930,7 @@ pub fn edge_build_stats( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], -) -> Result { +) -> Result { Ok(compact_edge_inputs_for_view(scene, plan, view)?.0.stats) } @@ -4861,7 +4938,7 @@ pub fn build_conditional_edge_topology_for_scene( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], -) -> Result, String> { +) -> Result, RendererError> { let (_, conditional) = compact_edge_inputs_for_view(scene, plan, view)?; Ok(conditional .batches @@ -4923,7 +5000,7 @@ fn build_compact_edge_models( context: &three_d::Context, batches: Vec, selected: bool, -) -> Result>, String> { +) -> Result>, RendererError> { batches .into_iter() .map(|batch| { @@ -4994,7 +5071,7 @@ fn build_scene_model_from_batches( batches: &[SceneInstanceBatch], topologies: &[SceneMeshTopology], resources: Option<&[ModelResource]>, -) -> Result { +) -> Result { let resource_bytes = |texture: &polymodel_renderer_protocol::scene::SceneTexture| { let key = texture .resource_key @@ -5007,7 +5084,7 @@ fn build_scene_model_from_batches( .find(|resource| resource.key == key || resource.path == key) }) .map(|resource| (key.to_owned(), resource.bytes.as_slice())) - .ok_or_else(|| format!("texture resource is not supplied: {key}")) + .ok_or_else(|| RendererError::MissingTexture(texture.id)) }; let mut albedo_cache = HashMap::::new(); let mut gloss_cache = HashMap::::new(); @@ -5016,13 +5093,11 @@ fn build_scene_model_from_batches( let model = batches .iter() .map(|batch| { - let geometry = index.geometry(scene, batch.node)?; + let _geometry = index.geometry(scene, batch.node)?; let transformations = batch .instances .iter() - .map(|instance| { - scene_transform_to_mat4(instance.transform).map_err(|error| error.to_string()) - }) + .map(|instance| scene_transform_to_mat4(instance.transform)) .collect::, _>>()?; let scene_material = index.material(scene, batch.material)?; let colour_rgb = scene_material @@ -5047,7 +5122,7 @@ fn build_scene_model_from_batches( .textures .iter() .find(|texture| texture.id == texture_id) - .ok_or_else(|| format!("albedo texture {texture_id:?} is missing"))?; + .ok_or(RendererError::MissingTexture(texture_id))?; let (key, bytes) = resource_bytes(texture)?; if !albedo_cache.contains_key(&key) { let (width, height, pixels, transparency) = decode_albedo_png(bytes)?; @@ -5063,9 +5138,12 @@ fn build_scene_model_from_batches( }, ); } - translucent |= *transparency_cache - .get(&key) - .ok_or_else(|| format!("albedo texture cache lost resource {key}"))?; + translucent |= + *transparency_cache + .get(&key) + .ok_or(RendererError::MissingTexture( + scene_material.albedo_texture.unwrap(), + ))?; cpu_material.albedo_texture = albedo_cache.get(&key).cloned(); } if let Some(texture_id) = scene_material.metallic_roughness_texture { @@ -5073,7 +5151,7 @@ fn build_scene_model_from_batches( .textures .iter() .find(|texture| texture.id == texture_id) - .ok_or_else(|| format!("GLOSSMAP texture {texture_id:?} is missing"))?; + .ok_or(RendererError::MissingTexture(texture_id))?; let (key, bytes) = resource_bytes(texture)?; if !gloss_cache.contains_key(&key) { let decoded = decode_glossmap_png(bytes)?; @@ -5109,9 +5187,13 @@ fn build_scene_model_from_batches( return Ok(None); } let geometry_index = index.geometry_index(batch.node)?; - let topology = topologies.get(geometry_index).ok_or_else(|| { - RendererError::invalid("render plan is missing geometry topology") - })?; + let topology = topologies + .get(geometry_index) + .ok_or(RendererError::InvalidRange { + context: "render plan geometry topology", + index: geometry_index, + len: topologies.len(), + })?; if topology.indices.is_empty() { return Ok(None); } @@ -5146,10 +5228,11 @@ fn build_scene_model_from_batches( } } if cpu_mesh.normals.as_ref().map(Vec::len) != Some(topology.positions.len()) { - return Err(format!( - "geometry {} produced normals outside its local position domain", - geometry.id.0 - )); + return Err(RendererError::InvalidRange { + context: "geometry normals", + index: cpu_mesh.normals.as_ref().map_or(0, Vec::len), + len: topology.positions.len(), + }); } Ok(Some(Gm::new( InstancedMesh::new( @@ -5163,12 +5246,14 @@ fn build_scene_model_from_batches( material, ))) }) - .collect::, String>>()? + .collect::, RendererError>>()? .into_iter() .flatten() .collect::>(); if model.is_empty() { - Err("scene contains no renderable instanced geometry".to_owned()) + Err(RendererError::ParseAdaptation( + "scene contains no renderable instanced geometry", + )) } else { Ok(model) } diff --git a/crates/polymodel-renderer-worker/src/tests.rs b/crates/polymodel-renderer-worker/src/tests.rs index 3722f37..38ab1b0 100644 --- a/crates/polymodel-renderer-worker/src/tests.rs +++ b/crates/polymodel-renderer-worker/src/tests.rs @@ -484,18 +484,26 @@ fn compact_segment_aabb_rejects_empty_or_nonfinite_production_inputs() { let identity = Mat4::from_scale(1.0); assert!(matches!( compact_segment_aabb(&[], &[identity]), - Err("compact segment geometry has no endpoints") + Err(RendererError::InvalidRange { + context: "compact segment endpoints", + index: 0, + len: 0, + }) )); assert!(matches!( compact_segment_aabb( &[three_d::vec3(0.0, 0.0, 0.0), three_d::vec3(1.0, 0.0, 0.0)], &[], ), - Err("compact segment geometry has no instance transforms") + Err(RendererError::InvalidRange { + context: "compact segment transforms", + index: 0, + len: 0, + }) )); assert!(matches!( compact_segment_aabb(&[three_d::vec3(f32::NAN, 0.0, 0.0)], &[identity]), - Err("compact segment geometry has non-finite endpoints") + Err(RendererError::NonFiniteTransform) )); let overflow = Mat4::from_nonuniform_scale(f32::MAX, 1.0, 1.0); assert!(matches!( @@ -503,7 +511,7 @@ fn compact_segment_aabb_rejects_empty_or_nonfinite_production_inputs() { &[three_d::vec3(-2.0, 0.0, 0.0), three_d::vec3(2.0, 0.0, 0.0)], &[overflow], ), - Err("compact segment geometry transform overflowed its bounds") + Err(RendererError::NonFiniteTransform) )); } @@ -515,19 +523,31 @@ fn compact_segment_cardinality_rejects_every_gpu_boundary_mismatch() { ); assert_eq!( validate_compact_segment_cardinality(u64::MAX / 6 + 1, None, 1, None).unwrap_err(), - "compact segment vertex count overflow" + RendererError::IndexOverflow { + context: "compact segment vertex count", + } ); assert_eq!( validate_compact_segment_cardinality(2, Some(1), 1, None).unwrap_err(), - "compact segment controls must match segment count" + RendererError::InvalidRange { + context: "compact segment controls", + index: 1, + len: 2, + } ); assert_eq!( validate_compact_segment_cardinality(1, None, 2, Some(1)).unwrap_err(), - "compact segment local views must match transform count" + RendererError::InvalidRange { + context: "compact segment local views", + index: 1, + len: 2, + } ); assert_eq!( validate_compact_segment_cardinality(1, None, u64::from(u32::MAX) + 1, None).unwrap_err(), - "compact segment instance count exceeds GPU range" + RendererError::IndexOverflow { + context: "compact segment instance count", + } ); } @@ -722,7 +742,14 @@ fn scene_render_plan_rejects_misaligned_occurrence_uvs() { let mut scene = picker_scene(); scene.occurrences[0].uvs = Some(vec![[0.0, 0.0]]); let error = scene_render_plan(&scene).expect_err("misaligned occurrence UVs"); - assert!(error.to_string().contains("invalid UV alignment")); + assert!(matches!( + error, + RendererError::InvalidRange { + context: "occurrence UV alignment", + index: 1, + len: 3, + } + )); } #[test] @@ -730,7 +757,14 @@ fn scene_render_plan_rejects_non_finite_occurrence_uvs() { let mut scene = picker_scene(); scene.occurrences[0].uvs = Some(vec![[f32::NAN, 0.0]; 3]); let error = scene_render_plan(&scene).expect_err("non-finite occurrence UVs"); - assert!(error.to_string().contains("invalid UV alignment")); + assert!(matches!( + error, + RendererError::InvalidRange { + context: "occurrence UV alignment", + index: 3, + len: 3, + } + )); } #[test] @@ -1093,29 +1127,23 @@ fn scene_consumers_resolve_sparse_opaque_ids_and_reject_invalid_references() { let mut duplicate = scene.clone(); duplicate.geometries.push(duplicate.geometries[0].clone()); - assert!( - occurrence_gpu_ranges(&duplicate) - .unwrap_err() - .to_string() - .contains("duplicate geometry id") + assert_eq!( + occurrence_gpu_ranges(&duplicate).unwrap_err(), + RendererError::DuplicateGeometry(GeometryId(41)) ); let mut dangling_geometry = scene.clone(); dangling_geometry.occurrences[0].node = NodeId(99); - assert!( - occurrence_gpu_ranges(&dangling_geometry) - .unwrap_err() - .to_string() - .contains("unknown geometry id 99") + assert_eq!( + occurrence_gpu_ranges(&dangling_geometry).unwrap_err(), + RendererError::MissingGeometry(GeometryId(99)) ); let mut dangling_material = scene; dangling_material.occurrences[0].material = Some(MaterialId(99)); - assert!( - occurrence_gpu_ranges(&dangling_material) - .unwrap_err() - .to_string() - .contains("unknown material id 99") + assert_eq!( + occurrence_gpu_ranges(&dangling_material).unwrap_err(), + RendererError::MissingMaterial(MaterialId(99)) ); } @@ -1310,10 +1338,13 @@ fn native_topology_rejects_foreign_index_domains() { Vec::new(), )) .unwrap_err(); - assert!( - error - .to_string() - .contains("outside its local 3-position domain") + assert_eq!( + error, + RendererError::InvalidRange { + context: "geometry index", + index: 3, + len: 3, + } ); } @@ -1396,7 +1427,14 @@ fn native_topology_rejects_malformed_conditional_line_indices() { ); geometry.conditional_lines = vec![[0, 1, 2, 3]]; let error = scene_mesh_topology(&geometry).unwrap_err(); - assert!(error.to_string().contains("conditional-line index 2")); + assert_eq!( + error, + RendererError::InvalidRange { + context: "geometry conditional-line index", + index: 2, + len: 2, + } + ); } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/polymodel-renderer-worker/src/texture.rs b/crates/polymodel-renderer-worker/src/texture.rs index 63eb33b..680c1f8 100644 --- a/crates/polymodel-renderer-worker/src/texture.rs +++ b/crates/polymodel-renderer-worker/src/texture.rs @@ -1,5 +1,7 @@ use polymodel_renderer_protocol::scene::SceneTexmapProjectionMode; +use crate::RendererError; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct GlossMapPixels { pub width: u32, @@ -9,7 +11,7 @@ pub struct GlossMapPixels { pub packed_rgb: Vec<[u8; 3]>, } -pub fn decode_glossmap_png(bytes: &[u8]) -> Result { +pub fn decode_glossmap_png(bytes: &[u8]) -> Result { let (info, output) = decode_png(bytes, "GLOSSMAP")?; let bytes = &output[..info.buffer_size()]; let (channels, has_alpha) = match info.color_type { @@ -18,19 +20,31 @@ pub fn decode_glossmap_png(bytes: &[u8]) -> Result { png::ColorType::Rgb => (3usize, false), png::ColorType::Grayscale => (1usize, false), png::ColorType::Indexed => { - return Err("GLOSSMAP indexed output was not expanded".to_owned()); + return Err(RendererError::TextureDecode( + "GLOSSMAP indexed output was not expanded".into(), + )); } }; let (sample_bytes, max) = match info.bit_depth { png::BitDepth::Eight => (1usize, u32::from(u8::MAX)), png::BitDepth::Sixteen => (2usize, u32::from(u16::MAX)), - depth => return Err(format!("unsupported decoded GLOSSMAP bit depth: {depth:?}")), + depth => { + return Err(RendererError::TextureDecode(format!( + "unsupported decoded GLOSSMAP bit depth: {depth:?}" + ))); + } }; let pixel_stride = channels .checked_mul(sample_bytes) - .ok_or_else(|| "GLOSSMAP pixel stride overflow".to_owned())?; + .ok_or(RendererError::IndexOverflow { + context: "GLOSSMAP pixel stride", + })?; if pixel_stride == 0 || bytes.len() % pixel_stride != 0 { - return Err("GLOSSMAP decoded pixel length is inconsistent".to_owned()); + return Err(RendererError::InvalidRange { + context: "GLOSSMAP decoded pixels", + index: bytes.len(), + len: pixel_stride, + }); } let mut alpha = Vec::with_capacity(bytes.len() / pixel_stride); for pixel in bytes.chunks_exact(pixel_stride) { @@ -54,29 +68,31 @@ pub fn decode_glossmap_png(bytes: &[u8]) -> Result { }) } -fn decode_png(bytes: &[u8], label: &str) -> Result<(png::OutputInfo, Vec), String> { +fn decode_png(bytes: &[u8], label: &str) -> Result<(png::OutputInfo, Vec), RendererError> { let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes)); decoder.set_transformations(png::Transformations::EXPAND); let mut reader = decoder .read_info() - .map_err(|error| format!("decode {label} header: {error}"))?; + .map_err(|error| RendererError::TextureDecode(format!("decode {label} header: {error}")))?; let output_size = reader .output_buffer_size() - .ok_or_else(|| format!("{label} decoded size overflow"))?; + .ok_or(RendererError::IndexOverflow { + context: "decoded texture size", + })?; let mut output = vec![0; output_size]; let info = reader .next_frame(&mut output) - .map_err(|error| format!("decode {label} pixels: {error}"))?; + .map_err(|error| RendererError::TextureDecode(format!("decode {label} pixels: {error}")))?; Ok((info, output)) } -pub fn decode_albedo_png(bytes: &[u8]) -> Result<(u32, u32, Vec<[u8; 4]>, bool), String> { +pub fn decode_albedo_png(bytes: &[u8]) -> Result<(u32, u32, Vec<[u8; 4]>, bool), RendererError> { let (info, output) = decode_png(bytes, "albedo")?; if info.bit_depth != png::BitDepth::Eight { - return Err(format!( + return Err(RendererError::TextureDecode(format!( "unsupported albedo bit depth: {:?}", info.bit_depth - )); + ))); } let source = &output[..info.buffer_size()]; let (channels, has_alpha) = match info.color_type { @@ -84,7 +100,11 @@ pub fn decode_albedo_png(bytes: &[u8]) -> Result<(u32, u32, Vec<[u8; 4]>, bool), png::ColorType::Rgb => (3, false), png::ColorType::GrayscaleAlpha => (2, true), png::ColorType::Grayscale => (1, false), - png::ColorType::Indexed => return Err("albedo indexed output was not expanded".into()), + png::ColorType::Indexed => { + return Err(RendererError::TextureDecode( + "albedo indexed output was not expanded".into(), + )); + } }; let mut rgba = Vec::with_capacity( (info.width as usize) @@ -104,32 +124,44 @@ pub fn decode_albedo_png(bytes: &[u8]) -> Result<(u32, u32, Vec<[u8; 4]>, bool), rgba.push([r, g, b, a]); } if source.len() != rgba.len() * channels { - return Err("albedo decoded pixel length is inconsistent".into()); + return Err(RendererError::InvalidRange { + context: "albedo decoded pixels", + index: source.len(), + len: rgba.len() * channels, + }); } let translucent = translucent || has_alpha && rgba.iter().any(|p| p[3] < 255); Ok((info.width, info.height, rgba, translucent)) } -pub fn packed_roughness_from_alpha(alpha: u32, max: u32) -> Result<[u8; 3], String> { +pub fn packed_roughness_from_alpha(alpha: u32, max: u32) -> Result<[u8; 3], RendererError> { if max == 0 || (max != u8::MAX as u32 && max != u16::MAX as u32) || alpha > max { - return Err("invalid gloss alpha range".to_owned()); + return Err(RendererError::InvalidRange { + context: "gloss alpha", + index: alpha as usize, + len: max as usize, + }); } - let roughness = max - .checked_sub(alpha) - .ok_or_else(|| "gloss alpha underflow".to_owned())?; + let roughness = max.checked_sub(alpha).ok_or(RendererError::IndexOverflow { + context: "gloss alpha", + })?; let numerator = u64::from(roughness) .checked_mul(255) .and_then(|value| value.checked_add(u64::from(max / 2))) - .ok_or_else(|| "roughness quantization overflow".to_owned())?; + .ok_or(RendererError::IndexOverflow { + context: "roughness quantization", + })?; let channel = numerator / u64::from(max); - let channel = u8::try_from(channel).map_err(|_| "roughness channel overflow".to_owned())?; + let channel = u8::try_from(channel).map_err(|_| RendererError::IndexOverflow { + context: "roughness channel", + })?; Ok([0, channel, 0]) } pub fn packed_roughness_from_alpha_samples( alpha: &[u32], max: u32, -) -> Result, String> { +) -> Result, RendererError> { alpha .iter() .copied() @@ -141,14 +173,16 @@ pub fn texmap_uvs( mode: SceneTexmapProjectionMode, parameters: &[f64], points: &[[f64; 3]], -) -> Result, String> { +) -> Result, RendererError> { let expected = match mode { SceneTexmapProjectionMode::Planar => 9, SceneTexmapProjectionMode::Cylindrical => 10, SceneTexmapProjectionMode::Spherical => 11, }; if parameters.len() != expected || parameters.iter().any(|value| !value.is_finite()) { - return Err("TEXMAP parameters must have the expected finite arity".to_owned()); + return Err(RendererError::ParseAdaptation( + "TEXMAP parameters must have the expected finite arity", + )); } let point = |offset: usize| { [ @@ -189,7 +223,9 @@ pub fn texmap_uvs( || !determinant.is_finite() || determinant <= f64::EPSILON { - return Err("degenerate PLANAR TEXMAP basis".to_owned()); + return Err(RendererError::ParseAdaptation( + "degenerate PLANAR TEXMAP basis", + )); } points .iter() @@ -206,7 +242,9 @@ pub fn texmap_uvs( SceneTexmapProjectionMode::Cylindrical => { let axis = sub(p2, p1); let axis_len = length(axis); - let axis_n = normalize(axis).ok_or("degenerate CYLINDRICAL TEXMAP axis")?; + let axis_n = normalize(axis).ok_or(RendererError::ParseAdaptation( + "degenerate CYLINDRICAL TEXMAP axis", + ))?; let radial = sub(p3, p1); let radial = sub( radial, @@ -216,10 +254,14 @@ pub fn texmap_uvs( axis_n[2] * dot(radial, axis_n), ], ); - let radial_n = normalize(radial).ok_or("degenerate CYLINDRICAL TEXMAP radius")?; + let radial_n = normalize(radial).ok_or(RendererError::ParseAdaptation( + "degenerate CYLINDRICAL TEXMAP radius", + ))?; let angle_extent = parameters[9].to_radians(); if !angle_extent.is_finite() || angle_extent.abs() <= f64::EPSILON { - return Err("degenerate CYLINDRICAL TEXMAP angle".to_owned()); + return Err(RendererError::ParseAdaptation( + "degenerate CYLINDRICAL TEXMAP angle", + )); } points .iter() @@ -237,7 +279,9 @@ pub fn texmap_uvs( .collect::>() } SceneTexmapProjectionMode::Spherical => { - let normal = normalize(sub(p2, p1)).ok_or("degenerate SPHERICAL TEXMAP direction")?; + let normal = normalize(sub(p2, p1)).ok_or(RendererError::ParseAdaptation( + "degenerate SPHERICAL TEXMAP direction", + ))?; let mut horizontal = sub(p3, p1); horizontal = sub( horizontal, @@ -247,9 +291,12 @@ pub fn texmap_uvs( normal[2] * dot(horizontal, normal), ], ); - let horizontal = normalize(horizontal).ok_or("degenerate SPHERICAL TEXMAP basis")?; - let vertical = normalize(cross(normal, horizontal)) - .ok_or("degenerate SPHERICAL TEXMAP vertical")?; + let horizontal = normalize(horizontal).ok_or(RendererError::ParseAdaptation( + "degenerate SPHERICAL TEXMAP basis", + ))?; + let vertical = normalize(cross(normal, horizontal)).ok_or( + RendererError::ParseAdaptation("degenerate SPHERICAL TEXMAP vertical"), + )?; let a = parameters[9].to_radians(); let b = parameters[10].to_radians(); if !a.is_finite() @@ -257,7 +304,9 @@ pub fn texmap_uvs( || a.abs() <= f64::EPSILON || b.abs() <= f64::EPSILON { - return Err("degenerate SPHERICAL TEXMAP angle".to_owned()); + return Err(RendererError::ParseAdaptation( + "degenerate SPHERICAL TEXMAP angle", + )); } points .iter() @@ -271,7 +320,7 @@ pub fn texmap_uvs( } }; if result.iter().flatten().any(|value| !value.is_finite()) { - return Err("TEXMAP generated non-finite UV".to_owned()); + return Err(RendererError::NonFiniteTransform); } let mut result = result; if matches!( @@ -302,7 +351,7 @@ pub fn texmap_uvs( let v = v as f32; (u.is_finite() && v.is_finite()) .then_some([u, v]) - .ok_or_else(|| "TEXMAP UV cannot be represented as finite f32 values".to_owned()) + .ok_or(RendererError::NonFiniteTransform) }) .collect() }