Skip to content

Fix Starfield BSGeometry mesh data export#59

Merged
ousnius merged 10 commits into
ousnius:mainfrom
DJLegends1011:fix/starfield-mesh-export
Apr 11, 2026
Merged

Fix Starfield BSGeometry mesh data export#59
ousnius merged 10 commits into
ousnius:mainfrom
DJLegends1011:fix/starfield-mesh-export

Conversation

@DJLegends1011
Copy link
Copy Markdown
Contributor

@DJLegends1011 DJLegends1011 commented Apr 10, 2026

Summary

  • Fix SyncUDEC3 write: bitwise OR (|=) instead of AND (&=), and set W bit (1 << 30)
  • Fix BSGeometryMeshData::Sync to sync element counts before writing data
  • Fix vertex coordinate packing to use int16_t with std::round for correct precision
  • Fix SaveExternalShapeData: use Mode::Writing instead of Mode::Reading, call mesh->meshData.Sync instead of mesh->Sync
  • Add BSGeometry finalization in FinalizeData() to update triSize and numVerts from mesh data

These fixes enable correct export of external .mesh files for Starfield BSGeometry shapes. Companion PR: ousnius/BodySlide-and-Outfit-Studio#570

Test plan

  • Export a Starfield NIF with BSGeometry shapes — verify .mesh files are written with correct data
  • Load exported .mesh files in NifSkope — verify vertex positions, normals, and triangles are intact
  • Verify no regressions for non-Starfield NIF read/write

🤖 Generated with Claude Code

DJLegends1011 and others added 7 commits April 10, 2026 15:56
- Fix SyncUDEC3 write: use |= instead of &= for packing, add W bit (1 << 30)
- Add count sync before write in BSGeometryMeshData::Sync
- Fix vertex pack to use int16_t + std::round for correct precision
- Fix SaveExternalShapeData: use Mode::Writing and meshData.Sync
- Add BSGeometry finalization in FinalizeData() to update triSize/numVerts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BSGeometry flag 512 (0x200) controls whether mesh data is embedded
inline in the NIF or stored as external .mesh files. This adds
read/write support for both modes and exposes HasInternalGeomData()
and SetInternalGeomData() API for toggling the flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cast ~0x200 to uint32_t to avoid GCC -Werror=sign-conversion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FinalizeData was overwriting triSize and numVerts from meshData even
when external mesh data hadn't been loaded, zeroing the header values.
Only update counts when mesh data is actually populated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove upper bound on IsSF() stream version check. Starfield patches
(MeshesPatch.ba2) ship NIFs with stream version 175 which use the
same format as 172/173.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per ousnius's feedback — keep a bounded range (172–175) so unknown
future stream versions don't silently pass validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Starfield BSGeometry mesh export by correcting multiple serialization/write-path issues so external .mesh files (and related BSGeometry metadata) are written consistently and with correct packing/counts.

Changes:

  • Corrects Starfield packing/serialization primitives (SyncUDEC3, vertex coordinate packing) and expands Starfield version detection.
  • Fixes .mesh external save path to use writing mode and serialize BSGeometryMeshData directly; avoids returning external path refs when geometry is internal.
  • Updates BSGeometry mesh metadata finalization and adds support for serializing internal (inline) mesh data vs external mesh name references.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/NifFile.cpp Fix external .mesh save mode/entry point; avoid exposing mesh path refs for internal-geometry BSGeometry; update tri/vert metadata during finalization.
src/Geometry.cpp Sync counts before writing mesh data; fix vertex packing precision; serialize inline mesh data when internal geometry is enabled.
include/Geometry.hpp Introduce internal/external geometry helpers and per-mesh internalGeom switch for correct serialization behavior.
include/BasicTypes.hpp Expand Starfield stream version range; fix SyncUDEC3 bit packing (OR vs AND and set W bit).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Geometry.cpp
Comment on lines +1603 to +1609
// When writing, update counts from actual data sizes
if (stream.GetMode() == NiStreamReversible::Mode::Writing) {
nTriIndices = static_cast<uint32_t>(tris.size()) * 3;
nVertices = static_cast<uint32_t>(vertices.size());
numVertices = static_cast<uint16_t>(std::min(nVertices, static_cast<uint32_t>(0xFFFF)));
nUV1 = uvSets.size() > 0 ? static_cast<uint32_t>(uvSets[0].size()) : 0;
nUV2 = uvSets.size() > 1 ? static_cast<uint32_t>(uvSets[1].size()) : 0;
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In BSGeometryMeshData::Sync, the new writing-mode count refresh clamps numVertices to 0xFFFF, but later the function unconditionally does numVertices = (uint16_t)nVertices after syncing nVertices, which overrides the clamp and can wrap if nVertices > 65535. Consider only assigning numVertices from nVertices in reading mode, or keep numVertices consistent with the same clamp when writing.

Copilot uses AI. Check for mistakes.
ousnius added 3 commits April 11, 2026 19:23
Set numVertices differently depending on stream mode: when reading, cast nVertices to uint16_t as before; when writing, clamp nVertices to 0xFFFF using std::min before casting. This ensures we don't write a numVertices larger than a 16-bit limit while preserving the original value when reading.
Add support for the 2-bit W component of UDEC3 tangents: introduce SyncUDEC3 to pack/unpack tangent W bits and switch packing to std::round for consistent rounding.

Store per-tangent W values in Geometry (tangentWs) and update BSGeometryMeshData::Sync to read/write tangent W components.

Add tests that load/save external .mesh data and convert external meshes to internal geometry.
Change variable 'di' from int to size_t to match other size/index types (e.g. map.size() and vector indexing). This fixes signed/unsigned mismatches and avoids related compiler warnings when tracking deleted triangle indices.
@ousnius ousnius self-requested a review April 11, 2026 19:01
Copy link
Copy Markdown
Owner

@ousnius ousnius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some more fixes and unit tests. This should be fine now.

@ousnius ousnius merged commit 3553ad9 into ousnius:main Apr 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants