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 three write_ply tiers. Kept separate from detail::PLYHeader::Format (three values, "what I parsed") rather than leaking a detail type into a public signature.
- Default stays
ASCII, so existing callers and header-grepping tests are unaffected; pgs-decimate opts in.
Binary writes native-endian and labels the header accordingly.
- Scalars always written as
float32 regardless of the mesh's T, 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
write per vertex — rather than one write per property.
- Open all three tiers with
std::ios::binary unconditionally. Note: this is a silent change for Windows ASCII callers, who stop getting CRLF. read_ply already 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_endian rejection.
- Swap enters at the two choke points every binary scalar read passes through:
read_ply_binary_prop (stream) and read_ply_prop_from_buf (buffer).
- Runtime
bool rather 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 00 memcpy'd into a native float is 4.6e-41, and no reversal of the widened double recovers 1.0.
Validation
- Throw when a face exceeds the
uchar list-count limits. Two distinct limits: 255 corners for vertex_indices, and 127 for texcoord, since that list writes 2*N. Declarations stay uchar — widening to uint32 would 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.
@throws documented on the tier-2/tier-3 write_ply overloads and the corresponding write_mesh dispatchers; 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_endian fixture 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.
Track:
ply-binary-io_20260904write_plyemitsformat ascii 1.0unconditionally. For a multi-million-face mesh that is several times the size of the binary equivalent and correspondingly slower to write —pgs-decimatewrites its output once per improved search round and pays that repeatedly. Separately,read_plysupportsbinary_little_endianbut does not actually honor the header's declared endianness:read_ply_binary_propandread_ply_prop_from_bufreinterpret raw bytes as native types, so it is native-endian reading labeled little-endian, andbinary_big_endianis 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
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.ASCII, so existing callers and header-grepping tests are unaffected;pgs-decimateopts in.Binarywrites native-endian and labels the header accordingly.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.writeper vertex — rather than onewriteper property.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
binary_big_endianrejection.read_ply_binary_prop(stream) andread_ply_prop_from_buf(buffer).boolrather than a template parameter: the flag is loop-invariant so the branch predicts perfectly, and templating doubles instantiated code for no gain.3F 80 00 00memcpy'd into a native float is 4.6e-41, and no reversal of the wideneddoublerecovers 1.0.Validation
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.@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.
binary_big_endianfixture with bytes reversed by the test, exercising the swap path on an LE host.Sequencing
Lands before #19 (Multi-Chart PLY Write Support). Both rewrite
write_ply_headerandwrite_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.