1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
use draco_core::mesh::Mesh;
// Parse C++ debug log lines of the form:
// "C++ OnNewVertexVisited: data_id=0 vertex=1 corner=1 point_id=1"
// For encoder logs, look for "C++ Encoder OnNewVertexVisited" prefix instead
fn parse_cpp_encoder_lines(content: &str) -> Vec<String> {
// Parse C++ "OnNewVertexVisited" lines from encoder and return canonical corner->point sequences
// We specifically want the ENCODER section, which starts after "ORIG GenerateSequenceInternal: using corner_order"
let mut out = Vec::new();
let mut in_encoder_section = false;
for line in content.lines() {
// Detect encoder section (has corner_order, not "NO corner_order")
if line.contains("using corner_order with") {
in_encoder_section = true;
}
// Detect decoder section (uses sequential faces)
if line.contains("NO corner_order") {
in_encoder_section = false;
}
// Only parse encoder OnNewVertexVisited lines
if in_encoder_section && line.contains("C++ OnNewVertexVisited") {
// extract corner and point_id
let mut corner: Option<&str> = None;
let mut point_id: Option<&str> = None;
for token in line.split_whitespace() {
if token.starts_with("corner=") {
corner = Some(token.trim_start_matches("corner=").trim_end_matches(','));
}
if token.starts_with("point_id=") {
point_id = Some(token.trim_start_matches("point_id=").trim_end_matches(','));
}
}
if let (Some(c), Some(p)) = (corner, point_id) {
// Use canonical point-level mapping: MAP_POINT:{corner}->p{point_id}
out.push(format!("MAP_POINT:{}->p{}", c, p));
}
}
}
out
}
#[test]
fn test_cpp_grid5x5_encoder_parity() {
// Initialize and clear the test event log
draco_core::test_event_log::init();
draco_core::test_event_log::clear();
// Read expected events from the original C++ debug log (encoder section)
let cpp_log = std::env::var("DRACO_CPP_GRID5X5_LOG")
.ok()
.map(std::fs::read_to_string)
.transpose()
.or_else(|_| {
std::fs::read_to_string("../../debug_logs/cpp_dec_grid5x5_original.txt").map(Some)
})
.or_else(|_| std::fs::read_to_string("./debug_logs/cpp_dec_grid5x5_original.txt").map(Some))
.unwrap_or(None);
let Some(cpp_log) = cpp_log else {
println!(
"Skipping test - C++ debug log not found. Set DRACO_CPP_GRID5X5_LOG or place cpp_dec_grid5x5_original.txt in debug_logs/"
);
return;
};
let expected = parse_cpp_encoder_lines(&cpp_log);
// Build a 5x5 grid and run the Rust encoder to capture encoder traversal events
fn create_grid_mesh(width: u32, height: u32) -> Mesh {
// Build like C++ TriangleSoupMeshBuilder: add faces and create points on first use,
// so that point indices are assigned in the same order as the C++ builder.
let mut mesh = Mesh::new();
// Temporary storage for unique points and a map from raw bytes to point index.
use std::collections::HashMap;
let mut value_map: HashMap<Vec<u8>, u32> = HashMap::new();
let mut unique_points: Vec<[f32; 3]> = Vec::new();
// Create a position attribute with no points yet.
let mut pos_attr = draco_core::geometry_attribute::PointAttribute::new();
pos_attr.init(
draco_core::geometry_attribute::GeometryAttributeType::Position,
3,
draco_core::draco_types::DataType::Float32,
false,
0,
);
// Reserve face storage and assign faces by mapping coordinates to point ids in the same order
// Triangle 1: p0(x,y), p1(x+1,y), p2(x,y+1)
// Triangle 2: p1(x+1,y), p3(x+1,y+1), p2(x,y+1)
let mut face_idx = 0u32;
for _ in 0..((width - 1) * (height - 1) * 2) {
// placeholder faces, we'll set them below once we have point ids
mesh.set_face(
draco_core::FaceIndex(face_idx),
[
draco_core::PointIndex(0),
draco_core::PointIndex(0),
draco_core::PointIndex(0),
],
);
face_idx += 1;
}
// Reset face_idx and fill faces while creating points in encountered order.
face_idx = 0;
for y in 0..height - 1 {
for x in 0..width - 1 {
let coords = [
[x as f32, y as f32, 0.0f32],
[x as f32 + 1.0f32, y as f32, 0.0f32],
[x as f32, y as f32 + 1.0f32, 0.0f32],
[x as f32 + 1.0f32, y as f32 + 1.0f32, 0.0f32],
];
let mut ids = [0u32; 4];
for i in 0..4 {
let bytes: Vec<u8> = coords[i]
.iter()
.flat_map(|f| f.to_le_bytes().to_vec())
.collect();
if let Some(&id) = value_map.get(&bytes) {
ids[i] = id;
} else {
let id = unique_points.len() as u32;
value_map.insert(bytes, id);
unique_points.push(coords[i]);
ids[i] = id;
}
}
// Triangle 1: p0, p1, p2
mesh.set_face(
draco_core::FaceIndex(face_idx),
[
draco_core::PointIndex(ids[0]),
draco_core::PointIndex(ids[1]),
draco_core::PointIndex(ids[2]),
],
);
face_idx += 1;
// Triangle 2: p1, p3, p2
mesh.set_face(
draco_core::FaceIndex(face_idx),
[
draco_core::PointIndex(ids[1]),
draco_core::PointIndex(ids[3]),
draco_core::PointIndex(ids[2]),
],
);
face_idx += 1;
}
}
// Now set the unique points into the attribute buffer and attach to the mesh.
let num_points = unique_points.len();
mesh.set_num_points(num_points);
// Re-init pos_attr with correct size
pos_attr.init(
draco_core::geometry_attribute::GeometryAttributeType::Position,
3,
draco_core::draco_types::DataType::Float32,
false,
num_points,
);
for (i, p) in unique_points.iter().enumerate() {
let offset = i * 3 * 4;
pos_attr
.buffer_mut()
.update(&p[0].to_le_bytes(), Some(offset));
pos_attr
.buffer_mut()
.update(&p[1].to_le_bytes(), Some(offset + 4));
pos_attr
.buffer_mut()
.update(&p[2].to_le_bytes(), Some(offset + 8));
}
mesh.add_attribute(pos_attr);
mesh
}
// Enable verbose encoder logging for this test so we can compare internal seeds
std::env::set_var("DRACO_VERBOSE", "1");
// Run encoder on a 5x5 grid and capture encoder events
let mesh = create_grid_mesh(5, 5);
// Print attribute mapped indices for comparison with C++ dedup results
if std::env::var("DRACO_VERBOSE").is_ok() {
let att = mesh.attribute(0);
let mut mapped = Vec::new();
for i in 0..mesh.num_points() {
mapped.push(att.mapped_index(draco_core::PointIndex(i as u32)).0);
}
println!("Rust pos mapped (first 25): {:?}", mapped);
}
let mut encoder = draco_core::mesh_encoder::MeshEncoder::new();
encoder.set_mesh(mesh);
draco_core::test_event_log::clear();
let mut enc_buf = draco_core::encoder_buffer::EncoderBuffer::new();
let mut opts = draco_core::encoder_options::EncoderOptions::default();
opts.set_global_int("encoding_method", 1);
opts.set_global_int("encoding_speed", 5);
encoder.encode(&opts, &mut enc_buf).expect("Encode failed");
let recorded_all = draco_core::test_event_log::take_events();
// Filter recorded events to the canonical MAP_POINT entries
let recorded: Vec<String> = recorded_all
.into_iter()
.filter(|s| s.starts_with("MAP_POINT:"))
.collect();
// Compare canonical point-level sequences (C++ logs only first 10 visits)
let min_len = std::cmp::min(expected.len(), recorded.len());
println!(
"\nExpected (from C++ encoder, {} entries): {:?}",
expected.len(),
expected
);
println!(
"Recorded (from Rust encoder, {} entries): {:?}",
recorded.len(),
&recorded[..min_len.min(40)]
);
for i in 0..min_len {
if expected[i] != recorded[i] {
panic!("Event mismatch idx {}: exp='{}' got='{}'\nExpected (first {}): {:?}\nRecorded (first {}): {:?}",
i, expected[i], recorded[i], min_len, &expected[..min_len], min_len, &recorded[..min_len]);
}
}
// Only warn if lengths differ since C++ might only log first N visits
if expected.len() != recorded.len() {
println!("Note: Event sequences differ in length: expected={} recorded={} (C++ only logs first 10)", expected.len(), recorded.len());
}
// If we got here without panic, first min_len entries match
println!(
"SUCCESS: First {} entries match between C++ and Rust encoder",
min_len
);
}