diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 996878b..40ad813 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -1,11 +1,22 @@ -# Binding Simple CD - Template - -# This template is for pure .NET bindings (no XML update) to publish NuGets to Nuget.org. -# Copy this file to your repository as `.github/workflows/CD.yml` and customize the inputs below. +# Keeps this binding on the latest stable meshoptimizer release, header and +# native libraries together. +# +# Three jobs rather than one, because the two halves cannot share a runner: the +# native libraries need a matrix across Linux, Windows and macOS, and the +# generator and packaging need a single machine holding the whole tree. +# +# `resolve` runs first and alone so exactly one place decides which revision this +# run is about. If the header and the binaries each worked it out for themselves, +# a release published midway through would leave them on different versions -- +# and that particular mismatch is invisible: P/Invoke binds late, so the package +# compiles, passes CI, publishes, and throws in the consumer's application. name: CD on: + schedule: + # Monthly, in step with the rest of the fleet. + - cron: '0 2 1 * *' workflow_dispatch: inputs: skip-assets-publishing: @@ -13,31 +24,67 @@ on: required: false type: boolean default: false + force-natives: + description: 'Rebuild the native libraries even when the release has not moved' + required: false + type: boolean + default: false + force-publish: + description: 'Publish even when the generated code is unchanged' + required: false + type: boolean + default: false jobs: - cd: + resolve: if: github.event_name != 'schedule' || github.ref == 'refs/heads/main' - uses: EvergineTeam/Evergine.Bindings/.github/workflows/binding-simple-cd.yml@v1 + uses: EvergineTeam/Evergine.Bindings/.github/workflows/binding-resolve-upstream.yml@v1 + + natives: + needs: resolve + # Five platform builds is not something to spend on a maybe, hence resolving + # first and asking. + if: needs.resolve.outputs.ref_moved == 'true' || inputs.force-natives + uses: ./.github/workflows/meshopt-cmake.yml + with: + ref: ${{ needs.resolve.outputs.resolved_ref }} + + cd: + needs: [resolve, natives] + # `natives` is skipped whenever the release has not moved, and a skipped + # dependency would otherwise take this job with it. The run still has to + # reach the CD so it can report the no-op. + if: always() && needs.resolve.result == 'success' && needs.natives.result != 'failure' && !cancelled() + uses: EvergineTeam/Evergine.Bindings/.github/workflows/binding-tracked-cd.yml@v1 with: - generator-project: "MeshOptimizerGen/MeshOptimizerGen/MeshOptimizerGen.csproj" # Path to your generator .csproj - generator-name: "MeshOptimizer" # Name of your generator executable - binding-project: "MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Evergine.Bindings.MeshOptimizer.csproj" # Path to your binding .csproj - target-framework: "net10.0" # Target framework for generator/binding - dotnet-version: "10.x" # .NET SDK version - nuget-version: "6.x" # NuGet CLI version - runtime-identifier: "linux-x64" # Runtime identifier (win-x64, linux-x64, etc.) - build-configuration: "Release" # Build configuration (Release, Debug, etc.) - revision: ${{ github.run_number }} # Revision for date-based version (bindings style). Use with bindings. - publish-enabled: ${{ !inputs.skip-assets-publishing }} # Publish NuGets to Nuget.org - enable-email-notifications: true # Enable email notifications on failure + generator-project: "MeshOptimizerGen/MeshOptimizerGen/MeshOptimizerGen.csproj" + generator-name: "MeshOptimizer" + binding-project: "MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Evergine.Bindings.MeshOptimizer.csproj" + target-framework: "net10.0" + dotnet-version: "10.x" + nuget-version: "6.x" + # Generation runs on Windows because that is where the CppAst generator can + # read the header at all. The libclang shipped in the NuGet package carries + # no system include paths and none of its own builtin headers, so on Linux + # `#include ` and `` do not resolve and the parse fails + # before a single binding is produced. RenderDoc.NET and KTX.NET generate on + # Windows for the same reason. + # + # This does not constrain the package: the five native libraries are built + # in their own matrix job, and `runtime-identifier` here only feeds the + # generator's publish, never the packing. + runner-os: windows-latest + runtime-identifier: "win-x64" + build-configuration: "Release" + revision: ${{ github.run_number }} + # Every artifact already carries its final repository path, so unpacking + # them over the tree is the whole mapping. + natives-artifact-pattern: "natives-*" + force-publish: ${{ inputs.force-publish || false }} + publish-enabled: ${{ !inputs.skip-assets-publishing }} + enable-email-notifications: true secrets: NUGET_UPLOAD_TOKEN: ${{ secrets.EVERGINE_NUGETORG_TOKEN }} WAVE_SENDGRID_TOKEN: ${{ secrets.WAVE_SENDGRID_TOKEN }} EVERGINE_EMAILREPORT_LIST: ${{ secrets.EVERGINE_EMAILREPORT_LIST }} EVERGINE_EMAIL: ${{ secrets.EVERGINE_EMAIL }} - -# Tips: -# - For direct version (add-ons style): -# version: "3.4.22.288-local" -# - For date-based version (bindings style): -# revision: "" # Uses github.run_number or custom logic diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d13d100..f1bf179 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -26,7 +26,13 @@ jobs: generator-name: "MeshOptimizer" # Name of your generator executable binding-project: "MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Evergine.Bindings.MeshOptimizer.csproj" # Path to your binding .csproj target-framework: "net10.0" # Target framework for generator/binding - runtime-identifier: "linux-x64" # Runtime identifier (win-x64, linux-x64, etc.) + # Windows, because the CppAst generator cannot read the header anywhere + # else: the libclang in the NuGet package has no system include paths and + # none of its own builtin headers, so and fail to + # resolve on Linux. CI must match CD or it validates a build the CD will + # not reproduce. + runner-os: windows-latest + runtime-identifier: "win-x64" # Runtime identifier (win-x64, linux-x64, etc.) build-configuration: "Release" # Build configuration (Release, Debug, etc.) nuget-artifacts: ${{ inputs.publish-artifacts || false }} # Upload NuGets as workflow artifacts revision: ${{ github.run_number }} # Revision for date-based version (bindings style). Use with bindings. diff --git a/.github/workflows/api-gate.yml b/.github/workflows/api-gate.yml new file mode 100644 index 0000000..8a5be6e --- /dev/null +++ b/.github/workflows/api-gate.yml @@ -0,0 +1,34 @@ +# Two questions on every pull request, both about contracts nobody can see in a +# diff of 6,000 generated lines. +# +# `api` measures what the change does to the public managed API. `coherence` +# checks that every P/Invoke still resolves in the native libraries the package +# ships, on all five platforms -- the one failure that compiles, passes CI, +# publishes, and only shows up in the consumer's application. +# +# auto-merge stays off here. This binding rebuilds five native libraries when it +# bumps, and that pipeline has run a handful of times; measuring first, merging +# later. + +name: API Gate + +on: + pull_request: + branches: [ "main" ] + +jobs: + api: + uses: EvergineTeam/Evergine.Bindings/.github/workflows/binding-api-gate.yml@v1 + with: + binding-project: "MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Evergine.Bindings.MeshOptimizer.csproj" + auto-merge: false + # meshoptimizer.h reports its own version, so this constant changes on every + # release. The gate counts a changed constant value as a removal, on purpose + # -- that is how a silent renumbering gets caught -- which would make every + # bump "breaking" and the verdict worthless. Exempting it leaves the verdict + # meaning what it says. + exempt-symbols: | + \.VERSION = + + coherence: + uses: EvergineTeam/Evergine.Bindings/.github/workflows/binding-native-coherence.yml@v1 diff --git a/.github/workflows/meshopt-cmake.yml b/.github/workflows/meshopt-cmake.yml index d6ea4ba..6a4594f 100644 --- a/.github/workflows/meshopt-cmake.yml +++ b/.github/workflows/meshopt-cmake.yml @@ -1,36 +1,80 @@ +# Builds the five native libraries this package ships. +# +# Callable from CD.yml, which passes the release the manifest resolved to. The +# header and these binaries must come from the same revision: P/Invoke binds +# late, so a mismatched package compiles, passes CI, publishes, and throws +# EntryPointNotFoundException in the consumer's application. Nothing between +# here and there would notice. +# +# The cmake invocations stay in this repository rather than in the toolbox. +# meshoptimizer's build has nothing in common with KTX's or xatlas's, and +# parameterising all three into one reusable workflow would produce something +# with an escape hatch per project -- worse than three small honest files. + name: Build meshoptimizer Libraries on: + workflow_call: + inputs: + ref: + description: 'meshoptimizer revision to build' + required: true + type: string workflow_dispatch: + inputs: + ref: + description: 'meshoptimizer revision to build (tag, branch or SHA)' + required: true + type: string + default: 'v1.2' jobs: build: runs-on: ${{ matrix.os }} strategy: + # One platform failing must not leave the others publishing a half-updated + # set of binaries. + fail-fast: true matrix: include: - os: ubuntu-latest arch: x64 cmake-arch: x64 + rid: linux-x64 + libname: libmeshoptimizer.so - os: ubuntu-latest arch: arm64 cmake-arch: aarch64 + rid: linux-arm64 + libname: libmeshoptimizer.so - os: windows-latest arch: x64 cmake-arch: x64 + rid: win-x64 + libname: meshoptimizer.dll - os: windows-latest arch: arm64 cmake-arch: ARM64 + rid: win-arm64 + libname: meshoptimizer.dll - os: macos-latest arch: arm64 cmake-arch: arm64 + rid: osx-arm64 + libname: libmeshoptimizer.dylib steps: - name: Checkout meshoptimizer - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: zeux/meshoptimizer - ref: v1.0 + ref: ${{ inputs.ref }} + + - name: Record what is being built + shell: bash + run: | + echo "Building ${{ inputs.ref }} for ${{ matrix.rid }}" + grep -m1 MESHOPTIMIZER_VERSION src/meshoptimizer.h - name: Install dependencies on Ubuntu if: matrix.os == 'ubuntu-latest' @@ -80,23 +124,60 @@ jobs: - name: Build meshoptimizer run: cmake --build build --config Release - - name: Upload meshoptimizer.dll (Windows) - if: matrix.os == 'windows-latest' - uses: actions/upload-artifact@v4 - with: - name: meshoptimizer-${{ matrix.arch }}-dll - path: build/**/*.dll + # Staged into the exact layout the package expects, so the CD can unpack + # every platform's artifact over the project directory and be done. The + # previous version uploaded `build/**/*.dll`, whose intermediate paths + # differ per platform, and a human mapped them by hand afterwards. + - name: Stage into the runtimes layout + shell: bash + run: | + set -euo pipefail + dest="staged/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/${{ matrix.rid }}/native" + mkdir -p "$dest" + found=$(find build -name '${{ matrix.libname }}' -type f | head -1) + [ -n "$found" ] || { echo "::error::${{ matrix.libname }} not produced for ${{ matrix.rid }}"; exit 1; } + cp "$found" "$dest/${{ matrix.libname }}" + ls -la "$dest" - - name: Upload libmeshoptimizer.so (Linux) - if: matrix.os == 'ubuntu-latest' + # The exported symbols are the contract the managed binding P/Invokes + # against. Recording them makes a rebuild auditable: comparing this list + # against the previous release's is what tells you the toolchain still + # produces an equivalent library, which byte comparison never will -- + # different compiler, different date, different bytes, same library. + # + # The dumper reads the binary format directly instead of scraping nm and + # dumpbin. Scraping dumpbin's prose reported the DLL's own name and + # fragments of decorated C++ names as exports, which turned a clean + # comparison into two phantom differences to explain away. + - name: Record exported symbols + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p exports + gh api "repos/EvergineTeam/Evergine.Bindings/contents/tools/dump-exports.py?ref=v1" \ + -H 'Accept: application/vnd.github.raw' > dump-exports.py + python -m pip install --quiet pefile pyelftools macholib + lib="staged/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/${{ matrix.rid }}/native/${{ matrix.libname }}" + python dump-exports.py "$lib" meshopt_ > "exports/${{ matrix.rid }}.txt" + count=$(wc -l < "exports/${{ matrix.rid }}.txt") + { + echo "### ${{ matrix.rid }} — $count exported symbols" + echo "" + echo "Built from \`${{ inputs.ref }}\`." + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload native library uses: actions/upload-artifact@v4 with: - name: meshoptimizer-${{ matrix.arch }}-so - path: build/**/*.so + name: natives-${{ matrix.rid }} + path: staged/ + if-no-files-found: error - - name: Upload libmeshoptimizer.dylib (macOS) - if: matrix.os == 'macos-latest' + - name: Upload exported symbols uses: actions/upload-artifact@v4 with: - name: meshoptimizer-${{ matrix.arch }}-dylib - path: build/**/*.dylib + name: exports-${{ matrix.rid }} + path: exports/ + if-no-files-found: error diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Constants.cs b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Constants.cs index d0da917..1d38cfc 100644 --- a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Constants.cs +++ b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Constants.cs @@ -5,6 +5,6 @@ namespace Evergine.Bindings.MeshOptimizer { public static partial class MeshOptimizer { - public const uint VERSION = 1000; + public const uint VERSION = 1020; } } diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Enums.cs b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Enums.cs index fad0dcd..7801157 100644 --- a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Enums.cs +++ b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Enums.cs @@ -10,28 +10,38 @@ namespace Evergine.Bindings.MeshOptimizer /// = K /// < /// = 16) signed X/Y as an output. - /// Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. Z will store 1.0f, W is preserved as is. + /// Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 (K + /// < + /// = 8) or 8 (K + /// < + /// = 16). Z will store 1.0f, W is preserved as is. /// Input data must contain 4 floats for every vector (count*4 total). /// meshopt_encodeFilterQuat encodes unit quaternions with K-bit (4 /// < /// = K /// < /// = 16) component encoding. - /// Each component is stored as an 16-bit integer; stride must be equal to 8. + /// Each component is stored as a 16-bit integer; stride must be equal to 8. /// Input data must contain 4 floats for every quaternion (count*4 total). /// meshopt_encodeFilterExp encodes arbitrary (finite) floating-point data with 8-bit exponent and K-bit integer mantissa (1 /// < /// = K /// < /// = 24). - /// Exponent can be shared between all components of a given vector as defined by stride or all values of a given component; stride must be divisible by 4. + /// Exponent can be shared between all components of a given vector as defined by stride or all values of a given component; stride must be divisible by 4 (and + /// < + /// = 256). /// Input data must contain stride/4 floats for every vector (count*stride/4 total). /// meshopt_encodeFilterColor encodes RGBA color data by converting RGB to YCoCg color space with K-bit (2 /// < /// = K /// < /// = 16) component encoding; A is stored using K-1 bits. - /// Each component is stored as an 8-bit or 16-bit integer; stride must be equal to 4 or 8. + /// Each component is stored as an 8-bit or 16-bit integer; stride must be equal to 4 (K + /// < + /// = 8) or 8 (K + /// < + /// = 16). /// Input data must contain 4 floats for every color (count*4 total). /// public enum EncodeExpMode @@ -94,10 +104,15 @@ public enum SimplifyOptions : uint /// Experimental: Allow collapses across attribute discontinuities, except for vertices that are tagged with meshopt_SimplifyVertex_Protect in vertex_lock. /// Permissive = 32, + + /// + /// Produce more regular triangle sizes and shapes during simplification, at a small cost to geometric and attribute quality. + /// + RegularizeLight = 64, } /// - /// Experimental: Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs + /// Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs /// [Flags] public enum SimplifyVertexOptions : uint @@ -112,6 +127,29 @@ public enum SimplifyVertexOptions : uint /// Protect attribute discontinuity at this vertex; must be used together with meshopt_SimplifyPermissive option. /// Protect = 2, + + /// + /// Increase priority for this vertex, making it more likely that it's preserved during simplification. + /// + Priority = 4, + } + + /// + /// Tangent generation options + /// + [Flags] + public enum TangentOptions : uint + { + + /// + /// Produce tangents compatible with MikkTSpace (same weighting and fallbacks) at the cost of reduced quality. Not recommended unless normal maps are baked. + /// + Compatible = 1, + + /// + /// Experimental: For vertices only connected to degenerate triangles, output zero tangents instead of an arbitrary fallback. + /// + ZeroFallback = 2, } } diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Functions.cs b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Functions.cs index 8aa8592..cc9baed 100644 --- a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Functions.cs +++ b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated/Functions.cs @@ -60,6 +60,29 @@ public static unsafe partial class MeshOptimizer [DllImport("meshoptimizer", EntryPoint = "meshopt_remapIndexBuffer", CallingConvention = CallingConvention.Cdecl)] public static extern void RemapIndexBuffer(uint* destination, uint* indices, nuint index_count, uint* remap); + /// + /// Experimental: Filter out redundant triangles from the index buffer and return the number of remaining indices + /// Triangles are considered redundant if they are degenerate (two vertices have the same vertex key) or duplicate (matching triangle was present earlier). + /// First vertex_size bytes of every vertex are compared for equality; typically vertex_size should be set to the size of the position attribute. + /// Note that duplicate triangles with opposite windings are preserved, as they may be needed for double-sided rendering. + /// destination must contain enough space for the resulting index buffer (index_count elements) + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_filterIndexBuffer", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint FilterIndexBuffer(uint* destination, uint* indices, nuint index_count, void* vertices, nuint vertex_count, nuint vertex_size, nuint vertex_stride); + + /// + /// Experimental: Filter out redundant triangles from the index buffer and return the number of remaining indices + /// Triangles are considered redundant if they are degenerate (two vertices have the same vertex key) or duplicate (matching triangle was present earlier). + /// All bytes in specified streams are compared for equality; streams should include attributes relevant for position transform (e.g. bone influences). + /// Note that duplicate triangles with opposite windings are preserved, as they may be needed for double-sided rendering. + /// destination must contain enough space for the resulting index buffer (index_count elements) + /// stream_count must be + /// < + /// = 16 + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_filterIndexBufferMulti", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint FilterIndexBufferMulti(uint* destination, uint* indices, nuint index_count, nuint vertex_count, Stream* streams, nuint stream_count); + /// /// Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary /// All vertices that are binary equivalent (wrt first vertex_size bytes) map to the first vertex in the original vertex buffer. @@ -217,6 +240,7 @@ public static unsafe partial class MeshOptimizer /// /// Set index encoder format version (defaults to 1) + /// This function is not thread safe and must not be called concurrently with meshopt_encodeIndexBuffer/meshopt_encodeIndexSequence. /// version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.14+) /// [DllImport("meshoptimizer", EntryPoint = "meshopt_encodeIndexVersion", CallingConvention = CallingConvention.Cdecl)] @@ -263,6 +287,49 @@ public static unsafe partial class MeshOptimizer [DllImport("meshoptimizer", EntryPoint = "meshopt_decodeIndexSequence", CallingConvention = CallingConvention.Cdecl)] public static extern int DecodeIndexSequence(void* destination, nuint index_count, nuint index_size, byte* buffer, nuint buffer_size); + /// + /// Meshlet encoder + /// Encodes meshlet data into an array of bytes that is generally smaller and compresses better compared to original. + /// Returns encoded data size on success, 0 on error; the only error condition is if buffer doesn't have enough space + /// This function encodes a single meshlet; when encoding multiple meshlets, additional headers may be necessary to store vertex/triangle count and encoded size. + /// For maximum efficiency the meshlet being encoded should be optimized using meshopt_optimizeMeshletLevel with level 1+ (3 recommended); additionally, vertex reference data should be optimized for locality (fetch). + /// buffer must contain enough space for the encoded meshlet (use meshopt_encodeMeshletBound to compute worst case size) + /// vertices may be NULL, in which case vertex_count must be 0 and only triangle data is encoded + /// vertex_count and triangle_count must be + /// < + /// = 256. + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_encodeMeshlet", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint EncodeMeshlet(byte* buffer, nuint buffer_size, uint* vertices, nuint vertex_count, byte* triangles, nuint triangle_count); + + [DllImport("meshoptimizer", EntryPoint = "meshopt_encodeMeshletBound", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint EncodeMeshletBound(nuint max_vertices, nuint max_triangles); + + /// + /// Meshlet decoder + /// Decodes meshlet data from an array of bytes generated by meshopt_encodeMeshlet + /// Returns 0 if decoding was successful, and an error code otherwise + /// The decoder is safe to use for untrusted input, but it may produce garbage data. + /// vertices must contain enough space for the resulting vertex data, aligned to 4 bytes (align(vertex_count * vertex_size, 4) bytes) + /// vertex_size must be 2 (16-bit vertex references) or 4 (32-bit vertex references) + /// triangles must contain enough space for the resulting triangle data, aligned to 4 bytes (align(triangle_count * triangle_size, 4) bytes) + /// triangle_size must be 3 (8-bit triangle indices) or 4 (32-bit packed triangles, stored as (a) | (b + /// < + /// < + /// 8) | (c + /// < + /// < + /// 16)) + /// vertex_count, triangle_count match those used during encoding exactly; buffer_size must be equal to the encoded size returned by meshopt_encodeMeshlet. + /// vertices may be NULL, in which case vertex_count must be 0 and the meshlet must contain just triangle data + /// When using "raw" decoding (meshopt_decodeMeshletRaw), both vertices and triangles should have available space further aligned to 16 bytes for efficient SIMD decoding. + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_decodeMeshlet", CallingConvention = CallingConvention.Cdecl)] + public static extern int DecodeMeshlet(void* vertices, nuint vertex_count, nuint vertex_size, void* triangles, nuint triangle_count, nuint triangle_size, byte* buffer, nuint buffer_size); + + [DllImport("meshoptimizer", EntryPoint = "meshopt_decodeMeshletRaw", CallingConvention = CallingConvention.Cdecl)] + public static extern int DecodeMeshletRaw(uint* vertices, nuint vertex_count, uint* triangles, nuint triangle_count, byte* buffer, nuint buffer_size); + /// /// Vertex buffer encoder /// Encodes vertex data into an array of bytes that is generally smaller and compresses better compared to original. @@ -298,6 +365,7 @@ public static unsafe partial class MeshOptimizer /// /// Set vertex encoder format version (defaults to 1) + /// This function is not thread safe and must not be called concurrently with meshopt_encodeVertexBuffer/meshopt_encodeVertexBufferLevel. /// version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.23+) /// [DllImport("meshoptimizer", EntryPoint = "meshopt_encodeVertexVersion", CallingConvention = CallingConvention.Cdecl)] @@ -330,7 +398,7 @@ public static unsafe partial class MeshOptimizer /// meshopt_decodeFilterOct decodes octahedral encoding of a unit vector with K-bit signed X/Y as an input; Z must store 1.0f. /// Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. W is preserved as is. /// meshopt_decodeFilterQuat decodes 3-component quaternion encoding with K-bit component encoding and a 2-bit component index indicating which component to reconstruct. - /// Each component is stored as an 16-bit integer; stride must be equal to 8. + /// Each component is stored as a 16-bit integer; stride must be equal to 8. /// meshopt_decodeFilterExp decodes exponential encoding of floating-point data with 8-bit exponent and 24-bit integer mantissa as 2^E*M. /// Each 32-bit component is decoded in isolation; stride must be divisible by 4. /// meshopt_decodeFilterColor decodes RGBA colors from YCoCg (+A) color encoding where RGB is converted to YCoCg space with K-bit component encoding, and A is stored using K-1 bits. @@ -370,9 +438,9 @@ public static unsafe partial class MeshOptimizer /// If the original vertex data isn't needed, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended. /// destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)! /// vertex_positions should have float3 position in the first 12 bytes of each vertex - /// target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + /// target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation (unless absolute error option is used) /// options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default - /// result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification + /// result_error can be NULL; when it's not NULL, it will contain the resulting (relative/absolute) error after simplification /// [DllImport("meshoptimizer", EntryPoint = "meshopt_simplify", CallingConvention = CallingConvention.Cdecl)] public static extern nuint Simplify(uint* destination, uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, nuint target_index_count, float target_error, uint options, float* result_error); @@ -394,10 +462,10 @@ public static unsafe partial class MeshOptimizer /// attribute_count must be /// < /// = 32 - /// vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved - /// target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + /// vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex composed of meshopt_SimplifyVertex_* flags + /// target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation (unless absolute error option is used) /// options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default - /// result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification + /// result_error can be NULL; when it's not NULL, it will contain the resulting (relative/absolute) error after simplification /// [DllImport("meshoptimizer", EntryPoint = "meshopt_simplifyWithAttributes", CallingConvention = CallingConvention.Cdecl)] public static extern nuint SimplifyWithAttributes(uint* destination, uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, float* vertex_attributes, nuint vertex_attributes_stride, float* attribute_weights, nuint attribute_count, byte* vertex_lock, nuint target_index_count, float target_error, uint options, float* result_error); @@ -418,10 +486,10 @@ public static unsafe partial class MeshOptimizer /// attribute_count must be /// < /// = 32 - /// vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved - /// target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + /// vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex composed of meshopt_SimplifyVertex_* flags + /// target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation (unless absolute error option is used) /// options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default - /// result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification + /// result_error can be NULL; when it's not NULL, it will contain the resulting (relative/absolute) error after simplification /// [DllImport("meshoptimizer", EntryPoint = "meshopt_simplifyWithUpdate", CallingConvention = CallingConvention.Cdecl)] public static extern nuint SimplifyWithUpdate(uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, float* vertex_attributes, nuint vertex_attributes_stride, float* attribute_weights, nuint attribute_count, byte* vertex_lock, nuint target_index_count, float target_error, uint options, float* result_error); @@ -617,6 +685,15 @@ public static unsafe partial class MeshOptimizer [DllImport("meshoptimizer", EntryPoint = "meshopt_optimizeMeshlet", CallingConvention = CallingConvention.Cdecl)] public static extern void OptimizeMeshlet(uint* meshlet_vertices, byte* meshlet_triangles, nuint triangle_count, nuint vertex_count); + /// + /// Meshlet optimizer + /// Reorders meshlet vertices and triangles to maximize locality, with higher levels resulting in smaller compressed size at the cost of optimization time. + /// At level 0 the result is equivalent to meshopt_optimizeMeshlet; levels >= 1 may rotate triangle corners to improve compression (which can change provoking vertex and affect OMM data). + /// level should be in the range [0, 9] with 0 equivalent to meshopt_optimizeMeshlet and 9 being the slowest; the sweet spot for compression ratio is around 3 + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_optimizeMeshletLevel", CallingConvention = CallingConvention.Cdecl)] + public static extern void OptimizeMeshletLevel(uint* meshlet_vertices, nuint vertex_count, byte* meshlet_triangles, nuint triangle_count, int level); + /// /// Cluster bounds generator /// Creates bounding volumes that can be used for frustum, backface and occlusion culling. @@ -635,6 +712,7 @@ public static unsafe partial class MeshOptimizer /// Real-Time Rendering 4th Edition, section 19.3). /// vertex_positions should have float3 position in the first 12 bytes of each vertex /// vertex_count should specify the number of vertices in the entire mesh, not cluster or meshlet + /// indices should have at most 256 unique vertex indices /// index_count/3 and triangle_count must not exceed implementation limits ( /// < /// = 512) @@ -654,6 +732,19 @@ public static unsafe partial class MeshOptimizer [DllImport("meshoptimizer", EntryPoint = "meshopt_computeSphereBounds", CallingConvention = CallingConvention.Cdecl)] public static extern Bounds ComputeSphereBounds(float* positions, nuint count, nuint positions_stride, float* radii, nuint radii_stride); + /// + /// Extract meshlet-local vertex and triangle indices from absolute cluster indices. + /// Fills triangles[] and vertices[] such that vertices[triangles[i]] == indices[i], and returns the number of unique vertices. + /// vertices must contain enough space for the resulting references (up to 256 elements) + /// triangles must contain enough space for local indices (index_count elements) + /// indices should have at most 256 unique vertex indices + /// index_count/3 must not exceed implementation limits ( + /// < + /// = 512) + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_extractMeshletIndices", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint ExtractMeshletIndices(uint* vertices, byte* triangles, uint* indices, nuint index_count); + /// /// Cluster partitioner /// Partitions clusters into groups of similar size, prioritizing grouping clusters that share vertices or are close to each other. @@ -697,6 +788,67 @@ public static unsafe partial class MeshOptimizer [DllImport("meshoptimizer", EntryPoint = "meshopt_spatialClusterPoints", CallingConvention = CallingConvention.Cdecl)] public static extern void SpatialClusterPoints(uint* destination, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, nuint cluster_size); + /// + /// Experimental: Opacity micromap generator (measure) + /// Computes a subdivision level for each input triangle, as well as deduplicating the triangles that reference the same UVs to reduce rasterization requests. + /// Returns the number of OMM entries. + /// levels and sources must contain enough space for the worst case output (index_count/3 elements, one per resulting OMM entry) + /// levels[i] will contain the subdivision level for entry i, with the total number of entries returned by the function; each entry should be rasterized from triangle index sources[i] + /// omm_indices must contain enough space for the resulting OMM indices (index_count/3 elements, one per triangle) + /// vertex_uvs should have float2 texture coordinate in the first 8 bytes of each vertex + /// max_level specifies the maximum subdivision level (0..12) + /// target_edge can be 0; when >0, triangle subdivision is adaptive and targets target_edge^2 texel area + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_opacityMapMeasure", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint OpacityMapMeasure(byte* levels, uint* sources, int* omm_indices, uint* indices, nuint index_count, float* vertex_uvs, nuint vertex_count, nuint vertex_uvs_stride, uint texture_width, uint texture_height, int max_level, float target_edge); + + /// + /// Experimental: Opacity micromap generator (rasterize) + /// Rasterizes opacity state for a single triangle entry by sampling the alpha texture, using bilinear filtering and 0.5 alpha cutoff. + /// result should contain enough space for the output opacity data (which can be computed using meshopt_opacityMapEntrySize) + /// level specifies the subdivision level (0..12) + /// states should be 2 for 2-state format (opaque/transparent) and 4 for 4-state format (opaque/transparent/unknown) + /// uv0/uv1/uv2 should refer to a float2 texture coordinate for each triangle corner; note that micromap data is sensitive to the corner order + /// texture_data should point to the alpha channel of the first pixel, encoded as UNORM8 + /// texture_stride specifies the distance in bytes between consecutive pixels, e.g. 4 for RGBA input + /// texture_pitch specifies the distance in bytes between consecutive rows, e.g. 4*texture_width for tightly packed RGBA input + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_opacityMapRasterize", CallingConvention = CallingConvention.Cdecl)] + public static extern void OpacityMapRasterize(byte* result, int level, int states, float* uv0, float* uv1, float* uv2, byte* texture_data, nuint texture_stride, nuint texture_pitch, uint texture_width, uint texture_height); + + [DllImport("meshoptimizer", EntryPoint = "meshopt_opacityMapEntrySize", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint OpacityMapEntrySize(int level, int states); + + /// + /// Experimental: Opacity micromap generator (compact) + /// Compacts and deduplicates opacity data, merging identical micromap entries and replacing micromap states with special indices (-4..-1) when possible. + /// Returns the number of OMM entries after compaction; the data array should be trimmed using the last offset/size. + /// data should contain opacity data for all input/output entries + /// levels should contain subdivision levels for all input/output entries + /// offsets should contain offset into data[] for each entry + /// levels[i] and offsets[i] will be updated with post-compaction level/offset for entry i, with the total number of entries returned by the function + /// omm_indices should contain indices into the original OMM data, and will be updated with a new index or a special index (-4..-1) when possible + /// states should be 2 for 2-state format (opaque/transparent) and 4 for 4-state format (opaque/transparent/unknown) + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_opacityMapCompact", CallingConvention = CallingConvention.Cdecl)] + public static extern nuint OpacityMapCompact(byte* data, nuint data_size, byte* levels, uint* offsets, nuint omm_count, int* omm_indices, nuint triangle_count, int states); + + /// + /// Experimental: Tangent space generator + /// Computes per-corner tangent vectors; for each corner, computes normalized tangent vector (xyz) and orientation (w, +/-1). + /// Bitangent can be reconstructed via cross(normal, tangent.xyz) * tangent.w. + /// To apply tangents to the mesh, either deindex and reindex it with the tangent stream, or copy tangents to existing vertex data while duplicating + /// vertices with different tangent vectors (e.g. on UV mirror seams). + /// Input can be indexed or unindexed (indices=NULL); this does not affect the resulting tangents, but indexed inputs are ~30% faster to process. + /// result must contain enough space for the output tangent data (index_count*4 elements) + /// indices can be NULL if the input is unindexed + /// vertex_positions should have float3 position in the first 12 bytes of each vertex + /// vertex_normals should have unit float3 normal in the first 12 bytes of each vertex + /// vertex_uvs should have float2 texture coordinate in the first 8 bytes of each vertex + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_generateTangents", CallingConvention = CallingConvention.Cdecl)] + public static extern void GenerateTangents(float* result, uint* indices, nuint index_count, float* vertex_positions, nuint vertex_count, nuint vertex_positions_stride, float* vertex_normals, nuint vertex_normals_stride, float* vertex_uvs, nuint vertex_uvs_stride, uint options); + /// /// Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value /// Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest @@ -721,5 +873,21 @@ public static unsafe partial class MeshOptimizer [DllImport("meshoptimizer", EntryPoint = "meshopt_dequantizeHalf", CallingConvention = CallingConvention.Cdecl)] public static extern float DequantizeHalf(ushort h); + /// + /// Experimental: Compute shared exponent suitable for mesh/cluster position quantization + /// Given mesh or cluster bounds, compute a shared exponent that can be used to quantize any position inside the bounds to a 24-bit integer grid. + /// The resulting output can be stored as a compact bit-stream to be decoded directly in shaders, or to be used as an input to RT BVH builders, + /// for example via D3D12_VERTEX_FORMAT_COMPRESSED1 in DXR2 (max_bits=16). + /// To quantize positions, compute: + /// scale = pow(2, exponent) + /// iv = int(round(v / scale)) + /// The resulting integer can be stored as signed 24-bit, or as an unsigned offset from a signed 24-bit anchor value, shared between all positions. + /// minv/maxv specify the axis-aligned bounding box of the mesh or cluster; each should refer to a float3 value + /// min_exp specifies the minimum value for the returned exponent, limiting precision to reduce size; e.g. min_exp = -10 will produce minimum error of 1mm given metric units + /// max_bits specifies the maximum allowed number of bits for the quantized integer range (offset from anchor is an unsigned integer up to 2^max_bits-1) + /// + [DllImport("meshoptimizer", EntryPoint = "meshopt_computePositionExponent", CallingConvention = CallingConvention.Cdecl)] + public static extern int ComputePositionExponent(float* minv, float* maxv, int min_exp, int max_bits); + } } diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-arm64/native/libmeshoptimizer.so b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-arm64/native/libmeshoptimizer.so index b295041..b6454c8 100644 Binary files a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-arm64/native/libmeshoptimizer.so and b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-arm64/native/libmeshoptimizer.so differ diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-x64/native/libmeshoptimizer.so b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-x64/native/libmeshoptimizer.so index a2ff58d..6eea316 100644 Binary files a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-x64/native/libmeshoptimizer.so and b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/linux-x64/native/libmeshoptimizer.so differ diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/osx-arm64/native/libmeshoptimizer.dylib b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/osx-arm64/native/libmeshoptimizer.dylib index ca455a8..dac179c 100644 Binary files a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/osx-arm64/native/libmeshoptimizer.dylib and b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/osx-arm64/native/libmeshoptimizer.dylib differ diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-arm64/native/meshoptimizer.dll b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-arm64/native/meshoptimizer.dll index a4f25da..d7f3732 100644 Binary files a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-arm64/native/meshoptimizer.dll and b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-arm64/native/meshoptimizer.dll differ diff --git a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-x64/native/meshoptimizer.dll b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-x64/native/meshoptimizer.dll index 04bf0d5..5b0ea8e 100644 Binary files a/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-x64/native/meshoptimizer.dll and b/MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/runtimes/win-x64/native/meshoptimizer.dll differ diff --git a/MeshOptimizerGen/MeshOptimizerGen/Headers/meshoptimizer.h b/MeshOptimizerGen/MeshOptimizerGen/Headers/meshoptimizer.h index c9239bc..e6aa58e 100644 --- a/MeshOptimizerGen/MeshOptimizerGen/Headers/meshoptimizer.h +++ b/MeshOptimizerGen/MeshOptimizerGen/Headers/meshoptimizer.h @@ -1,7 +1,7 @@ /** - * meshoptimizer - version 1.0 + * meshoptimizer - version 1.2 * - * Copyright (C) 2016-2025, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) + * Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) * Report bugs and download new versions at https://github.com/zeux/meshoptimizer * * This library is distributed under the MIT License. See notice at the end of this file. @@ -12,7 +12,7 @@ #include /* Version macro; major * 1000 + minor * 10 + patch */ -#define MESHOPTIMIZER_VERSION 1000 /* 1.0 */ +#define MESHOPTIMIZER_VERSION 1020 /* 1.2 */ /* If no API is defined, assume default */ #ifndef MESHOPTIMIZER_API @@ -103,6 +103,27 @@ MESHOPTIMIZER_API void meshopt_remapVertexBuffer(void* destination, const void* */ MESHOPTIMIZER_API void meshopt_remapIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap); +/** + * Experimental: Filter out redundant triangles from the index buffer and return the number of remaining indices + * Triangles are considered redundant if they are degenerate (two vertices have the same vertex key) or duplicate (matching triangle was present earlier). + * First vertex_size bytes of every vertex are compared for equality; typically vertex_size should be set to the size of the position attribute. + * Note that duplicate triangles with opposite windings are preserved, as they may be needed for double-sided rendering. + * + * destination must contain enough space for the resulting index buffer (index_count elements) + */ +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_filterIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride); + +/** + * Experimental: Filter out redundant triangles from the index buffer and return the number of remaining indices + * Triangles are considered redundant if they are degenerate (two vertices have the same vertex key) or duplicate (matching triangle was present earlier). + * All bytes in specified streams are compared for equality; streams should include attributes relevant for position transform (e.g. bone influences). + * Note that duplicate triangles with opposite windings are preserved, as they may be needed for double-sided rendering. + * + * destination must contain enough space for the resulting index buffer (index_count elements) + * stream_count must be <= 16 + */ +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_filterIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count); + /** * Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary * All vertices that are binary equivalent (wrt first vertex_size bytes) map to the first vertex in the original vertex buffer. @@ -252,6 +273,7 @@ MESHOPTIMIZER_API size_t meshopt_encodeIndexBufferBound(size_t index_count, size /** * Set index encoder format version (defaults to 1) + * This function is not thread safe and must not be called concurrently with meshopt_encodeIndexBuffer/meshopt_encodeIndexSequence. * * version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.14+) */ @@ -295,6 +317,38 @@ MESHOPTIMIZER_API size_t meshopt_encodeIndexSequenceBound(size_t index_count, si */ MESHOPTIMIZER_API int meshopt_decodeIndexSequence(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size); +/** + * Meshlet encoder + * Encodes meshlet data into an array of bytes that is generally smaller and compresses better compared to original. + * Returns encoded data size on success, 0 on error; the only error condition is if buffer doesn't have enough space + * This function encodes a single meshlet; when encoding multiple meshlets, additional headers may be necessary to store vertex/triangle count and encoded size. + * For maximum efficiency the meshlet being encoded should be optimized using meshopt_optimizeMeshletLevel with level 1+ (3 recommended); additionally, vertex reference data should be optimized for locality (fetch). + * + * buffer must contain enough space for the encoded meshlet (use meshopt_encodeMeshletBound to compute worst case size) + * vertices may be NULL, in which case vertex_count must be 0 and only triangle data is encoded + * vertex_count and triangle_count must be <= 256. + */ +MESHOPTIMIZER_API size_t meshopt_encodeMeshlet(unsigned char* buffer, size_t buffer_size, const unsigned int* vertices, size_t vertex_count, const unsigned char* triangles, size_t triangle_count); +MESHOPTIMIZER_API size_t meshopt_encodeMeshletBound(size_t max_vertices, size_t max_triangles); + +/** + * Meshlet decoder + * Decodes meshlet data from an array of bytes generated by meshopt_encodeMeshlet + * Returns 0 if decoding was successful, and an error code otherwise + * The decoder is safe to use for untrusted input, but it may produce garbage data. + * + * vertices must contain enough space for the resulting vertex data, aligned to 4 bytes (align(vertex_count * vertex_size, 4) bytes) + * vertex_size must be 2 (16-bit vertex references) or 4 (32-bit vertex references) + * triangles must contain enough space for the resulting triangle data, aligned to 4 bytes (align(triangle_count * triangle_size, 4) bytes) + * triangle_size must be 3 (8-bit triangle indices) or 4 (32-bit packed triangles, stored as (a) | (b << 8) | (c << 16)) + * vertex_count, triangle_count match those used during encoding exactly; buffer_size must be equal to the encoded size returned by meshopt_encodeMeshlet. + * vertices may be NULL, in which case vertex_count must be 0 and the meshlet must contain just triangle data + * + * When using "raw" decoding (meshopt_decodeMeshletRaw), both vertices and triangles should have available space further aligned to 16 bytes for efficient SIMD decoding. + */ +MESHOPTIMIZER_API int meshopt_decodeMeshlet(void* vertices, size_t vertex_count, size_t vertex_size, void* triangles, size_t triangle_count, size_t triangle_size, const unsigned char* buffer, size_t buffer_size); +MESHOPTIMIZER_API int meshopt_decodeMeshletRaw(unsigned int* vertices, size_t vertex_count, unsigned int* triangles, size_t triangle_count, const unsigned char* buffer, size_t buffer_size); + /** * Vertex buffer encoder * Encodes vertex data into an array of bytes that is generally smaller and compresses better compared to original. @@ -324,6 +378,7 @@ MESHOPTIMIZER_API size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, /** * Set vertex encoder format version (defaults to 1) + * This function is not thread safe and must not be called concurrently with meshopt_encodeVertexBuffer/meshopt_encodeVertexBufferLevel. * * version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.23+) */ @@ -355,7 +410,7 @@ MESHOPTIMIZER_API int meshopt_decodeVertexVersion(const unsigned char* buffer, s * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. W is preserved as is. * * meshopt_decodeFilterQuat decodes 3-component quaternion encoding with K-bit component encoding and a 2-bit component index indicating which component to reconstruct. - * Each component is stored as an 16-bit integer; stride must be equal to 8. + * Each component is stored as a 16-bit integer; stride must be equal to 8. * * meshopt_decodeFilterExp decodes exponential encoding of floating-point data with 8-bit exponent and 24-bit integer mantissa as 2^E*M. * Each 32-bit component is decoded in isolation; stride must be divisible by 4. @@ -373,19 +428,19 @@ MESHOPTIMIZER_API void meshopt_decodeFilterColor(void* buffer, size_t count, siz * These functions can be used to encode data in a format that meshopt_decodeFilter can decode * * meshopt_encodeFilterOct encodes unit vectors with K-bit (2 <= K <= 16) signed X/Y as an output. - * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. Z will store 1.0f, W is preserved as is. + * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 (K <= 8) or 8 (K <= 16). Z will store 1.0f, W is preserved as is. * Input data must contain 4 floats for every vector (count*4 total). * * meshopt_encodeFilterQuat encodes unit quaternions with K-bit (4 <= K <= 16) component encoding. - * Each component is stored as an 16-bit integer; stride must be equal to 8. + * Each component is stored as a 16-bit integer; stride must be equal to 8. * Input data must contain 4 floats for every quaternion (count*4 total). * * meshopt_encodeFilterExp encodes arbitrary (finite) floating-point data with 8-bit exponent and K-bit integer mantissa (1 <= K <= 24). - * Exponent can be shared between all components of a given vector as defined by stride or all values of a given component; stride must be divisible by 4. + * Exponent can be shared between all components of a given vector as defined by stride or all values of a given component; stride must be divisible by 4 (and <= 256). * Input data must contain stride/4 floats for every vector (count*stride/4 total). * * meshopt_encodeFilterColor encodes RGBA color data by converting RGB to YCoCg color space with K-bit (2 <= K <= 16) component encoding; A is stored using K-1 bits. - * Each component is stored as an 8-bit or 16-bit integer; stride must be equal to 4 or 8. + * Each component is stored as an 8-bit or 16-bit integer; stride must be equal to 4 (K <= 8) or 8 (K <= 16). * Input data must contain 4 floats for every color (count*4 total). */ enum meshopt_EncodeExpMode @@ -422,10 +477,12 @@ enum meshopt_SimplifyRegularize = 1 << 4, /* Experimental: Allow collapses across attribute discontinuities, except for vertices that are tagged with meshopt_SimplifyVertex_Protect in vertex_lock. */ meshopt_SimplifyPermissive = 1 << 5, + /* Produce more regular triangle sizes and shapes during simplification, at a small cost to geometric and attribute quality. */ + meshopt_SimplifyRegularizeLight = 1 << 6, }; /** - * Experimental: Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs + * Simplification vertex flags/locks, for use in `vertex_lock` arrays in simplification APIs */ enum { @@ -433,6 +490,8 @@ enum meshopt_SimplifyVertex_Lock = 1 << 0, /* Protect attribute discontinuity at this vertex; must be used together with meshopt_SimplifyPermissive option. */ meshopt_SimplifyVertex_Protect = 1 << 1, + /* Increase priority for this vertex, making it more likely that it's preserved during simplification. */ + meshopt_SimplifyVertex_Priority = 1 << 2, }; /** @@ -447,9 +506,9 @@ enum * * destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)! * vertex_positions should have float3 position in the first 12 bytes of each vertex - * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation (unless absolute error option is used) * options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default - * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification + * result_error can be NULL; when it's not NULL, it will contain the resulting (relative/absolute) error after simplification */ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error); @@ -470,10 +529,10 @@ MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsig * vertex_attributes should have attribute_count floats for each vertex * attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position * attribute_count must be <= 32 - * vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved - * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + * vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex composed of meshopt_SimplifyVertex_* flags + * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation (unless absolute error option is used) * options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default - * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification + * result_error can be NULL; when it's not NULL, it will contain the resulting (relative/absolute) error after simplification */ MESHOPTIMIZER_API size_t meshopt_simplifyWithAttributes(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error); @@ -493,10 +552,10 @@ MESHOPTIMIZER_API size_t meshopt_simplifyWithAttributes(unsigned int* destinatio * vertex_attributes should have attribute_count floats for each vertex * attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position * attribute_count must be <= 32 - * vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex; 1 denotes vertices that can't be moved - * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + * vertex_lock can be NULL; when it's not NULL, it should have a value for each vertex composed of meshopt_SimplifyVertex_* flags + * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation (unless absolute error option is used) * options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default - * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification + * result_error can be NULL; when it's not NULL, it will contain the resulting (relative/absolute) error after simplification */ MESHOPTIMIZER_API size_t meshopt_simplifyWithUpdate(unsigned int* indices, size_t index_count, float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, const unsigned char* vertex_lock, size_t target_index_count, float target_error, unsigned int options, float* result_error); @@ -706,6 +765,15 @@ MESHOPTIMIZER_API size_t meshopt_buildMeshletsSpatial(struct meshopt_Meshlet* me */ MESHOPTIMIZER_API void meshopt_optimizeMeshlet(unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t triangle_count, size_t vertex_count); +/** + * Meshlet optimizer + * Reorders meshlet vertices and triangles to maximize locality, with higher levels resulting in smaller compressed size at the cost of optimization time. + * At level 0 the result is equivalent to meshopt_optimizeMeshlet; levels >= 1 may rotate triangle corners to improve compression (which can change provoking vertex and affect OMM data). + * + * level should be in the range [0, 9] with 0 equivalent to meshopt_optimizeMeshlet and 9 being the slowest; the sweet spot for compression ratio is around 3 + */ +MESHOPTIMIZER_API void meshopt_optimizeMeshletLevel(unsigned int* meshlet_vertices, size_t vertex_count, unsigned char* meshlet_triangles, size_t triangle_count, int level); + struct meshopt_Bounds { /* bounding sphere, useful for frustum and occlusion culling */ @@ -743,6 +811,7 @@ struct meshopt_Bounds * * vertex_positions should have float3 position in the first 12 bytes of each vertex * vertex_count should specify the number of vertices in the entire mesh, not cluster or meshlet + * indices should have at most 256 unique vertex indices * index_count/3 and triangle_count must not exceed implementation limits (<= 512) */ MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); @@ -757,6 +826,17 @@ MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeMeshletBounds(const unsig */ MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeSphereBounds(const float* positions, size_t count, size_t positions_stride, const float* radii, size_t radii_stride); +/** + * Extract meshlet-local vertex and triangle indices from absolute cluster indices. + * Fills triangles[] and vertices[] such that vertices[triangles[i]] == indices[i], and returns the number of unique vertices. + * + * vertices must contain enough space for the resulting references (up to 256 elements) + * triangles must contain enough space for local indices (index_count elements) + * indices should have at most 256 unique vertex indices + * index_count/3 must not exceed implementation limits (<= 512) + */ +MESHOPTIMIZER_API size_t meshopt_extractMeshletIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count); + /** * Cluster partitioner * Partitions clusters into groups of similar size, prioritizing grouping clusters that share vertices or are close to each other. @@ -800,6 +880,76 @@ MESHOPTIMIZER_API void meshopt_spatialSortTriangles(unsigned int* destination, c */ MESHOPTIMIZER_API void meshopt_spatialClusterPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t cluster_size); +/** + * Experimental: Opacity micromap generator (measure) + * Computes a subdivision level for each input triangle, as well as deduplicating the triangles that reference the same UVs to reduce rasterization requests. + * Returns the number of OMM entries. + * + * levels and sources must contain enough space for the worst case output (index_count/3 elements, one per resulting OMM entry) + * levels[i] will contain the subdivision level for entry i, with the total number of entries returned by the function; each entry should be rasterized from triangle index sources[i] + * omm_indices must contain enough space for the resulting OMM indices (index_count/3 elements, one per triangle) + * vertex_uvs should have float2 texture coordinate in the first 8 bytes of each vertex + * max_level specifies the maximum subdivision level (0..12) + * target_edge can be 0; when >0, triangle subdivision is adaptive and targets target_edge^2 texel area + */ +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapMeasure(unsigned char* levels, unsigned int* sources, int* omm_indices, const unsigned int* indices, size_t index_count, const float* vertex_uvs, size_t vertex_count, size_t vertex_uvs_stride, unsigned int texture_width, unsigned int texture_height, int max_level, float target_edge); + +/** + * Experimental: Opacity micromap generator (rasterize) + * Rasterizes opacity state for a single triangle entry by sampling the alpha texture, using bilinear filtering and 0.5 alpha cutoff. + * + * result should contain enough space for the output opacity data (which can be computed using meshopt_opacityMapEntrySize) + * level specifies the subdivision level (0..12) + * states should be 2 for 2-state format (opaque/transparent) and 4 for 4-state format (opaque/transparent/unknown) + * uv0/uv1/uv2 should refer to a float2 texture coordinate for each triangle corner; note that micromap data is sensitive to the corner order + * texture_data should point to the alpha channel of the first pixel, encoded as UNORM8 + * texture_stride specifies the distance in bytes between consecutive pixels, e.g. 4 for RGBA input + * texture_pitch specifies the distance in bytes between consecutive rows, e.g. 4*texture_width for tightly packed RGBA input + */ +MESHOPTIMIZER_EXPERIMENTAL void meshopt_opacityMapRasterize(unsigned char* result, int level, int states, const float* uv0, const float* uv1, const float* uv2, const unsigned char* texture_data, size_t texture_stride, size_t texture_pitch, unsigned int texture_width, unsigned int texture_height); +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapEntrySize(int level, int states); + +/** + * Experimental: Opacity micromap generator (compact) + * Compacts and deduplicates opacity data, merging identical micromap entries and replacing micromap states with special indices (-4..-1) when possible. + * Returns the number of OMM entries after compaction; the data array should be trimmed using the last offset/size. + * + * data should contain opacity data for all input/output entries + * levels should contain subdivision levels for all input/output entries + * offsets should contain offset into data[] for each entry + * levels[i] and offsets[i] will be updated with post-compaction level/offset for entry i, with the total number of entries returned by the function + * omm_indices should contain indices into the original OMM data, and will be updated with a new index or a special index (-4..-1) when possible + * states should be 2 for 2-state format (opaque/transparent) and 4 for 4-state format (opaque/transparent/unknown) + */ +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_opacityMapCompact(unsigned char* data, size_t data_size, unsigned char* levels, unsigned int* offsets, size_t omm_count, int* omm_indices, size_t triangle_count, int states); + +/** + * Tangent generation options + */ +enum +{ + /* Produce tangents compatible with MikkTSpace (same weighting and fallbacks) at the cost of reduced quality. Not recommended unless normal maps are baked. */ + meshopt_TangentCompatible = 1 << 0, + /* Experimental: For vertices only connected to degenerate triangles, output zero tangents instead of an arbitrary fallback. */ + meshopt_TangentZeroFallback = 1 << 1, +}; + +/** + * Experimental: Tangent space generator + * Computes per-corner tangent vectors; for each corner, computes normalized tangent vector (xyz) and orientation (w, +/-1). + * Bitangent can be reconstructed via cross(normal, tangent.xyz) * tangent.w. + * To apply tangents to the mesh, either deindex and reindex it with the tangent stream, or copy tangents to existing vertex data while duplicating + * vertices with different tangent vectors (e.g. on UV mirror seams). + * Input can be indexed or unindexed (indices=NULL); this does not affect the resulting tangents, but indexed inputs are ~30% faster to process. + * + * result must contain enough space for the output tangent data (index_count*4 elements) + * indices can be NULL if the input is unindexed + * vertex_positions should have float3 position in the first 12 bytes of each vertex + * vertex_normals should have unit float3 normal in the first 12 bytes of each vertex + * vertex_uvs should have float2 texture coordinate in the first 8 bytes of each vertex + */ +MESHOPTIMIZER_EXPERIMENTAL void meshopt_generateTangents(float* result, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride, unsigned int options); + /** * Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest @@ -821,11 +971,31 @@ MESHOPTIMIZER_API float meshopt_quantizeFloat(float v, int N); */ MESHOPTIMIZER_API float meshopt_dequantizeHalf(unsigned short h); +/** + * Experimental: Compute shared exponent suitable for mesh/cluster position quantization + * Given mesh or cluster bounds, compute a shared exponent that can be used to quantize any position inside the bounds to a 24-bit integer grid. + * The resulting output can be stored as a compact bit-stream to be decoded directly in shaders, or to be used as an input to RT BVH builders, + * for example via D3D12_VERTEX_FORMAT_COMPRESSED1 in DXR2 (max_bits=16). + * + * To quantize positions, compute: + * scale = pow(2, exponent) + * iv = int(round(v / scale)) + * The resulting integer can be stored as signed 24-bit, or as an unsigned offset from a signed 24-bit anchor value, shared between all positions. + * + * minv/maxv specify the axis-aligned bounding box of the mesh or cluster; each should refer to a float3 value + * min_exp specifies the minimum value for the returned exponent, limiting precision to reduce size; e.g. min_exp = -10 will produce minimum error of 1mm given metric units + * max_bits specifies the maximum allowed number of bits for the quantized integer range (offset from anchor is an unsigned integer up to 2^max_bits-1) + */ +MESHOPTIMIZER_EXPERIMENTAL int meshopt_computePositionExponent(const float* minv, const float* maxv, int min_exp, int max_bits); + /** * Set allocation callbacks * These callbacks will be used instead of the default operator new/operator delete for all temporary allocations in the library. * Note that all algorithms only allocate memory for temporary use. * allocate/deallocate are always called in a stack-like order - last pointer to be allocated is deallocated first. + * This function is not thread safe and must not be called concurrently with any other meshopt_ function. + * + * In shared library builds, allocations from templated index wrappers in this header will only be redirected if MESHOPTIMIZER_ALLOC_EXPORT is defined. */ MESHOPTIMIZER_API void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*)); @@ -870,6 +1040,10 @@ inline size_t meshopt_generateVertexRemapCustom(unsigned int* destination, const template inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t index_count, const unsigned int* remap); template +inline size_t meshopt_filterIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride); +template +inline size_t meshopt_filterIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count); +template inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride); template inline void meshopt_generateShadowIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const meshopt_Stream* streams, size_t stream_count); @@ -899,6 +1073,8 @@ template inline size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const T* indices, size_t index_count); template inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const unsigned char* buffer, size_t buffer_size); +template +inline int meshopt_decodeMeshlet(V* vertices, size_t vertex_count, T* triangles, size_t triangle_count, const unsigned char* buffer, size_t buffer_size); inline size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level); template inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL); @@ -938,6 +1114,8 @@ template inline size_t meshopt_partitionClusters(unsigned int* destination, const T* cluster_indices, size_t total_index_count, const unsigned int* cluster_index_counts, size_t cluster_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_partition_size); template inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); +template +inline void meshopt_generateTangents(float* result, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride, unsigned int options = 0); #endif /* Inline implementation */ @@ -1115,11 +1293,29 @@ template inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t index_count, const unsigned int* remap) { meshopt_IndexAdapter in(NULL, indices, indices ? index_count : 0); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_remapIndexBuffer(out.data, indices ? in.data : NULL, index_count, remap); } +template +inline size_t meshopt_filterIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride) +{ + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); + + return meshopt_filterIndexBuffer(out.data, in.data, index_count, vertices, vertex_count, vertex_size, vertex_stride); +} + +template +inline size_t meshopt_filterIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count) +{ + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); + + return meshopt_filterIndexBufferMulti(out.data, in.data, index_count, vertex_count, streams, stream_count); +} + template inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride) { @@ -1163,7 +1359,7 @@ inline size_t meshopt_generateProvokingIndexBuffer(T* destination, unsigned int* meshopt_IndexAdapter out(destination, NULL, index_count); size_t bound = vertex_count + (index_count / 3); - assert(size_t(T(bound - 1)) == bound - 1); // bound - 1 must fit in T + assert(bound == 0 || size_t(T(bound - 1)) == bound - 1); // bound - 1 must fit in T (void)bound; return meshopt_generateProvokingIndexBuffer(out.data, reorder, in.data, index_count, vertex_count); @@ -1255,6 +1451,15 @@ inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const return meshopt_decodeIndexSequence(destination, index_count, sizeof(T), buffer, buffer_size); } +template +inline int meshopt_decodeMeshlet(V* vertices, size_t vertex_count, T* triangles, size_t triangle_count, const unsigned char* buffer, size_t buffer_size) +{ + char types_valid[(sizeof(V) == 2 || sizeof(V) == 4) && (sizeof(T) == 1 || sizeof(T) == 4) ? 1 : -1]; + (void)types_valid; + + return meshopt_decodeMeshlet(vertices, vertex_count, sizeof(V), triangles, triangle_count, sizeof(T) == 1 ? 3 : 4, buffer, buffer_size); +} + inline size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level) { return meshopt_encodeVertexBufferLevel(buffer, buffer_size, vertices, vertex_count, vertex_size, level, -1); @@ -1326,7 +1531,7 @@ template inline size_t meshopt_unstripify(T* destination, const T* indices, size_t index_count, T restart_index) { meshopt_IndexAdapter in(NULL, indices, index_count); - meshopt_IndexAdapter out(destination, NULL, (index_count - 2) * 3); + meshopt_IndexAdapter out(destination, NULL, index_count == 0 ? 0 : (index_count - 2) * 3); return meshopt_unstripify(out.data, in.data, index_count, unsigned(restart_index)); } @@ -1419,10 +1624,18 @@ inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_ meshopt_spatialSortTriangles(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride); } + +template +inline void meshopt_generateTangents(float* result, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_normals, size_t vertex_normals_stride, const float* vertex_uvs, size_t vertex_uvs_stride, unsigned int options) +{ + meshopt_IndexAdapter in(NULL, indices, indices ? index_count : 0); + + meshopt_generateTangents(result, indices ? in.data : NULL, index_count, vertex_positions, vertex_count, vertex_positions_stride, vertex_normals, vertex_normals_stride, vertex_uvs, vertex_uvs_stride, options); +} #endif /** - * Copyright (c) 2016-2025 Arseny Kapoulkine + * Copyright (c) 2016-2026 Arseny Kapoulkine * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation diff --git a/MeshOptimizerGen/MeshOptimizerGen/Program.cs b/MeshOptimizerGen/MeshOptimizerGen/Program.cs index 8af48fe..c3f51aa 100644 --- a/MeshOptimizerGen/MeshOptimizerGen/Program.cs +++ b/MeshOptimizerGen/MeshOptimizerGen/Program.cs @@ -1,13 +1,12 @@ -using CppAst; +using CppAst; using System; -using System.Diagnostics; using System.IO; namespace MeshOptimizerGen { class Program { - static void Main(string[] args) + static int Main(string[] args) { var headerFile = Path.Combine(AppContext.BaseDirectory, "Headers", "meshoptimizer.h"); var options = new CppParserOptions @@ -17,28 +16,61 @@ static void Main(string[] args) var compilation = CppParser.ParseFile(headerFile, options); - // Print diagnostic messages + // Parse failures used to go to Debug.WriteLine and the process still + // exited 0. In a Release build Debug.WriteLine compiles to nothing, so + // a header this generator could not read produced no output, no + // message and no error -- CD went green and shipped the previous + // bindings against new native libraries. if (compilation.HasErrors) { + Console.Error.WriteLine($"::error::failed to parse {headerFile}"); foreach (var message in compilation.Diagnostics.Messages) - { - Debug.WriteLine(message); - } + Console.Error.WriteLine($" {message}"); + return 1; } - else - { - string outputPath = Path.Combine( - AppContext.BaseDirectory, - "..", "..", "..", "..", "..", - "Evergine.Bindings.MeshOptimizer", "Generated"); - outputPath = Path.GetFullPath(outputPath); + string outputPath = ResolveOutputPath(); + Directory.CreateDirectory(outputPath); + + Console.WriteLine($"Header : {headerFile}"); + Console.WriteLine($"Output : {outputPath}"); + + CsCodeGenerator.Instance.Generate(compilation, outputPath); + + Console.WriteLine($"Generated {Directory.GetFiles(outputPath, "*.cs").Length} file(s)."); + return 0; + } - if (!Directory.Exists(outputPath)) - Directory.CreateDirectory(outputPath); + /// + /// Locate Evergine.Bindings.MeshOptimizer/Generated by walking up from the + /// executable. + /// + /// + /// This used to be a fixed climb of five directories, which is only correct + /// when the generator is published without a runtime identifier. CD passes + /// one, adding a level, so every run wrote its output a directory below the + /// project and nobody noticed: the files landed somewhere no build reads, + /// the checked-in bindings stayed as a human last generated them, and the + /// step reported success. + /// + /// Searching for the project by name instead means the depth of the build + /// output stops being something this file has to know. + /// + static string ResolveOutputPath() + { + const string Project = "Evergine.Bindings.MeshOptimizer"; - CsCodeGenerator.Instance.Generate(compilation, outputPath); + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, Project); + if (Directory.Exists(candidate)) + return Path.Combine(candidate, "Generated"); + dir = dir.Parent; } + + throw new DirectoryNotFoundException( + $"could not find {Project} above {AppContext.BaseDirectory}"); } } } diff --git a/README.md b/README.md index d5c03f4..67a8d24 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains low-level bindings for MeshOptimizer used in Evergine. This binding is generated from the MeshOptimizer release: -[https://github.com/zeux/meshoptimizer/releases/tag/v1.0](https://github.com/zeux/meshoptimizer/releases/tag/v1.0) +[https://github.com/zeux/meshoptimizer/releases/tag/v1.2](https://github.com/zeux/meshoptimizer/releases/tag/v1.2) [![CI](https://github.com/EvergineTeam/Meshoptimizer.NET/actions/workflows/CI.yml/badge.svg)](https://github.com/EvergineTeam/Meshoptimizer.NET/actions/workflows/CI.yml) [![CD](https://github.com/EvergineTeam/Meshoptimizer.NET/actions/workflows/CD.yml/badge.svg)](https://github.com/EvergineTeam/Meshoptimizer.NET/actions/workflows/CD.yml) diff --git a/binding.yml b/binding.yml index 3a8a585..6a143a1 100644 --- a/binding.yml +++ b/binding.yml @@ -13,9 +13,16 @@ upstream: language: cpp project: https://github.com/zeux/meshoptimizer version-from: git-release + # This binding follows tagged releases, not a branch. It ships native binaries, + # and the header and those binaries have to come from the same revision -- a + # branch would move the header underneath libraries built from something else. + # `current` is rewritten by binding-fetch-upstream when it brings in a newer + # release, which makes this the one place the version lives. + release: + track: stable + current: v1.2 sources: - repo: zeux/meshoptimizer - ref: master remote-path: src/meshoptimizer.h path: MeshOptimizerGen/MeshOptimizerGen/Headers/meshoptimizer.h format: c-header @@ -27,7 +34,11 @@ generator: - MeshOptimizerGen/Evergine.Bindings.MeshOptimizer/Generated # NOTE — this package ships native binaries under Evergine.Bindings.MeshOptimizer/runtimes. -# The header and those binaries must come from the same upstream revision. `ref: master` -# is a moving target, so a header bump is only valid together with a native rebuild; -# binding-updater must label any such pull request `needs-human-review`. Pinning `ref` -# to a release tag would remove this hazard and is worth considering. +# The header and those binaries must come from the same upstream revision, so a header +# bump is only valid together with a native rebuild. That rebuild is not optional and it +# is not something CI would catch if it were skipped: P/Invoke binds late, so a mismatched +# package compiles, passes CI, publishes, and then throws EntryPointNotFoundException in +# the consumer's application. +# +# The CD keeps the two in step: `meshopt-cmake.yml` rebuilds all five platforms at the +# resolved release before the header is committed. Do not bump the header by hand.