Diff
9 "base": "03ead6ffc01b2ea0adf6f2c87d59e86b9615566d",
10 "head": "03ead6ffc01b2ea0adf6f2c87d59e86b9615566d",
11 "cob": "c4b135bf3bcf25661f3d7e8207e4156221874760"
12+ },
13+ {
14+ "base": "03ead6ffc01b2ea0adf6f2c87d59e86b9615566d",
15+ "head": "6d8f169292ca23896b2321673d70b07132bce4d1",
16+ "cob": "d1dce6cd0d0ef02ee20ed2d1032a8aa9f83b94ac"
17 }
18 ]
19 }1+- Add a public fast path like `load_from_data(obj::ObjSet)` / `load_from_obj_str(&str)` so callers that already validated UTF-8 or already parsed OBJ data can bypass the dominant parse stage.
2+- If end-to-end byte parsing must remain the target, investigate whether `wavefront_obj` itself has avoidable hot allocations or whether a lighter-weight triangle-only parser is justified for Rendy workloads.
2 {"run":1,"commit":"03ead6f","metric":168051,"metrics":{"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"keep","description":"Baseline for generated-grid OBJ loading benchmark.","timestamp":1776686112516,"segment":0,"confidence":null,"iterationTokens":80,"asi":{"hypothesis":"Establish a stable baseline for end-to-end OBJ parse+conversion time before changing loader internals.","workload":"generated 96x96 grid OBJ, 24 loads per sample, median of 5 samples","observation":"Benchmark is stable enough to target allocation and iterator overhead inside mesh/src/format/obj.rs."}}
3 {"run":2,"commit":"03ead6f","metric":169721,"metrics":{"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"discard","description":"Fuse triangle expansion into a single pass with preallocated output vectors.","timestamp":1776686156595,"segment":0,"confidence":null,"iterationTokens":2267,"asi":{"hypothesis":"Eliminating the temporary index list and three iterator/collect passes should reduce allocations and traversal overhead.","rollback_reason":"Benchmark regressed slightly versus baseline; extra match logic inside the fused loop likely outweighed the saved temporary vector on this workload.","next_action_hint":"Try retaining the simpler multi-pass structure but preallocate exact capacities and reduce repeated iterator machinery more surgically.","learning":"The parser cost may dominate enough that micro-optimizing traversal shape alone is not a guaranteed win."}}
4 {"run":3,"commit":"03ead6f","metric":168392,"metrics":{"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"discard","description":"Preallocate object/index storage while keeping the original three-pass vertex extraction structure.","timestamp":1776686218166,"segment":0,"confidence":null,"iterationTokens":440,"asi":{"hypothesis":"Exact-capacity reservation may trim allocator overhead without changing the loader's dataflow.","rollback_reason":"A confirmation rerun landed above baseline, so the tiny apparent improvement was noise-level and not trustworthy.","next_action_hint":"Look for changes that cut larger costs, or improve benchmark instrumentation to isolate parser versus conversion time.","learning":"Capacity reservation alone does not produce a meaningful end-to-end win on this workload.","note":"One immediate confirmation rerun after an initial near-tie also failed to show a durable improvement."}}
5+{"run":4,"commit":"6d8f169","metric":168804,"metrics":{"parse_us":163471,"convert_us":6148,"bytes":978234,"grid":96,"iterations":24,"objects":24},"status":"keep","description":"Add parser/conversion split metrics to the benchmark to reveal where OBJ load time is spent.","timestamp":1776686285511,"segment":0,"confidence":null,"iterationTokens":3127,"asi":{"hypothesis":"Extra benchmark instrumentation will show whether future optimization work should focus on wavefront parsing or Rendy's conversion layer.","observation":"On this workload, parse time (~163 ms) dwarfs conversion time (~6 ms), so most end-to-end wins likely require attacking parse overhead or changing the accepted input path.","reason_to_keep":"This keeps the benchmark more informative for subsequent experiments even though the measured primary metric itself did not improve.","next_focus":"Favor ideas that reduce UTF-8/parse overhead, or consider API-level fast paths that bypass reparsing when structured OBJ data is already available."}}
1 //! Loading mesh data from obj format.
2
3 use log::trace;
4+use std::str;
5 use {
6 crate::{mesh::MeshBuilder, Normal, Position, TexCoord},
7 wavefront_obj::obj,
11 pub fn load_from_obj(
12 bytes: &[u8],
13 ) -> Result<Vec<(MeshBuilder<'static>, Option<String>)>, failure::Error> {
13- let string = std::str::from_utf8(bytes)?;
14+ let string = str::from_utf8(bytes)?;
15+
16+ if let Some(meshes) = try_load_fast_path(string)? {
17+ return Ok(meshes);
18+ }
19+
20 let set = obj::parse(string).map_err(|e| {
21 failure::format_err!(
22 "Error during parsing obj-file at line '{}': {}",
27 load_from_data(set)
28 }
29
30+fn try_load_fast_path(
31+ string: &str,
32+) -> Result<Option<Vec<(MeshBuilder<'static>, Option<String>)>>, failure::Error> {
33+ let mut positions_src = Vec::new();
34+ let mut normals_src = Vec::new();
35+ let mut tex_coords_src = Vec::new();
36+ let mut positions = Vec::new();
37+ let mut normals = Vec::new();
38+ let mut tex_coords = Vec::new();
39+
40+ for line in string.lines() {
41+ let line = line.trim();
42+ if line.is_empty() || line.starts_with('#') || line.starts_with("s ") {
43+ continue;
44+ }
45+
46+ let mut parts = line.split_ascii_whitespace();
47+ let tag = match parts.next() {
48+ Some(tag) => tag,
49+ None => continue,
50+ };
51+
52+ match tag {
53+ "v" => match parse_position(parts) {
54+ Ok(position) => positions_src.push(position),
55+ Err(_) => return Ok(None),
56+ },
57+ "vn" => match parse_normal(parts) {
58+ Ok(normal) => normals_src.push(normal),
59+ Err(_) => return Ok(None),
60+ },
61+ "vt" => match parse_tex_coord(parts) {
62+ Ok(tex_coord) => tex_coords_src.push(tex_coord),
63+ Err(_) => return Ok(None),
64+ },
65+ "f" => {
66+ let a = match parse_face_index(parts.next(), positions_src.len(), tex_coords_src.len(), normals_src.len()) {
67+ Ok(index) => index,
68+ Err(_) => return Ok(None),
69+ };
70+ let b = match parse_face_index(parts.next(), positions_src.len(), tex_coords_src.len(), normals_src.len()) {
71+ Ok(index) => index,
72+ Err(_) => return Ok(None),
73+ };
74+ let c = match parse_face_index(parts.next(), positions_src.len(), tex_coords_src.len(), normals_src.len()) {
75+ Ok(index) => index,
76+ Err(_) => return Ok(None),
77+ };
78+ if parts.next().is_some() {
79+ return Ok(None);
80+ }
81+
82+ for index in [a, b, c] {
83+ positions.push(positions_src[index.0]);
84+ tex_coords.push(index.1.map(|i| tex_coords_src[i]).unwrap_or(TexCoord([0.0, 0.0])));
85+ normals.push(index.2.map(|i| normals_src[i]).unwrap_or(Normal([0.0, 0.0, 0.0])));
86+ }
87+ }
88+ _ => return Ok(None),
89+ }
90+ }
91+
92+ if positions.is_empty() {
93+ return Ok(None);
94+ }
95+
96+ let mut builder = MeshBuilder::new();
97+ builder.add_vertices(positions);
98+ builder.add_vertices(normals);
99+ builder.add_vertices(tex_coords);
100+ Ok(Some(vec![(builder, None)]))
101+}
102+
103+fn parse_position<'a, I>(parts: I) -> Result<Position, failure::Error>
104+where
105+ I: Iterator<Item = &'a str>,
106+{
107+ Ok(Position(parse_xyz(parts)?))
108+}
109+
110+fn parse_normal<'a, I>(parts: I) -> Result<Normal, failure::Error>
111+where
112+ I: Iterator<Item = &'a str>,
113+{
114+ Ok(Normal(parse_xyz(parts)?))
115+}
116+
117+fn parse_xyz<'a, I>(mut parts: I) -> Result<[f32; 3], failure::Error>
118+where
119+ I: Iterator<Item = &'a str>,
120+{
121+ let x = parse_f32(parts.next())?;
122+ let y = parse_f32(parts.next())?;
123+ let z = parse_f32(parts.next())?;
124+ if parts.next().is_some() {
125+ return Err(failure::format_err!("unsupported vertex format"));
126+ }
127+ Ok([x, y, z])
128+}
129+
130+fn parse_tex_coord<'a, I>(mut parts: I) -> Result<TexCoord, failure::Error>
131+where
132+ I: Iterator<Item = &'a str>,
133+{
134+ let u = parse_f32(parts.next())?;
135+ let v = parse_f32(parts.next())?;
136+ if parts.next().is_some() {
137+ return Err(failure::format_err!("unsupported texture vertex format"));
138+ }
139+ Ok(TexCoord([u, v]))
140+}
141+
142+fn parse_f32(value: Option<&str>) -> Result<f32, failure::Error> {
143+ value
144+ .ok_or_else(|| failure::format_err!("missing float"))?
145+ .parse::<f32>()
146+ .map_err(Into::into)
147+}
148+
149+fn parse_face_index(
150+ value: Option<&str>,
151+ positions_len: usize,
152+ tex_coords_len: usize,
153+ normals_len: usize,
154+) -> Result<(usize, Option<usize>, Option<usize>), failure::Error> {
155+ let value = value.ok_or_else(|| failure::format_err!("missing face index"))?;
156+ let mut parts = value.split('/');
157+ let position = parse_obj_index(parts.next(), positions_len)?;
158+ let tex_coord = match parts.next() {
159+ Some("") | None => None,
160+ Some(value) => Some(parse_obj_index(Some(value), tex_coords_len)?),
161+ };
162+ let normal = match parts.next() {
163+ Some("") | None => None,
164+ Some(value) => Some(parse_obj_index(Some(value), normals_len)?),
165+ };
166+ if parts.next().is_some() {
167+ return Err(failure::format_err!("unsupported face format"));
168+ }
169+ Ok((position, tex_coord, normal))
170+}
171+
172+fn parse_obj_index(value: Option<&str>, len: usize) -> Result<usize, failure::Error> {
173+ let raw = value
174+ .ok_or_else(|| failure::format_err!("missing index"))?
175+ .parse::<isize>()?;
176+ if raw <= 0 {
177+ return Err(failure::format_err!("relative or zero indices are unsupported in fast path"));
178+ }
179+ let index = raw as usize - 1;
180+ if index >= len {
181+ return Err(failure::format_err!("index out of bounds"));
182+ }
183+ Ok(index)
184+}
185+
186 fn load_from_data(
187 obj_set: obj::ObjSet,
188 ) -> Result<Vec<(MeshBuilder<'static>, Option<String>)>, failure::Error> {