Repository navigation
PLY Binary IO: binary write support and header-declared endianness on read #25
Description
Track: ply-binary-io_20260904
write_ply emits format ascii 1.0 unconditionally. For a multi-million-face mesh that is several times the size of the binary equivalent and correspondingly slower to write — pgs-decimate writes its output once per improved search round and pays that repeatedly. Separately, read_ply supports binary_little_endian but does not actually honor the header's declared endianness: read_ply_binary_prop and read_ply_prop_from_buf reinterpret raw bytes as native types, so it is native-endian reading labeled little-endian, and binary_big_endian is rejected outright.
These are one contract and land together: a writer that can emit binary must not be able to produce a file the reader refuses or silently misreads.
Scope
Write
- Public
educelab::PLYFormat { ASCII, Binary }, threaded through all threewrite_plytiers. Kept separate fromdetail::PLYHeader::Format(three values, "what I parsed") rather than leaking adetailtype into a public signature. - Default stays
ASCII, so existing callers and header-grepping tests are unaffected;pgs-decimateopts in. Binarywrites native-endian and labels the header accordingly.- Scalars always written as
float32regardless of the mesh'sT, matching what the header already declares in ASCII and what OpenMVS emits. Avoids the on-disk format depending on a template parameter. - Mirror the reader's record batching — precomputed property offsets, one
writeper vertex — rather than onewriteper property. - Open all three tiers with
std::ios::binaryunconditionally. Note: this is a silent change for Windows ASCII callers, who stop getting CRLF.read_plyalready trims\r, so nothing regresses on read.
Read
- Honor the endianness declared in the header, byte-swapping when file order differs from host order. Removes the
binary_big_endianrejection. - Swap enters at the two choke points every binary scalar read passes through:
read_ply_binary_prop(stream) andread_ply_prop_from_buf(buffer). - Runtime
boolrather than a template parameter: the flag is loop-invariant so the branch predicts perfectly, and templating doubles instantiated code for no gain. - The swap must apply to the raw fixed-width value before the cast to the destination type. Converting first and reversing afterwards yields a valid number that is the wrong one — BE
3F 80 00 00memcpy'd into a native float is 4.6e-41, and no reversal of the wideneddoublerecovers 1.0.
Validation
- Throw when a face exceeds the
ucharlist-count limits. Two distinct limits: 255 corners forvertex_indices, and 127 fortexcoord, since that list writes2*N. Declarations stayuchar— widening touint32would cost 3 bytes per face (~30MB on a 10M-face mesh) to guard a case that does not occur. - Message names which limit fired and the offending face index.
@throwsdocumented on the tier-2/tier-3write_plyoverloads and the correspondingwrite_meshdispatchers; tier 1 carries only the 255 limit.
Testing
Round-trip tests cannot anchor this work — the sized-alias bug in #24 survived precisely because the suite only ever read what libcore wrote, and a byte-order mistake shared by reader and writer round-trips just as happily.
- Reader: a hand-crafted
binary_big_endianfixture with bytes reversed by the test, exercising the swap path on an LE host. - Writer: byte-level assertion of a small mesh's binary body against a hand-derived literal, so the test encodes the format rather than libcore's opinion of it. Round-trip used only for structural breadth (n-gons, UVs, colors) where hand-computing bytes stops paying.
Sequencing
Lands before #19 (Multi-Chart PLY Write Support). Both rewrite write_ply_header and write_ply_data; #19 is Pending at 0/15, this is the live need, and it is the smaller and more additive of the two.
Depends on #24.
Activity
docs(conductor): register PLY Binary IO and write_mesh options tracks (…
docs(conductor): PLY binary IO spec and implementation plan
feat(MeshIO): binary PLY write and byte-order-correct read (#25) (#28)