From f8dc4f830adabdcded206a23b4d99d58e498bbfa Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 03:19:24 -0400 Subject: [PATCH 1/7] Scratch that is given back last first comes from a thread's own arena ALScratchArena gives each thread a 4 MB region, made the first time the thread takes from it, and hands out 16-byte aligned blocks from its top; giving a block back moves the top down to it. A block that does not fit what is left comes from operator new and goes back there. Nothing is locked, since a thread touches only its own arena. Scope takes up to 16 blocks and gives them back as it ends, and counters say what the arena served and the most of it in use at once. meshoptimizer frees its scratch in exactly that order, and the size is set by LLVolumeFace::cacheOptimize, its largest user: about 35 bytes an index and 36 a vertex, so faces up to about 32,000 triangles stay off the heap. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/CMakeLists.txt | 3 + indra/llcommon/alscratcharena.cpp | 131 ++++++++++++++ indra/llcommon/alscratcharena.h | 104 +++++++++++ indra/llcommon/tests/alscratcharena_test.cpp | 172 +++++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 indra/llcommon/alscratcharena.cpp create mode 100644 indra/llcommon/alscratcharena.h create mode 100644 indra/llcommon/tests/alscratcharena_test.cpp diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index ed355dd282..e0cc0c5b23 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -25,6 +25,7 @@ target_sources( alregex.cpp alsanitizer.cpp alsavehistory.cpp + alscratcharena.cpp alwatchedfile.cpp apply.cpp commoncontrol.cpp @@ -131,6 +132,7 @@ target_sources( alregex.h alsanitizer.h alsavehistory.h + alscratcharena.h alsimd.h alwatchedfile.h always_return.h @@ -310,6 +312,7 @@ if(AL_BUILD_TESTS) tests/alrecoverystore_test.cpp tests/alregex_test.cpp tests/alsavehistory_test.cpp + tests/alscratcharena_test.cpp tests/alserialworker_test.cpp tests/alsimd_test.cpp tests/alwatchedfile_test.cpp diff --git a/indra/llcommon/alscratcharena.cpp b/indra/llcommon/alscratcharena.cpp new file mode 100644 index 0000000000..01b5322376 --- /dev/null +++ b/indra/llcommon/alscratcharena.cpp @@ -0,0 +1,131 @@ +/** + * @file alscratcharena.cpp + * @brief Each thread's scratch memory, given back in the reverse of the + * order it was taken + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "alscratcharena.h" + +#include "llmemory.h" + +#include + +namespace +{ + // Each arena block sits after a header of this size holding the block's + // size, by which its return is checked to be the last one taken. + constexpr size_t HEADER = ALScratchArena::ALIGNMENT; + + struct Arena + { + U8* mBase = nullptr; + size_t mTop = 0; + // The arena could not be made: every block comes from the heap. + bool mUnavailable = false; + ALScratchArena::Counters mCounters; + + ~Arena() + { + ll_aligned_free<64>(mBase); + } + }; + + thread_local Arena tArena; +} // namespace + +//static +void* ALScratchArena::allocate(size_t bytes) +{ + Arena& arena = tArena; + if (!arena.mBase && !arena.mUnavailable) + { + arena.mBase = static_cast(ll_aligned_malloc<64>(CAPACITY)); + arena.mUnavailable = !arena.mBase; + } + + // Compared before it is rounded, so that a size near the top of size_t, + // as meshoptimizer asks for when a count overflows, goes to the heap. + const size_t room = CAPACITY - arena.mTop; + if (arena.mBase && room > HEADER && bytes <= room - HEADER) + { + const size_t size = (bytes + ALIGNMENT - 1) & ~(ALIGNMENT - 1); + if (size <= room - HEADER) + { + U8* block = arena.mBase + arena.mTop + HEADER; + *reinterpret_cast(block - HEADER) = size; + arena.mTop += HEADER + size; + ++arena.mCounters.arena_blocks; + arena.mCounters.peak = llmax(arena.mCounters.peak, arena.mTop); + return block; + } + } + + ++arena.mCounters.heap_blocks; + arena.mCounters.heap_bytes += bytes; + return ::operator new(bytes); +} + +//static +void ALScratchArena::deallocate(void* block) +{ + Arena& arena = tArena; + U8* p = static_cast(block); + if (arena.mBase && p >= arena.mBase && p < arena.mBase + CAPACITY) + { + llassert(p + *reinterpret_cast(p - HEADER) == arena.mBase + arena.mTop); + arena.mTop = static_cast(p - HEADER - arena.mBase); + return; + } + ::operator delete(block); +} + +//static +ALScratchArena::Counters ALScratchArena::counters() +{ + return tArena.mCounters; +} + +//static +void ALScratchArena::resetCounters() +{ + Arena& arena = tArena; + arena.mCounters = Counters(); + arena.mCounters.peak = arena.mTop; +} + +ALScratchArena::Scope::~Scope() +{ + while (mCount > 0) + { + ALScratchArena::deallocate(mBlocks[--mCount]); + } +} + +void* ALScratchArena::Scope::take(size_t bytes) +{ + llassert_always(mCount < MAX_BLOCKS); + void* block = ALScratchArena::allocate(bytes); + mBlocks[mCount++] = block; + return block; +} diff --git a/indra/llcommon/alscratcharena.h b/indra/llcommon/alscratcharena.h new file mode 100644 index 0000000000..467db078c6 --- /dev/null +++ b/indra/llcommon/alscratcharena.h @@ -0,0 +1,104 @@ +/** + * @file alscratcharena.h + * @brief Each thread's scratch memory, given back in the reverse of the + * order it was taken + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#ifndef AL_SCRATCHARENA_H +#define AL_SCRATCHARENA_H + +#include "llpreprocessor.h" +#include "stdtypes.h" + +#include + +// Scratch memory for work that gives its blocks back last first, as +// meshoptimizer does. Each thread has its own arena of CAPACITY bytes, +// made the first time the thread takes a block from it: a block is taken +// from the top, and giving it back moves the top down to it. A block that +// does not fit what is left comes from the process heap through operator +// new, and goes back there. A thread touches only its own arena, so +// nothing is locked. +// +// The order is the caller's to keep: a block goes back before every block +// taken after it has. Nothing holding a block may yield to a coroutine +// that takes from the same thread's arena. +class LL_COMMON_API ALScratchArena +{ +public: + // Bytes in each thread's arena. LLVolumeFace::cacheOptimize, the + // largest user, peaks at about 35 bytes an index and 36 a vertex: 2 MB + // for a mesh LOD face of 8,385 vertices and 16,128 triangles. Twice + // that leaves faces up to about 32,000 triangles off the heap; larger + // ones take their largest blocks from it. Only the pages a thread has + // used are touched. + static constexpr size_t CAPACITY = 4 * 1024 * 1024; + + // Every block starts on a multiple of this, as operator new's do. + static constexpr size_t ALIGNMENT = 16; + + // A block of at least bytes from the calling thread's arena, or from + // the heap where the arena has no room for it. + static void* allocate(size_t bytes); + + // Gives back a block allocate took on this thread. + static void deallocate(void* block); + + // What the calling thread's arena has served since the last reset. + struct Counters + { + size_t arena_blocks = 0; // blocks taken from the arena + size_t heap_blocks = 0; // blocks taken from the heap + size_t heap_bytes = 0; // bytes those asked for + size_t peak = 0; // the most of the arena in use at once + }; + + static Counters counters(); + static void resetCounters(); + + // Blocks taken for a scope, given back last first as it ends. + class Scope + { + public: + Scope() = default; + ~Scope(); + + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; + + // Room for count values of T, not initialised. + template + T* allocate(size_t count) + { + return static_cast(take(count * sizeof(T))); + } + + private: + void* take(size_t bytes); + + static constexpr size_t MAX_BLOCKS = 16; + void* mBlocks[MAX_BLOCKS]; + size_t mCount = 0; + }; +}; + +#endif // AL_SCRATCHARENA_H diff --git a/indra/llcommon/tests/alscratcharena_test.cpp b/indra/llcommon/tests/alscratcharena_test.cpp new file mode 100644 index 0000000000..7ffa0f7710 --- /dev/null +++ b/indra/llcommon/tests/alscratcharena_test.cpp @@ -0,0 +1,172 @@ +/** + * @file alscratcharena_test.cpp + * @brief A thread's scratch arena: blocks last on top, given back last + * first, and the heap for what does not fit. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "alscratcharena.h" + +#include + +#include +#include +#include +#include + +namespace +{ + std::uintptr_t address(const void* p) + { + return reinterpret_cast(p); + } + + TEST_CASE("alscratcharena: a block given back is where the next one is taken", "[alscratcharena]") + { + ALScratchArena::resetCounters(); + void* first = ALScratchArena::allocate(100); + ALScratchArena::deallocate(first); + void* second = ALScratchArena::allocate(300); + CHECK(second == first); + ALScratchArena::deallocate(second); + + const ALScratchArena::Counters counters = ALScratchArena::counters(); + CHECK(counters.arena_blocks == 2); + CHECK(counters.heap_blocks == 0); + } + + TEST_CASE("alscratcharena: blocks are aligned and each lies above the one before", "[alscratcharena]") + { + void* a = ALScratchArena::allocate(1); + void* b = ALScratchArena::allocate(17); + void* c = ALScratchArena::allocate(0); + void* d = ALScratchArena::allocate(48); + for (void* p : { a, b, c, d }) + { + CHECK(address(p) % ALScratchArena::ALIGNMENT == 0); + } + CHECK(address(b) >= address(a) + 1); + CHECK(address(c) >= address(b) + 17); + CHECK(address(d) > address(c)); + ALScratchArena::deallocate(d); + ALScratchArena::deallocate(c); + ALScratchArena::deallocate(b); + ALScratchArena::deallocate(a); + void* again = ALScratchArena::allocate(1); + CHECK(again == a); + ALScratchArena::deallocate(again); + } + + // A block bigger than what is left comes from the heap, and a smaller + // one taken after it still comes from the arena; both go back to where + // they came from in either order of kind. + TEST_CASE("alscratcharena: a block the arena has no room for comes from the heap", "[alscratcharena]") + { + ALScratchArena::resetCounters(); + void* base = ALScratchArena::allocate(16); + ALScratchArena::deallocate(base); + + void* most = ALScratchArena::allocate(ALScratchArena::CAPACITY - 4096); + void* over = ALScratchArena::allocate(8192); + void* small = ALScratchArena::allocate(1024); + // The first block follows a header of one ALIGNMENT at the arena's + // start. The heap may place a block right after the arena's end. + const std::uintptr_t lo = address(base) - ALScratchArena::ALIGNMENT; + const std::uintptr_t hi = lo + ALScratchArena::CAPACITY; + CHECK((address(most) >= lo && address(most) < hi)); + CHECK(!(address(over) >= lo && address(over) < hi)); + CHECK((address(small) > address(most) && address(small) < hi)); + // The heap's block is written over in full, as a user writes it. + std::memset(over, 0xab, 8192); + ALScratchArena::deallocate(small); + ALScratchArena::deallocate(over); + ALScratchArena::deallocate(most); + + void* whole = ALScratchArena::allocate(ALScratchArena::CAPACITY); + CHECK(!(address(whole) >= lo && address(whole) < hi)); + ALScratchArena::deallocate(whole); + + const ALScratchArena::Counters counters = ALScratchArena::counters(); + CHECK(counters.arena_blocks == 3); + CHECK(counters.heap_blocks == 2); + CHECK(counters.heap_bytes == 8192 + ALScratchArena::CAPACITY); + CHECK(counters.peak <= ALScratchArena::CAPACITY); + CHECK(counters.peak > ALScratchArena::CAPACITY - 4096); + + void* after = ALScratchArena::allocate(16); + CHECK(after == base); + ALScratchArena::deallocate(after); + } + + // meshoptimizer asks for the most a size_t holds where a count times + // its size would overflow, so that the heap refuses it. + TEST_CASE("alscratcharena: a size no arena could hold is the heap's to refuse", "[alscratcharena]") + { + CHECK_THROWS_AS(ALScratchArena::allocate(size_t(-1)), std::bad_alloc); + void* p = ALScratchArena::allocate(32); + void* q = ALScratchArena::allocate(32); + CHECK(address(q) > address(p)); + ALScratchArena::deallocate(q); + ALScratchArena::deallocate(p); + } + + TEST_CASE("alscratcharena: a scope gives its blocks back as it ends", "[alscratcharena]") + { + void* base = ALScratchArena::allocate(16); + ALScratchArena::deallocate(base); + { + ALScratchArena::Scope outer; + U32* a = outer.allocate(1000); + { + ALScratchArena::Scope inner; + F32* b = inner.allocate(4000); + CHECK(address(b) > address(a)); + } + F64* c = outer.allocate(10); + CHECK(address(c) > address(a)); + } + void* after = ALScratchArena::allocate(16); + CHECK(after == base); + ALScratchArena::deallocate(after); + } + + TEST_CASE("alscratcharena: each thread takes from an arena of its own", "[alscratcharena]") + { + void* mine = ALScratchArena::allocate(64); + ALScratchArena::resetCounters(); + void* theirs = nullptr; + ALScratchArena::Counters their_counters; + std::thread([&] + { + ALScratchArena::resetCounters(); + theirs = ALScratchArena::allocate(64); + ALScratchArena::deallocate(theirs); + their_counters = ALScratchArena::counters(); + }) + .join(); + CHECK(!(address(theirs) >= address(mine) && address(theirs) < address(mine) + ALScratchArena::CAPACITY)); + CHECK(their_counters.arena_blocks == 1); + CHECK(ALScratchArena::counters().arena_blocks == 0); + ALScratchArena::deallocate(mine); + } +} From 7a7296abfc770b3a2ca4bc3639e032155b4e8e6f Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 03:20:50 -0400 Subject: [PATCH 2/7] Mesh tangents, welds and vertex order come from meshoptimizer in one pass LLVolumeFace::cacheOptimize took tangents from Blender's MikkTSpace over an unwelded copy of the face, welded it again, and ordered it with meshoptimizer, allocating at every step. It now: - takes MikkTSpace's tangents from meshoptimizer's compatible mode over the face as authored, its positions scaled out of the unit cube and its unit normals by the inverse; - brings each corner's tangent back into the face's frame, puts it in its normal's plane and normalizes it before the weld, so tangents a ulp apart never make two vertices; a corner the plane leaves nothing takes an axis in it, as the prims' do; - welds the corners into one vertex for each of a vertex's tangents, and copies of a vertex the same in every attribute into one; a face whose weld would pass 16-bit indices keeps one of each vertex, each with its first corner's tangent; - orders the triangles for the vertex cache and the vertices for fetch, and writes every attribute once, in its final place; - takes all its scratch from the thread's ALScratchArena, on which meshoptimizer now allocates too (LLMeshOptimizer::initClass, from LLAppViewer::init). GLTF primitives take their tangents from the same compatible mode, so Blender's MikkTSpace port and Lengyel's generator (LLCalculateTangentArray) are gone, with the mikktspace external. unpackVolumeFaces decodes in decodeVolumeFaces and optimizes after, so tests can hold a face as decoded. Tests cover the welds, the fallback past 16-bit indices, the arena, the tangents' frame, and a hidden case that reads the viewer's mesh cache (ALMeshCorpus, AL_MESH_CORPUS_DIR). Over the whole cache, 896,033 faces, every triangle and corner is kept, no vertex is made twice, and every tangent is unit, orthogonal to its normal and of the handedness its triangles' UVs give. Over a 2,000-mesh sample, cacheOptimize takes about 36% fewer cycles than before, and allocates 61 heap blocks in all where it took about 49 a face. Co-Authored-By: Claude Opus 5.5 --- doc/ARCHITECTURE.md | 2 +- doc/TESTING.md | 2 + indra/cmake/Dependencies.cmake | 1 - indra/cmake/attribution.json | 7 - indra/externals/README.md | 1 - indra/externals/mikktspace/LICENSE.txt | 208 ----- .../mikktspace/mikk_atomic_hash_set.hh | 184 ---- .../mikktspace/mikktspace/mikk_float3.hh | 120 --- .../mikktspace/mikktspace/mikk_util.hh | 158 ---- .../mikktspace/mikktspace/mikktspace.hh | 839 ------------------ indra/llmath/CMakeLists.txt | 3 +- indra/llmath/llmeshoptimizer.cpp | 23 +- indra/llmath/llmeshoptimizer.h | 4 + indra/llmath/llvolume.cpp | 636 ++++++------- indra/llmath/llvolume.h | 15 +- indra/llmath/tests/almeshcorpus.h | 248 ++++++ indra/llmath/tests/llmeshoptimizer_test.cpp | 87 ++ indra/llmath/tests/llvolume_bench.cpp | 181 ++-- indra/llmath/tests/llvolume_test.cpp | 643 +++++++++++++- indra/newview/gltf/primitive.cpp | 55 +- indra/newview/llappviewer.cpp | 4 + 21 files changed, 1358 insertions(+), 2063 deletions(-) delete mode 100644 indra/externals/mikktspace/LICENSE.txt delete mode 100644 indra/externals/mikktspace/mikktspace/mikk_atomic_hash_set.hh delete mode 100644 indra/externals/mikktspace/mikktspace/mikk_float3.hh delete mode 100644 indra/externals/mikktspace/mikktspace/mikk_util.hh delete mode 100644 indra/externals/mikktspace/mikktspace/mikktspace.hh create mode 100644 indra/llmath/tests/almeshcorpus.h create mode 100644 indra/llmath/tests/llmeshoptimizer_test.cpp diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 016f1df11c..06e4c773aa 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -2,7 +2,7 @@ **Foundation layer:** - **llcommon** - Foundation: threading (`LLThread`, `LL::ThreadPool`, `LL::WorkQueue`), logging, string utils, timers, event system (`LLEventPump`), coroutines (`LLCoros`), LLSD (universal data type), `LLSingleton` base class, regular expressions (`ALRegex`, over RE2: linear time and no lookaround, so the script editor's find and vim search keep Boost.Regex for their Perl syntax) -- **llmath** - Math primitives: vectors, matrices, quaternions, bounding boxes. Also includes mikktspace tangent generation and meshoptimizer +- **llmath** - Math primitives: vectors, matrices, quaternions, bounding boxes. Also includes meshoptimizer **Data/IO layer:** - **llfilesystem** - Virtual filesystem and asset cache diff --git a/doc/TESTING.md b/doc/TESTING.md index 1bb1bc8064..01607c4150 100644 --- a/doc/TESTING.md +++ b/doc/TESTING.md @@ -18,6 +18,7 @@ ctest --test-dir build-- --output-on-failure -LE manual - **In another order:** `--order rand --rng-seed `. Cases run in declared order by default. - **Every case in a process of its own:** configure with `-DAL_TEST_PER_CASE=ON`. Each case is then its own entry, found when CTest runs. It is for chasing a case that passes alone and fails with the others. - **The log:** `LOGTEST=DEBUG` logs everything at that level as it happens. `LOGFAIL=DEBUG` keeps it and prints a failing case's log only. `--al-log` also writes the log to `..log`. +- **Real mesh assets:** llmath's hidden `[corpus]` case and its corpus benchmark read the meshes of a viewer's cache from the directory `AL_MESH_CORPUS_DIR` names, a seeded sample of them (`indra/llmath/tests/almeshcorpus.h` says how to size and split it), and skip where it is not set: `llmath_tests "[corpus]"`. The assets are other people's, so nothing from them goes into the tree. ## Writing a test @@ -60,6 +61,7 @@ Everything a test file defines goes in an anonymous namespace. A test binary is - `UIImageProbe` (`llrender/lluiimage.h`); - `VertexBufferProbe` (`llrender/llfonttextcache.h`); - `RenderProbe` (`llrender/llrender.h`); + - `VolumeProbe` (`llmath/llvolume.h`); - `TextViewProbe` (`llui/text/altextview.h` and `altextruler.h`); - `alincludesearch_data` (`alscript/preprocessor/alincludesearch.h`). - **Internal is not enough for LL's name-keyed registries.** `LLSingleton`, `LLInstanceTracker` and `LLThreadLocalSingletonPointer` key on `typeid(T).name()`, which is `(anonymous namespace)::X` for every anonymous `X`. A type used with one of them needs a name unique across the tree. diff --git a/indra/cmake/Dependencies.cmake b/indra/cmake/Dependencies.cmake index 22725ff828..f2f3bf4168 100644 --- a/indra/cmake/Dependencies.cmake +++ b/indra/cmake/Dependencies.cmake @@ -101,7 +101,6 @@ al_import(ll::glext HEADER GL/glcorearb.h) al_import(ll::pocketfft HEADER pocketfft_hdronly.h DEFINES POCKETFFT_NO_MULTITHREADING) al_import(ll::tinygltf HEADER tiny_gltf.h) al_import(ll::vhacd HEADER VHACD.h) -al_import(ll::mikktspace INCLUDE ${INDRA_SOURCE_DIR}/externals/mikktspace/) # Per platform, and per option. The target is empty where the condition is # false, so the option gates sources at the consumer and nothing else. diff --git a/indra/cmake/attribution.json b/indra/cmake/attribution.json index 175d4f96d9..21f903a83a 100644 --- a/indra/cmake/attribution.json +++ b/indra/cmake/attribution.json @@ -61,13 +61,6 @@ }, "extras": [ - { - "name": "mikktspace", - "holder": "Copyright (C) 2011 by Morten S. Mikkelsen; Copyright (C) 2022 Blender Authors", - "license": "Apache-2.0", - "homepage": "https://github.com/blender/blender/tree/main/intern/mikktspace", - "text": "externals/mikktspace/LICENSE.txt" - }, { "name": "three.js", "holder": "Copyright (c) 2010-2021 three.js authors", diff --git a/indra/externals/README.md b/indra/externals/README.md index 64930abb73..0688ad19e1 100644 --- a/indra/externals/README.md +++ b/indra/externals/README.md @@ -3,4 +3,3 @@ This directory contains vendored external resources which are unsuited for vcpkg or upstream are no longer active * js - ThreeJS and JPEGEncoder -* mikktspace - Blender Mikktspace - code relatively static and unchanging diff --git a/indra/externals/mikktspace/LICENSE.txt b/indra/externals/mikktspace/LICENSE.txt deleted file mode 100644 index 86b1a0ba77..0000000000 --- a/indra/externals/mikktspace/LICENSE.txt +++ /dev/null @@ -1,208 +0,0 @@ -Copyright (C) 2022 Blender Authors -Copyright (C) 2011 by Morten S. Mikkelsen - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - diff --git a/indra/externals/mikktspace/mikktspace/mikk_atomic_hash_set.hh b/indra/externals/mikktspace/mikktspace/mikk_atomic_hash_set.hh deleted file mode 100644 index 470315995d..0000000000 --- a/indra/externals/mikktspace/mikktspace/mikk_atomic_hash_set.hh +++ /dev/null @@ -1,184 +0,0 @@ -/* SPDX-FileCopyrightText: 2012-2021 Meta Platforms, Inc. and affiliates. - * SPDX-FileCopyrightText: 2022 Blender Authors - * - * SPDX-License-Identifier: Apache-2.0 */ - -/* Simplified version of Folly's AtomicHashArray - * (https://github.com/facebook/folly/blob/main/folly/AtomicHashArray.h). - * - * Notable changes: - * - Standalone and header-only. - * - Behaves like a set, not like a map: There's no value type anymore, only keys. - * - Capacity check logic have been removed, the code assumes you know the required size in - * advance. - * - Custom allocator support has been removed. - * - Erase has been removed. - * - Find has been removed. - */ - -/** \file - * \ingroup mikktspace - */ - -#pragma once - -#ifdef _MSC_VER -# include -#endif - -#include -#include - -namespace mikk { - -struct AtomicHashSetLinearProbeFcn { - inline size_t operator()(size_t idx, size_t /* numProbes */, size_t capacity) const - { - idx += 1; // linear probing - - // Avoid modulus because it's slow - return (idx < capacity) ? idx : (idx - capacity); - } -}; - -struct AtomicHashSetQuadraticProbeFcn { - inline size_t operator()(size_t idx, size_t numProbes, size_t capacity) const - { - idx += numProbes; // quadratic probing - - // Avoid modulus because it's slow - return (idx < capacity) ? idx : (idx - capacity); - } -}; - -template, - class KeyEqual = std::equal_to, - class ProbeFcn = AtomicHashSetLinearProbeFcn> -class AtomicHashSet { - static_assert((std::is_convertible::value || - std::is_convertible::value || - std::is_convertible::value), - "You are trying to use AtomicHashSet with disallowed key " - "types. You must use atomically compare-and-swappable integer " - "keys, or a different container class."); - - public: - const size_t capacity_; - const KeyT kEmptyKey_; - - KeyHash hasher_; - KeyEqual equalityChecker_; - - private: - size_t kAnchorMask_; - /* When using a single thread, we can avoid overhead by not bothering with atomic cells. */ - typedef typename std::conditional, KeyT>::type cell_type; - std::vector cells_; - - public: - struct Config { - KeyT emptyKey; - double maxLoadFactor; - double growthFactor; - size_t capacity; // if positive, overrides maxLoadFactor - - // Cannot have constexpr ctor because some compilers rightly complain. - Config() : emptyKey((KeyT)-1), maxLoadFactor(0.8), growthFactor(-1), capacity(0) {} - }; - - /* Instead of a mess of arguments, we take a max size and a Config struct to - * simulate named ctor parameters. The Config struct has sensible defaults - * for everything, but is overloaded - if you specify a positive capacity, - * that will be used directly instead of computing it based on maxLoadFactor. - */ - AtomicHashSet(size_t maxSize, - KeyHash hasher = KeyHash(), - KeyEqual equalityChecker = KeyEqual(), - const Config &c = Config()) - : capacity_(size_t(double(maxSize) / c.maxLoadFactor) + 1), - kEmptyKey_(c.emptyKey), - hasher_(hasher), - equalityChecker_(equalityChecker), - cells_(capacity_) - { - /* Get next power of two. Could be done more effiently with builtin_clz, but this is not - * performance-critical. */ - kAnchorMask_ = 1; - while (kAnchorMask_ < capacity_) { - kAnchorMask_ *= 2; - } - /* Get mask for lower bits. */ - kAnchorMask_ -= 1; - - /* Not great, but the best we can do to support both atomic and non-atomic cells - * since std::atomic doesn't have a copy constructor so cells_(capacity_, kEmptyKey_) - * in the initializer list won't work. */ - std::fill((KeyT *)cells_.data(), (KeyT *)cells_.data() + capacity_, kEmptyKey_); - } - - AtomicHashSet(const AtomicHashSet &) = delete; - AtomicHashSet &operator=(const AtomicHashSet &) = delete; - - ~AtomicHashSet() = default; - - /* Sequential specialization. */ - bool tryUpdateCell(KeyT *cell, KeyT &existingKey, KeyT newKey) - { - if (*cell == existingKey) { - *cell = newKey; - return true; - } - existingKey = *cell; - return false; - } - - /* Atomic specialization. */ - bool tryUpdateCell(std::atomic *cell, KeyT &existingKey, KeyT newKey) - { - return cell->compare_exchange_strong(existingKey, newKey, std::memory_order_acq_rel); - } - - std::pair emplace(KeyT key) - { - size_t idx = keyToAnchorIdx(key); - size_t numProbes = 0; - for (;;) { - cell_type *cell = &cells_[idx]; - KeyT existingKey = kEmptyKey_; - /* Try to replace empty cell with our key. */ - if (tryUpdateCell(cell, existingKey, key)) { - /* Cell was empty, we're done. */ - return std::make_pair(key, true); - } - - /* Cell was not empty, check if the existing key is equal. */ - if (equalityChecker_(existingKey, key)) { - /* Found equal element, we're done. */ - return std::make_pair(existingKey, false); - } - - /* Continue to next cell according to probe strategy. */ - ++numProbes; - if ((numProbes >= capacity_)) { - // probed every cell...fail - assert(false); - return std::make_pair(kEmptyKey_, false); - } - - idx = ProbeFcn()(idx, numProbes, capacity_); - } - } - - private: - inline size_t keyToAnchorIdx(const KeyT k) const - { - const size_t hashVal = hasher_(k); - const size_t probe = hashVal & kAnchorMask_; - return (probe < capacity_) ? probe : hashVal % capacity_; - } - -}; // AtomicHashSet - -} // namespace mikk diff --git a/indra/externals/mikktspace/mikktspace/mikk_float3.hh b/indra/externals/mikktspace/mikktspace/mikk_float3.hh deleted file mode 100644 index b0b34e9fde..0000000000 --- a/indra/externals/mikktspace/mikktspace/mikk_float3.hh +++ /dev/null @@ -1,120 +0,0 @@ -/* SPDX-FileCopyrightText: 2020-2023 Blender Authors - * - * SPDX-License-Identifier: Apache-2.0 */ - -/** \file - * \ingroup mikktspace - */ - -#pragma once - -#include - -namespace mikk { - -struct float3 { - float x, y, z; - - float3() = default; - - float3(const float *ptr) : x{ptr[0]}, y{ptr[1]}, z{ptr[2]} {} - - float3(const float (*ptr)[3]) : float3((const float *)ptr) {} - - explicit float3(float value) : x(value), y(value), z(value) {} - - explicit float3(int value) : x((float)value), y((float)value), z((float)value) {} - - float3(float x_, float y_, float z_) : x{x_}, y{y_}, z{z_} {} - - static float3 zero() - { - return {0.0f, 0.0f, 0.0f}; - } - - friend float3 operator*(const float3 &a, float b) - { - return {a.x * b, a.y * b, a.z * b}; - } - - friend float3 operator*(float b, const float3 &a) - { - return {a.x * b, a.y * b, a.z * b}; - } - - friend float3 operator-(const float3 &a, const float3 &b) - { - return {a.x - b.x, a.y - b.y, a.z - b.z}; - } - - friend float3 operator-(const float3 &a) - { - return {-a.x, -a.y, -a.z}; - } - - friend bool operator==(const float3 &a, const float3 &b) - { - return a.x == b.x && a.y == b.y && a.z == b.z; - } - - float length_squared() const - { - return x * x + y * y + z * z; - } - - float length() const - { - return sqrt(length_squared()); - } - - static float distance(const float3 &a, const float3 &b) - { - return (a - b).length(); - } - - friend float3 operator+(const float3 &a, const float3 &b) - { - return {a.x + b.x, a.y + b.y, a.z + b.z}; - } - - void operator+=(const float3 &b) - { - this->x += b.x; - this->y += b.y; - this->z += b.z; - } - - friend float3 operator*(const float3 &a, const float3 &b) - { - return {a.x * b.x, a.y * b.y, a.z * b.z}; - } - - float3 normalize() const - { - const float len = length(); - return (len != 0.0f) ? *this * (1.0f / len) : *this; - } - - float reduce_add() const - { - return x + y + z; - } -}; - -inline float dot(const float3 &a, const float3 &b) -{ - return a.x * b.x + a.y * b.y + a.z * b.z; -} - -inline float distance(const float3 &a, const float3 &b) -{ - return float3::distance(a, b); -} - -/* Projects v onto the surface with normal n. */ -inline float3 project(const float3 &n, const float3 &v) -{ - return (v - n * dot(n, v)).normalize(); -} - -} // namespace mikk diff --git a/indra/externals/mikktspace/mikktspace/mikk_util.hh b/indra/externals/mikktspace/mikktspace/mikk_util.hh deleted file mode 100644 index bff35a3b1a..0000000000 --- a/indra/externals/mikktspace/mikktspace/mikk_util.hh +++ /dev/null @@ -1,158 +0,0 @@ -/* SPDX-FileCopyrightText: 2022-2023 Blender Authors - * - * SPDX-License-Identifier: Apache-2.0 */ - -/** \file - * \ingroup mikktspace - */ - -#pragma once - -#include -#include - -#ifndef M_PI_F -# define M_PI_F (3.1415926535897932f) /* pi */ -#endif - -namespace mikk { - -inline bool not_zero(const float fX) -{ - return fabsf(fX) > FLT_MIN; -} - -/* Helpers for (un)packing a 2-bit vertex index and a 30-bit face index to one integer. */ -static uint pack_index(const uint face, const uint vert) -{ - assert((vert & 0x3) == vert); - return (face << 2) | (vert & 0x3); -} - -static void unpack_index(uint &face, uint &vert, const uint indexIn) -{ - vert = indexIn & 0x3; - face = indexIn >> 2; -} - -/* From intern/cycles/util/math_fast.h */ -inline float fast_acosf(float x) -{ - const float f = fabsf(x); - /* clamp and crush denormals. */ - const float m = (f < 1.0f) ? 1.0f - (1.0f - f) : 1.0f; - /* Based on http://www.pouet.net/topic.php?which=9132&page=2 - * 85% accurate (ULP 0) - * Examined 2130706434 values of acos: - * 15.2000597 avg ULP diff, 4492 max ULP, 4.51803e-05 max error // without "denormal crush" - * Examined 2130706434 values of acos: - * 15.2007108 avg ULP diff, 4492 max ULP, 4.51803e-05 max error // with "denormal crush" - */ - const float a = sqrtf(1.0f - m) * - (1.5707963267f + m * (-0.213300989f + m * (0.077980478f + m * -0.02164095f))); - return x < 0 ? M_PI_F - a : a; -} - -static uint rotl(uint x, uint k) -{ - return (x << k) | (x >> (32 - k)); -} - -static uint hash_uint3(uint kx, uint ky, uint kz) -{ - uint a, b, c; - a = b = c = 0xdeadbeef + (2 << 2) + 13; - - c += kz; - b += ky; - a += kx; - - c = (c ^ b) - rotl(b, 14); - a = (a ^ c) - rotl(c, 11); - b = (b ^ a) - rotl(a, 25); - c = (c ^ b) - rotl(b, 16); - - return c; -} - -static uint hash_uint3_fast(const uint x, const uint y, const uint z) -{ - return (x * 73856093) ^ (y * 19349663) ^ (z * 83492791); -} - -static uint float_as_uint(const float v) -{ - return *((uint *)(&v)); -} - -static float uint_as_float(const uint v) -{ - return *((float *)(&v)); -} - -static uint hash_float3_fast(const float x, const float y, const float z) -{ - return hash_uint3_fast(float_as_uint(x), float_as_uint(y), float_as_uint(z)); -} - -static uint hash_float3x3(const float3 &x, const float3 &y, const float3 &z) -{ - return hash_uint3(hash_float3_fast(x.x, x.y, x.z), - hash_float3_fast(y.x, y.y, y.z), - hash_float3_fast(z.x, z.y, z.z)); -} - -template -void radixsort(std::vector &data, std::vector &data2, KeyGetter getKey) -{ - typedef decltype(getKey(data[0])) key_t; - constexpr size_t datasize = sizeof(key_t); - static_assert(datasize % 2 == 0); - static_assert(std::is_integral::value); - - uint bins[datasize][257] = {{0}}; - - /* Count number of elements per bin. */ - for (const T &item : data) { - key_t key = getKey(item); - for (uint pass = 0; pass < datasize; pass++) - bins[pass][((key >> (8 * pass)) & 0xff) + 1]++; - } - - /* Compute prefix sum to find position of each bin in the sorted array. */ - for (uint pass = 0; pass < datasize; pass++) { - for (uint i = 2; i < 256; i++) { - bins[pass][i] += bins[pass][i - 1]; - } - } - - int shift = 0; - for (uint pass = 0; pass < datasize; pass++, shift += 8) { - /* Insert the elements in their correct location based on their bin. */ - for (const T &item : data) { - uint pos = bins[pass][(getKey(item) >> shift) & 0xff]++; - data2[pos] = item; - } - - /* Swap arrays. */ - std::swap(data, data2); - } -} - -static void float_add_atomic(float *val, float add) -{ - /* Hacky, but atomic floats are only supported from C++20 onward. - * This works in practice since `std::atomic` is really just an `uint32_t` in memory, - * so this cast lets us do a 32-bit CAS operation (which is used to build the atomic float - * operation) without needing any external libraries or compiler-specific builtins. */ - std::atomic *atomic_val = reinterpret_cast *>(val); - for (;;) { - uint32_t old_v = atomic_val->load(); - uint32_t new_v = float_as_uint(uint_as_float(old_v) + add); - if (atomic_val->compare_exchange_weak(old_v, new_v)) { - return; - } - } -} - -} // namespace mikk diff --git a/indra/externals/mikktspace/mikktspace/mikktspace.hh b/indra/externals/mikktspace/mikktspace/mikktspace.hh deleted file mode 100644 index 9f1761c504..0000000000 --- a/indra/externals/mikktspace/mikktspace/mikktspace.hh +++ /dev/null @@ -1,839 +0,0 @@ -/* SPDX-FileCopyrightText: 2011 Morten S. Mikkelsen - * SPDX-FileCopyrightText: 2022 Blender Authors - * - * SPDX-License-Identifier: Apache-2.0 */ - -/** \file - * \ingroup mikktspace - */ - -#include -#include -#include -#include - -#ifdef WITH_TBB -# include -#endif - -typedef uint32_t uint; - -#include "mikk_atomic_hash_set.hh" -#include "mikk_float3.hh" -#include "mikk_util.hh" - -namespace mikk { - -static constexpr uint UNSET_ENTRY = 0xffffffffu; - -template class Mikktspace { - struct Triangle { - /* Stores neighboring triangle for group assignment. */ - std::array neighbor; - /* Stores assigned group of each vertex. */ - std::array group; - /* Stores vertex indices that make up the triangle. */ - std::array vertices; - - /* Computed face tangent, will be accumulated into group. */ - float3 tangent; - - /* Index of the face that this triangle belongs to. */ - uint faceIdx; - /* Index of the first of this triangle's vertices' TSpaces. */ - uint tSpaceIdx; - - /* Stores mapping from this triangle's vertices to the original - * face's vertices (relevant for quads). */ - std::array faceVertex; - - // flags - bool markDegenerate : 1; - bool quadOneDegenTri : 1; - bool groupWithAny : 1; - bool orientPreserving : 1; - - Triangle(uint faceIdx_, uint tSpaceIdx_) - : tangent{0.0f}, - faceIdx{faceIdx_}, - tSpaceIdx{tSpaceIdx_}, - markDegenerate{false}, - quadOneDegenTri{false}, - groupWithAny{true}, - orientPreserving{false} - { - neighbor.fill(UNSET_ENTRY); - group.fill(UNSET_ENTRY); - } - - void setVertices(uint8_t i0, uint8_t i1, uint8_t i2) - { - faceVertex[0] = i0; - faceVertex[1] = i1; - faceVertex[2] = i2; - vertices[0] = pack_index(faceIdx, i0); - vertices[1] = pack_index(faceIdx, i1); - vertices[2] = pack_index(faceIdx, i2); - } - }; - - struct Group { - float3 tangent; - uint vertexRepresentative; - bool orientPreserving; - - Group(uint vertexRepresentative_, bool orientPreserving_) - : tangent{0.0f}, - vertexRepresentative{vertexRepresentative_}, - orientPreserving{orientPreserving_} - { - } - - void normalizeTSpace() - { - tangent = tangent.normalize(); - } - - void accumulateTSpaceAtomic(float3 v_tangent) - { - float_add_atomic(&tangent.x, v_tangent.x); - float_add_atomic(&tangent.y, v_tangent.y); - float_add_atomic(&tangent.z, v_tangent.z); - } - - void accumulateTSpace(float3 v_tangent) - { - tangent += v_tangent; - } - }; - - struct TSpace { - float3 tangent = float3(1.0f, 0.0f, 0.0f); - uint counter = 0; - bool orientPreserving = false; - - void accumulateGroup(const Group &group) - { - assert(counter < 2); - - if (counter == 0) { - tangent = group.tangent; - } - else if (tangent == group.tangent) { - // this if is important. Due to floating point precision - // averaging when ts0==ts1 will cause a slight difference - // which results in tangent space splits later on, so do nothing - } - else { - tangent = (tangent + group.tangent).normalize(); - } - - counter++; - orientPreserving = group.orientPreserving; - } - }; - - Mesh &mesh; - - std::vector triangles; - std::vector tSpaces; - std::vector groups; - - uint nrTSpaces, nrFaces, nrTriangles, totalTriangles; - - int nrThreads; - bool isParallel; - - public: - Mikktspace(Mesh &mesh_) : mesh(mesh_) {} - - void genTangSpace() - { - nrFaces = uint(mesh.GetNumFaces()); - -#ifdef WITH_TBB - nrThreads = tbb::this_task_arena::max_concurrency(); - isParallel = (nrThreads > 1) && (nrFaces > 10000); -#else - nrThreads = 1; - isParallel = false; -#endif - - // make an initial triangle --> face index list - generateInitialVerticesIndexList(); - - if (nrTriangles == 0) { - return; - } - - // make a welded index list of identical positions and attributes (pos, norm, texc) - generateSharedVerticesIndexList(); - - // mark all triangle pairs that belong to a quad with only one - // good triangle. These need special treatment in degenEpilogue(). - // Additionally, move all good triangles to the start of - // triangles[] without changing order and - // put the degenerate triangles last. - degenPrologue(); - - if (nrTriangles == 0) { - // No point in building tangents if there are no non-degenerate triangles, so just zero them - tSpaces.resize(nrTSpaces); - } - else { - // evaluate triangle level attributes and neighbor list - initTriangle(); - - // match up edge pairs - buildNeighbors(); - - // based on the 4 rules, identify groups based on connectivity - build4RuleGroups(); - - // make tspaces, each group is split up into subgroups. - // Finally a tangent space is made for every resulting subgroup - generateTSpaces(); - - // degenerate quads with one good triangle will be fixed by copying a space from - // the good triangle to the coinciding vertex. - // all other degenerate triangles will just copy a space from any good triangle - // with the same welded index in vertices[]. - degenEpilogue(); - } - - uint index = 0; - for (uint f = 0; f < nrFaces; f++) { - const uint verts = mesh.GetNumVerticesOfFace(f); - if (verts != 3 && verts != 4) { - continue; - } - - // set data - for (uint i = 0; i < verts; i++) { - const TSpace &tSpace = tSpaces[index++]; - mesh.SetTangentSpace(f, i, tSpace.tangent, tSpace.orientPreserving); - } - } - } - - protected: - template void runParallel(uint start, uint end, F func) - { -#ifdef WITH_TBB - if (isParallel) { - tbb::parallel_for(start, end, func); - } - else -#endif - { - for (uint i = start; i < end; i++) { - func(i); - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////////// - - float3 getPosition(uint vertexID) - { - uint f, v; - unpack_index(f, v, vertexID); - return mesh.GetPosition(f, v); - } - - float3 getNormal(uint vertexID) - { - uint f, v; - unpack_index(f, v, vertexID); - return mesh.GetNormal(f, v); - } - - float3 getTexCoord(uint vertexID) - { - uint f, v; - unpack_index(f, v, vertexID); - return mesh.GetTexCoord(f, v); - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////////// - - void generateInitialVerticesIndexList() - { - nrTriangles = 0; - for (uint f = 0; f < nrFaces; f++) { - const uint verts = mesh.GetNumVerticesOfFace(f); - if (verts == 3) { - nrTriangles += 1; - } - else if (verts == 4) { - nrTriangles += 2; - } - } - - triangles.reserve(nrTriangles); - - nrTSpaces = 0; - for (uint f = 0; f < nrFaces; f++) { - const uint verts = mesh.GetNumVerticesOfFace(f); - if (verts != 3 && verts != 4) { - continue; - } - - uint tA = uint(triangles.size()); - triangles.emplace_back(f, nrTSpaces); - Triangle &triA = triangles[tA]; - - if (verts == 3) { - triA.setVertices(0, 1, 2); - } - else { - uint tB = uint(triangles.size()); - triangles.emplace_back(f, nrTSpaces); - Triangle &triB = triangles[tB]; - - // need an order independent way to evaluate - // tspace on quads. This is done by splitting - // along the shortest diagonal. - float distSQ_02 = (mesh.GetTexCoord(f, 2) - mesh.GetTexCoord(f, 0)).length_squared(); - float distSQ_13 = (mesh.GetTexCoord(f, 3) - mesh.GetTexCoord(f, 1)).length_squared(); - bool quadDiagIs_02; - if (distSQ_02 != distSQ_13) { - quadDiagIs_02 = (distSQ_02 < distSQ_13); - } - else { - distSQ_02 = (mesh.GetPosition(f, 2) - mesh.GetPosition(f, 0)).length_squared(); - distSQ_13 = (mesh.GetPosition(f, 3) - mesh.GetPosition(f, 1)).length_squared(); - quadDiagIs_02 = !(distSQ_13 < distSQ_02); - } - - if (quadDiagIs_02) { - triA.setVertices(0, 1, 2); - triB.setVertices(0, 2, 3); - } - else { - triA.setVertices(0, 1, 3); - triB.setVertices(1, 2, 3); - } - } - - nrTSpaces += verts; - } - } - - struct VertexHash { - Mikktspace *mikk; - inline uint operator()(const uint &k) const - { - return hash_float3x3(mikk->getPosition(k), mikk->getNormal(k), mikk->getTexCoord(k)); - } - }; - - struct VertexEqual { - Mikktspace *mikk; - inline bool operator()(const uint &kA, const uint &kB) const - { - return mikk->getTexCoord(kA) == mikk->getTexCoord(kB) && - mikk->getNormal(kA) == mikk->getNormal(kB) && - mikk->getPosition(kA) == mikk->getPosition(kB); - } - }; - - /* Merge identical vertices. - * To find vertices with identical position, normal and texcoord, we calculate a hash of the 9 - * values. Then, by sorting based on that hash, identical elements (having identical hashes) will - * be moved next to each other. Since there might be hash collisions, the elements of each block - * are then compared with each other and duplicates are merged. - */ - template void generateSharedVerticesIndexList_impl() - { - uint numVertices = nrTriangles * 3; - AtomicHashSet set(numVertices, {this}, {this}); - runParallel(0u, nrTriangles, [&](uint t) { - for (uint i = 0; i < 3; i++) { - auto res = set.emplace(triangles[t].vertices[i]); - if (!res.second) { - triangles[t].vertices[i] = res.first; - } - } - }); - } - void generateSharedVerticesIndexList() - { - if (isParallel) { - generateSharedVerticesIndexList_impl(); - } - else { - generateSharedVerticesIndexList_impl(); - } - } - - ///////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////// Degenerate triangles //////////////////////////////////// - - void degenPrologue() - { - // Mark all degenerate triangles - totalTriangles = nrTriangles; - std::atomic degenTriangles(0); - runParallel(0u, totalTriangles, [&](uint t) { - const float3 p0 = getPosition(triangles[t].vertices[0]); - const float3 p1 = getPosition(triangles[t].vertices[1]); - const float3 p2 = getPosition(triangles[t].vertices[2]); - if (p0 == p1 || p0 == p2 || p1 == p2) // degenerate - { - triangles[t].markDegenerate = true; - degenTriangles.fetch_add(1); - } - }); - nrTriangles -= degenTriangles.load(); - - if (totalTriangles == nrTriangles) { - return; - } - - // locate quads with only one good triangle - runParallel(0u, totalTriangles - 1, [&](uint t) { - Triangle &triangleA = triangles[t], &triangleB = triangles[t + 1]; - if (triangleA.faceIdx != triangleB.faceIdx) { - /* Individual triangle, skip. */ - return; - } - if (triangleA.markDegenerate != triangleB.markDegenerate) { - triangleA.quadOneDegenTri = true; - triangleB.quadOneDegenTri = true; - } - }); - - std::stable_partition(triangles.begin(), triangles.end(), [](const Triangle &tri) { - return !tri.markDegenerate; - }); - } - - void degenEpilogue() - { - if (nrTriangles == totalTriangles) { - return; - } - - std::unordered_map goodTriangleMap; - for (uint t = 0; t < nrTriangles; t++) { - for (uint i = 0; i < 3; i++) { - goodTriangleMap.emplace(triangles[t].vertices[i], pack_index(t, i)); - } - } - - // deal with degenerate triangles - // punishment for degenerate triangles is O(nrTriangles) extra memory. - for (uint t = nrTriangles; t < totalTriangles; t++) { - // degenerate triangles on a quad with one good triangle are skipped - // here but processed in the next loop - if (triangles[t].quadOneDegenTri) { - continue; - } - - for (uint i = 0; i < 3; i++) { - const auto entry = goodTriangleMap.find(triangles[t].vertices[i]); - if (entry == goodTriangleMap.end()) { - // Matching vertex from good triangle is not found. - continue; - } - - uint tSrc, iSrc; - unpack_index(tSrc, iSrc, entry->second); - const uint iSrcVert = triangles[tSrc].faceVertex[iSrc]; - const uint iSrcOffs = triangles[tSrc].tSpaceIdx; - const uint iDstVert = triangles[t].faceVertex[i]; - const uint iDstOffs = triangles[t].tSpaceIdx; - // copy tspace - tSpaces[iDstOffs + iDstVert] = tSpaces[iSrcOffs + iSrcVert]; - } - } - - // deal with degenerate quads with one good triangle - for (uint t = 0; t < nrTriangles; t++) { - // this triangle belongs to a quad where the - // other triangle is degenerate - if (!triangles[t].quadOneDegenTri) { - continue; - } - uint vertFlag = (1u << triangles[t].faceVertex[0]) | (1u << triangles[t].faceVertex[1]) | - (1u << triangles[t].faceVertex[2]); - uint missingFaceVertex = 0; - if ((vertFlag & 2) == 0) { - missingFaceVertex = 1; - } - else if ((vertFlag & 4) == 0) { - missingFaceVertex = 2; - } - else if ((vertFlag & 8) == 0) { - missingFaceVertex = 3; - } - - uint faceIdx = triangles[t].faceIdx; - float3 dstP = mesh.GetPosition(faceIdx, missingFaceVertex); - bool found = false; - for (uint i = 0; i < 3; i++) { - const uint faceVertex = triangles[t].faceVertex[i]; - const float3 srcP = mesh.GetPosition(faceIdx, faceVertex); - if (srcP == dstP) { - const uint offset = triangles[t].tSpaceIdx; - tSpaces[offset + missingFaceVertex] = tSpaces[offset + faceVertex]; - found = true; - break; - } - } - assert(found); - (void)found; - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////////// - - // returns the texture area times 2 - float calcTexArea(uint tri) - { - const float3 t1 = getTexCoord(triangles[tri].vertices[0]); - const float3 t2 = getTexCoord(triangles[tri].vertices[1]); - const float3 t3 = getTexCoord(triangles[tri].vertices[2]); - - const float t21x = t2.x - t1.x; - const float t21y = t2.y - t1.y; - const float t31x = t3.x - t1.x; - const float t31y = t3.y - t1.y; - - const float signedAreaSTx2 = t21x * t31y - t21y * t31x; - return fabsf(signedAreaSTx2); - } - - void initTriangle() - { - // triangles[f].iFlag is cleared in generateInitialVerticesIndexList() - // which is called before this function. - - // evaluate first order derivatives - runParallel(0u, nrTriangles, [&](uint t) { - Triangle &triangle = triangles[t]; - - // initial values - const float3 v1 = getPosition(triangle.vertices[0]); - const float3 v2 = getPosition(triangle.vertices[1]); - const float3 v3 = getPosition(triangle.vertices[2]); - const float3 t1 = getTexCoord(triangle.vertices[0]); - const float3 t2 = getTexCoord(triangle.vertices[1]); - const float3 t3 = getTexCoord(triangle.vertices[2]); - - const float t21x = t2.x - t1.x; - const float t21y = t2.y - t1.y; - const float t31x = t3.x - t1.x; - const float t31y = t3.y - t1.y; - const float3 d1 = v2 - v1, d2 = v3 - v1; - - const float signedAreaSTx2 = t21x * t31y - t21y * t31x; - const float3 vOs = (t31y * d1) - (t21y * d2); // eq 18 - const float3 vOt = (-t31x * d1) + (t21x * d2); // eq 19 - - triangle.orientPreserving = (signedAreaSTx2 > 0); - - if (not_zero(signedAreaSTx2)) { - const float lenOs2 = vOs.length_squared(); - const float lenOt2 = vOt.length_squared(); - const float fS = triangle.orientPreserving ? 1.0f : (-1.0f); - if (not_zero(lenOs2)) { - triangle.tangent = vOs * (fS / sqrtf(lenOs2)); - } - - // if this is a good triangle - if (not_zero(lenOs2) && not_zero(lenOt2)) { - triangle.groupWithAny = false; - } - } - }); - - // force otherwise healthy quads to a fixed orientation - runParallel(0u, nrTriangles - 1, [&](uint t) { - Triangle &triangleA = triangles[t], &triangleB = triangles[t + 1]; - if (triangleA.faceIdx != triangleB.faceIdx) { - // this is not a quad - return; - } - - // bad triangles should already have been removed by - // degenPrologue(), but just in case check that neither are degenerate - if (!(triangleA.markDegenerate || triangleB.markDegenerate)) { - // if this happens the quad has extremely bad mapping!! - if (triangleA.orientPreserving != triangleB.orientPreserving) { - bool chooseOrientFirstTri = false; - if (triangleB.groupWithAny) { - chooseOrientFirstTri = true; - } - else if (calcTexArea(t) >= calcTexArea(t + 1)) { - chooseOrientFirstTri = true; - } - - // force match - const uint t0 = chooseOrientFirstTri ? t : (t + 1); - const uint t1 = chooseOrientFirstTri ? (t + 1) : t; - triangles[t1].orientPreserving = triangles[t0].orientPreserving; - } - } - }); - } - - ///////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////// Edges /////////////////////////////////////////// - - struct NeighborShard { - struct Entry { - Entry(uint32_t key_, uint data_) : key(key_), data(data_) {} - uint key, data; - }; - std::vector entries; - - NeighborShard(size_t capacity) - { - entries.reserve(capacity); - } - - void buildNeighbors(Mikktspace *mikk) - { - /* Entries are added by iterating over t, so by using a stable sort, - * we don't have to compare based on t as well. */ - { - std::vector tempEntries(entries.size(), {0, 0}); - radixsort(entries, tempEntries, [](const Entry &e) { return e.key; }); - } - - for (uint i = 0; i < entries.size(); i++) { - const Entry &a = entries[i]; - uint tA, iA; - unpack_index(tA, iA, a.data); - Mikktspace::Triangle &triA = mikk->triangles[tA]; - - if (triA.neighbor[iA] != UNSET_ENTRY) { - continue; - } - - uint i0A = triA.vertices[iA], i1A = triA.vertices[(iA != 2) ? (iA + 1) : 0]; - for (uint j = i + 1; j < entries.size(); j++) { - const Entry &b = entries[j]; - uint tB, iB; - unpack_index(tB, iB, b.data); - Mikktspace::Triangle &triB = mikk->triangles[tB]; - - if (b.key != a.key) - break; - - if (triB.neighbor[iB] != UNSET_ENTRY) { - continue; - } - - uint i1B = triB.vertices[iB], i0B = triB.vertices[(iB != 2) ? (iB + 1) : 0]; - if (i0A == i0B && i1A == i1B) { - triA.neighbor[iA] = tB; - triB.neighbor[iB] = tA; - break; - } - } - } - } - }; - - void buildNeighbors() - { - /* In order to parallelize the processing, we divide the vertices into shards. - * Since only vertex pairs with the same key will be checked, we can process - * shards independently as long as we ensure that all vertices with the same - * key go into the same shard. - * This is done by hashing the key to get the shard index of each vertex. - */ - // TODO: Two-step filling that first counts and then fills? Could be parallel then. - uint targetNrShards = isParallel ? uint(4 * nrThreads) : 1; - uint nrShards = 1, hashShift = 32; - while (nrShards < targetNrShards) { - nrShards *= 2; - hashShift -= 1; - } - - /* Reserve 25% extra to account for variation due to hashing. */ - size_t reserveSize = size_t(double(3 * nrTriangles) * 1.25 / nrShards); - std::vector shards(nrShards, {reserveSize}); - - for (uint t = 0; t < nrTriangles; t++) { - Triangle &triangle = triangles[t]; - for (uint i = 0; i < 3; i++) { - const uint i0 = triangle.vertices[i]; - const uint i1 = triangle.vertices[(i != 2) ? (i + 1) : 0]; - const uint high = std::max(i0, i1), low = std::min(i0, i1); - const uint hash = hash_uint3(high, low, 0); - /* TODO: Reusing the hash here means less hash space inside each shard. - * Computing a second hash with a different seed it probably not worth it? */ - const uint shard = isParallel ? (hash >> hashShift) : 0; - shards[shard].entries.emplace_back(hash, pack_index(t, i)); - } - } - - runParallel(0u, nrShards, [&](uint s) { shards[s].buildNeighbors(this); }); - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////////// - - void assignRecur(const uint t, uint groupId) - { - if (t == UNSET_ENTRY) { - return; - } - - Triangle &triangle = triangles[t]; - Group &group = groups[groupId]; - - // track down vertex - const uint vertRep = group.vertexRepresentative; - uint i = 3; - if (triangle.vertices[0] == vertRep) { - i = 0; - } - else if (triangle.vertices[1] == vertRep) { - i = 1; - } - else if (triangle.vertices[2] == vertRep) { - i = 2; - } - assert(i < 3); - - // early out - if (triangle.group[i] != UNSET_ENTRY) { - return; - } - - if (triangle.groupWithAny) { - // first to group with a group-with-anything triangle - // determines its orientation. - // This is the only existing order dependency in the code!! - if (triangle.group[0] == UNSET_ENTRY && triangle.group[1] == UNSET_ENTRY && - triangle.group[2] == UNSET_ENTRY) - { - triangle.orientPreserving = group.orientPreserving; - } - } - - if (triangle.orientPreserving != group.orientPreserving) { - return; - } - - triangle.group[i] = groupId; - - const uint t_L = triangle.neighbor[i]; - const uint t_R = triangle.neighbor[i > 0 ? (i - 1) : 2]; - assignRecur(t_L, groupId); - assignRecur(t_R, groupId); - } - - void build4RuleGroups() - { - /* NOTE: This could be parallelized by grouping all [t, i] pairs into - * shards by hash(triangles[t].vertices[i]). This way, each shard can be processed - * independently and in parallel. - * However, the `groupWithAny` logic needs special handling (e.g. lock a mutex when - * encountering a `groupWithAny` triangle, then sort it out, then unlock and proceed). */ - for (uint t = 0; t < nrTriangles; t++) { - Triangle &triangle = triangles[t]; - for (uint i = 0; i < 3; i++) { - // if not assigned to a group - if (triangle.groupWithAny || triangle.group[i] != UNSET_ENTRY) { - continue; - } - - const uint newGroupId = uint(groups.size()); - triangle.group[i] = newGroupId; - - groups.emplace_back(triangle.vertices[i], bool(triangle.orientPreserving)); - - const uint t_L = triangle.neighbor[i]; - const uint t_R = triangle.neighbor[i > 0 ? (i - 1) : 2]; - assignRecur(t_L, newGroupId); - assignRecur(t_R, newGroupId); - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////////////// - - template void accumulateTSpaces(uint t) - { - const Triangle &triangle = triangles[t]; - // only valid triangles get to add their contribution - if (triangle.groupWithAny) { - return; - } - - /* Todo: Vectorize? - * Also: Could add special case for flat shading, when all normals are equal half of the fCos - * projections and two of the three tangent projections are unnecessary. */ - std::array n, p; - for (uint i = 0; i < 3; i++) { - n[i] = getNormal(triangle.vertices[i]); - p[i] = getPosition(triangle.vertices[i]); - } - - std::array fCos = {dot(project(n[0], p[1] - p[0]), project(n[0], p[2] - p[0])), - dot(project(n[1], p[2] - p[1]), project(n[1], p[0] - p[1])), - dot(project(n[2], p[0] - p[2]), project(n[2], p[1] - p[2]))}; - - for (uint i = 0; i < 3; i++) { - uint groupId = triangle.group[i]; - if (groupId != UNSET_ENTRY) { - float3 tangent = project(n[i], triangle.tangent) * - fast_acosf(std::clamp(fCos[i], -1.0f, 1.0f)); - if constexpr (atomic) { - groups[groupId].accumulateTSpaceAtomic(tangent); - } - else { - groups[groupId].accumulateTSpace(tangent); - } - } - } - } - - void generateTSpaces() - { - if (isParallel) { - runParallel(0u, nrTriangles, [&](uint t) { accumulateTSpaces(t); }); - } - else { - for (uint t = 0; t < nrTriangles; t++) { - accumulateTSpaces(t); - } - } - - /* TODO: Worth parallelizing? Probably not. */ - for (Group &group : groups) { - group.normalizeTSpace(); - } - - tSpaces.resize(nrTSpaces); - - for (uint t = 0; t < nrTriangles; t++) { - Triangle &triangle = triangles[t]; - for (uint i = 0; i < 3; i++) { - uint groupId = triangle.group[i]; - if (groupId == UNSET_ENTRY) { - continue; - } - const Group group = groups[groupId]; - assert(triangle.orientPreserving == group.orientPreserving); - - // output tspace - const uint offset = triangle.tSpaceIdx; - const uint faceVertex = triangle.faceVertex[i]; - tSpaces[offset + faceVertex].accumulateGroup(group); - } - } - } -}; - -} // namespace mikk diff --git a/indra/llmath/CMakeLists.txt b/indra/llmath/CMakeLists.txt index 82c83e5e6c..f3560b8b8d 100644 --- a/indra/llmath/CMakeLists.txt +++ b/indra/llmath/CMakeLists.txt @@ -98,7 +98,7 @@ target_sources( xform.h ) -target_link_libraries(llmath PRIVATE al::flags PUBLIC llcommon ll::mikktspace ll::meshoptimizer) +target_link_libraries(llmath PRIVATE al::flags PUBLIC llcommon ll::meshoptimizer) target_precompile_headers(llmath REUSE_FROM llprecompiled) # Add tests @@ -117,6 +117,7 @@ if(AL_BUILD_TESTS) tests/llbboxlocal_test.cpp tests/llmatrix3a_test.cpp tests/llmatrix4a_test.cpp + tests/llmeshoptimizer_test.cpp tests/llmodularmath_test.cpp tests/lloctree_test.cpp tests/llquaternion2_test.cpp diff --git a/indra/llmath/llmeshoptimizer.cpp b/indra/llmath/llmeshoptimizer.cpp index bee9d71e1a..ce0899cfa4 100644 --- a/indra/llmath/llmeshoptimizer.cpp +++ b/indra/llmath/llmeshoptimizer.cpp @@ -30,20 +30,37 @@ #include +#include "alscratcharena.h" #include "llmath.h" #include "v2math.h" +namespace +{ + void* MESHOPTIMIZER_ALLOC_CALLCONV scratch_allocate(size_t bytes) + { + return ALScratchArena::allocate(bytes); + } + + void MESHOPTIMIZER_ALLOC_CALLCONV scratch_deallocate(void* block) + { + ALScratchArena::deallocate(block); + } +} // namespace + LLMeshOptimizer::LLMeshOptimizer() { - // Todo: Looks like for memory management, we can add allocator and deallocator callbacks - // Should be one time - // meshopt_setAllocator(allocate, deallocate); } LLMeshOptimizer::~LLMeshOptimizer() { } +//static +void LLMeshOptimizer::initClass() +{ + meshopt_setAllocator(scratch_allocate, scratch_deallocate); +} + //static void LLMeshOptimizer::generateShadowIndexBufferU32(U32 *destination, const U32 *indices, diff --git a/indra/llmath/llmeshoptimizer.h b/indra/llmath/llmeshoptimizer.h index 59a2f86b4c..f5d4169941 100644 --- a/indra/llmath/llmeshoptimizer.h +++ b/indra/llmath/llmeshoptimizer.h @@ -37,6 +37,10 @@ class LLMeshOptimizer LLMeshOptimizer(); ~LLMeshOptimizer(); + // Makes meshoptimizer take its scratch from the calling thread's + // ALScratchArena. Called once, before any thread calls meshoptimizer. + static void initClass(); + static void generateShadowIndexBufferU32( U32 *destination, const U32 *indices, diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 0eead48ebe..5b91624281 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -49,14 +49,13 @@ #include "llsdserialize.h" #include "llvector4a.h" #include "llmatrix4a.h" +#include "alscratcharena.h" #include "alsimdkernels.h" #include "llmeshoptimizer.h" #include "lltimer.h" #include "llvolumeoctree.h" #include "workqueue.h" -#include "mikktspace/mikktspace.hh" - #include constexpr F32 MIN_CUT_DELTA = 0.02f; @@ -2561,6 +2560,27 @@ bool LLVolume::unpackVolumeFaces(U8* in_data, S32 size) } bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + if (!decodeVolumeFaces(mdl)) + { + return false; + } + + if (!cacheOptimize(true)) + { + // Out of memory? + LL_WARNS() << "Failed to optimize!" << LL_ENDL; + mVolumeFaces.clear(); + return false; + } + + mSculptLevel = 0; // success! + + return true; +} + +bool LLVolume::decodeVolumeFaces(const LLSD& mdl) { LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; { @@ -2949,16 +2969,6 @@ bool LLVolume::unpackVolumeFacesInternal(const LLSD& mdl) } } - if (!cacheOptimize(true)) - { - // Out of memory? - LL_WARNS() << "Failed to optimize!" << LL_ENDL; - mVolumeFaces.clear(); - return false; - } - - mSculptLevel = 0; // success! - return true; } @@ -5377,298 +5387,306 @@ class LLVCacheLRU } }; -// data structures for tangent generation - -struct MikktData +namespace +{ + LLVector4a plane_axis(const LLVector4a& normal); + + // v at unit length in x, y and z, or nothing where it has no length. + LL_FORCE_INLINE void unit3_or_zero(LLVector4a& v) + { + LLVector4a length2; + length2.setAllDot3(v, v); + LLVector4a unit = v; + unit.normalize3(); + v.setSelectWithMask(length2.greaterThan(LLVector4a(FLT_MIN)), unit, LLVector4a::getZero()); + } +} // namespace + +// A decoded mesh LOD face made ready to draw: MikkTSpace's tangents, as +// meshoptimizer's compatible mode computes them, and the vertices and +// triangles in the order a vertex cache and a fetch take them best. +// +// The tangents are taken in the shape the mesh was authored in, its +// positions scaled back out of the unit cube by mNormalizedScale and its +// normals by the inverse, and they come back as directions in the face's +// own frame, which the vertex matrix takes as it takes the positions, each +// in its normal's plane. A vertex whose corners take different tangents, as +// on a mirrored UV seam, becomes one vertex for each, and copies of a vertex +// the same in every attribute become one. Every attribute is written once a +// vertex, in its final place, from the face's own arrays, and all the +// scratch is the thread's arena. A face that would need more vertices than +// 16-bit indices reach keeps one of each vertex it has, each with its first +// corner's tangent. +bool LLVolumeFace::cacheOptimize(bool gen_tangents) { - LLVolumeFace* face; - std::vector p; - std::vector n; - std::vector tc; - std::vector w; - std::vector t; + LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; + llassert(!mOptimized); + mOptimized = true; - MikktData(LLVolumeFace* f) - : face(f) + const bool tangents = gen_tangents && mNormals && mTexCoords; + if (!mPositions || !mIndices || mNumVertices <= 0 || mNumIndices <= 0) { - U32 count = face->mNumIndices; - - try - { - p.resize(count); - n.resize(count); - tc.resize(count); - t.resize(count); - - if (face->mWeights) - { - w.resize(count); - } - } - catch (std::bad_alloc&) - { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLCoros") << "Bad memory allocation in MikktData, elements count: " << count << LL_ENDL; - } - - - LLVector3 inv_scale(1.f / face->mNormalizedScale.mV[0], 1.f / face->mNormalizedScale.mV[1], 1.f / face->mNormalizedScale.mV[2]); - - - for (S32 i = 0; i < face->mNumIndices; ++i) + if (!tangents) { - U32 idx = face->mIndices[i]; - - p[i].set(face->mPositions[idx].getF32ptr()); - p[i].scaleVec(face->mNormalizedScale); //put mesh in original coordinate frame when reconstructing tangents - n[i].set(face->mNormals[idx].getF32ptr()); - n[i].scaleVec(inv_scale); - n[i].normalize(); - tc[i].set(face->mTexCoords[idx]); - - if (face->mWeights) - { - w[i].set(face->mWeights[idx].getF32ptr()); - } + // Nothing to order. + return true; } + LL_WARNS_ONCE("LLVolume") << "Invalid volume face data for tangent generation: " + << "mPositions=" << (void*)mPositions + << ", mIndices=" << (void*)mIndices + << ", mNumVertices=" << mNumVertices + << ", mNumIndices=" << mNumIndices << LL_ENDL; + return false; } - uint32_t GetNumFaces() - { - return uint32_t(face->mNumIndices / 3); - } - - uint32_t GetNumVerticesOfFace(const uint32_t face_num) - { - return 3; - } - - mikk::float3 GetPosition(const uint32_t face_num, const uint32_t vert_num) + if (mNumIndices % 3 != 0) { - F32* v = p[face_num * 3 + vert_num].mV; - return mikk::float3(v); + LL_WARNS_ONCE("LLVolume") << "Non-triangulated mesh, mNumIndices=" << mNumIndices << LL_ENDL; + return false; } - mikk::float3 GetTexCoord(const uint32_t face_num, const uint32_t vert_num) + if (tangents && (mNormalizedScale.mV[0] == 0.0f || mNormalizedScale.mV[1] == 0.0f || mNormalizedScale.mV[2] == 0.0f)) { - F32* uv = tc[face_num * 3 + vert_num].mV; - return mikk::float3(uv[0], uv[1], 1.0f); + LL_WARNS_ONCE("LLVolume") << "Invalid normalized scale: " << mNormalizedScale << LL_ENDL; + return false; } - mikk::float3 GetNormal(const uint32_t face_num, const uint32_t vert_num) - { - F32* normal = n[face_num * 3 + vert_num].mV; - return mikk::float3(normal); - } + const size_t index_count = mNumIndices; + const size_t vertex_count = mNumVertices; - void SetTangentSpace(const uint32_t face_num, const uint32_t vert_num, mikk::float3 T, bool orientation) + try { - S32 i = face_num * 3 + vert_num; - t[i].set(T.x, T.y, T.z, orientation ? 1.0f : -1.0f); - } -}; - -bool LLVolumeFace::cacheOptimize(bool gen_tangents) -{ //optimize for vertex cache according to Forsyth method: - LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; - llassert(!mOptimized); - mOptimized = true; + ALScratchArena::Scope scratch; - if (gen_tangents && mNormals && mTexCoords) - { - if (!mPositions || !mIndices || mNumVertices <= 0 || mNumIndices <= 0) + // The indices at 32 bits, as meshoptimizer takes them, checked as + // they go. + U32* indices = scratch.allocate(index_count); + U32 highest = 0; + for (size_t i = 0; i < index_count; ++i) { - LL_WARNS_ONCE("LLVolume") << "Invalid volume face data for tangent generation: " - << "mPositions=" << (void*)mPositions - << ", mIndices=" << (void*)mIndices - << ", mNumVertices=" << mNumVertices - << ", mNumIndices=" << mNumIndices << LL_ENDL; - return false; + indices[i] = mIndices[i]; + highest = llmax(highest, indices[i]); } - - if (mNumIndices % 3 != 0) + if (highest >= vertex_count) { - LL_WARNS_ONCE("LLVolume") << "Non-triangulated mesh, mNumIndices=" << mNumIndices << LL_ENDL; + const U16* bad = std::find_if(mIndices, mIndices + index_count, [&](U16 index) { return index >= vertex_count; }); + LL_WARNS_ONCE("LLVolume") << "Out of bounds index detected: mIndices[" << (bad - mIndices) << "]=" + << *bad << " >= mNumVertices=" << mNumVertices << LL_ENDL; return false; } - for (S32 i = 0; i < mNumIndices; ++i) + const LLVector4a inv_scale(1.f / mNormalizedScale.mV[0], 1.f / mNormalizedScale.mV[1], 1.f / mNormalizedScale.mV[2], 1.f); + + // Each corner's tangent, four floats a corner. + F32* corner_tangents = nullptr; + if (tangents) { - if (mIndices[i] >= mNumVertices) + LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("cacheOptimize - tangents"); + corner_tangents = scratch.allocate(index_count * 4); + + const LLVector4a scale(mNormalizedScale.mV[0], mNormalizedScale.mV[1], mNormalizedScale.mV[2], 1.f); + LLVector4Logical xyz; + xyz.clear(); + xyz.setElement<0>(); + xyz.setElement<1>(); + xyz.setElement<2>(); + + ALScratchArena::Scope inputs; + LLVector4a* positions = inputs.allocate(vertex_count); + LLVector4a* normals = inputs.allocate(vertex_count); + for (size_t v = 0; v < vertex_count; ++v) { - LL_WARNS_ONCE("LLVolume") << "Out of bounds index detected: mIndices[" << i << "]=" - << mIndices[i] << " >= mNumVertices=" << mNumVertices << LL_ENDL; - return false; + positions[v].setMul(mPositions[v], scale); + // The face keeps its normals at unit length. + LLVector4a normal; + normal.setSelectWithMask(xyz, mNormals[v], LLVector4a::getZero()); + unit3_or_zero(normal); + mNormals[v] = normal; + normals[v].setMul(normal, inv_scale); + unit3_or_zero(normals[v]); + } + meshopt_generateTangents(corner_tangents, indices, index_count, positions[0].getF32ptr(), vertex_count, sizeof(LLVector4a), + normals[0].getF32ptr(), sizeof(LLVector4a), mTexCoords[0].mV, sizeof(LLVector2), + meshopt_TangentCompatible | meshopt_TangentZeroFallback); + } + + if (tangents) + { + // Each corner's tangent as the face keeps it, before the weld + // compares them: back in the face's frame, in which a direction + // along the surface is the authored one over the scale, put in + // its normal's plane and normalized. Where the plane leaves it + // nothing, as at a corner of triangles with no area or whose + // tangents cancel or run along the normal, it is an axis in the + // plane, as the prims' take it. w stays the corner's. + for (size_t c = 0; c < index_count; ++c) + { + F32* stored = corner_tangents + c * 4; + const F32 w = stored[3]; + LLVector4a tangent; + tangent.load4a(stored); + tangent.mul(inv_scale); + const LLVector4a& normal = mNormals[indices[c]]; + LLVector4a along; + along.setAllDot3(normal, tangent); + along.mul(normal); + LLVector4a in_plane; + in_plane.setSub(tangent, along); + LLVector4a left, whole; + left.setAllDot3(in_plane, in_plane); + whole.setAllDot3(tangent, tangent); + if (left[0] > 1e-6f * whole[0]) + { + in_plane.normalize3(); + } + else + { + in_plane = plane_axis(normal); + } + in_plane.getF32ptr()[3] = w; + in_plane.store4a(stored); + } + } + + // The vertices the triangles are to take, and for each the corner + // its tangent is taken from. With tangents they are welded from the + // corners, one for each of a vertex's tangents, in the order the + // corners first use them, or, where that would need more than + // 16-bit indices reach, one for each vertex; without, they are the + // face's own. + const U32* welded_indices = indices; + size_t welded_count = vertex_count; + U32* first_corner = nullptr; + if (tangents) + { + LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("cacheOptimize - weld"); + + // A vertex the face holds more than once, the same in every + // attribute, is one vertex: each corner names its vertex by a + // number all the copies share. + U32* canonical = scratch.allocate(vertex_count); + const meshopt_Stream attributes[] = { + { mPositions, sizeof(F32) * 3, sizeof(LLVector4a) }, + { mNormals, sizeof(F32) * 3, sizeof(LLVector4a) }, + { mTexCoords, sizeof(LLVector2), sizeof(LLVector2) }, + { mWeights, sizeof(LLVector4a), sizeof(LLVector4a) }, + }; + const size_t distinct = meshopt_generateVertexRemapMulti(canonical, indices, index_count, vertex_count, attributes, mWeights ? 4 : 3); + U32* corner_vertices = scratch.allocate(index_count); + for (size_t c = 0; c < index_count; ++c) + { + corner_vertices[c] = canonical[indices[c]]; } - } - if (mNormalizedScale.mV[0] == 0.0f || mNormalizedScale.mV[1] == 0.0f || mNormalizedScale.mV[2] == 0.0f) - { - LL_WARNS_ONCE("LLVolume") << "Invalid normalized scale: " << mNormalizedScale << LL_ENDL; - return false; + U32* remap = scratch.allocate(index_count); + const meshopt_Stream streams[] = { + { corner_vertices, sizeof(U32), sizeof(U32) }, + { corner_tangents, sizeof(F32) * 4, sizeof(F32) * 4 }, + }; + const size_t unique = meshopt_generateVertexRemapMulti(remap, nullptr, index_count, index_count, streams, + sizeof(streams) / sizeof(streams[0])); + if (unique < 65535) + { + welded_indices = remap; + welded_count = unique; + } + else + { + welded_indices = corner_vertices; + welded_count = distinct; + } + first_corner = scratch.allocate(welded_count); + for (size_t c = index_count; c-- > 0;) + { + first_corner[welded_indices[c]] = (U32)c; + } } - // generate mikkt space tangents before cache optimizing since the index buffer may change - // a bit of a hack to do this here, but this function gets called exactly once for the lifetime of a mesh - // and is executed on a background thread - MikktData data(this); - mikk::Mikktspace ctx(data); - try - { - ctx.genTangSpace(); - } - catch (std::bad_alloc&) + U32* ordered = scratch.allocate(index_count); { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLVolume") << "Bad memory allocation in MikktData::genTangSpace" << LL_ENDL; + LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("cacheOptimize - cache order"); + meshopt_optimizeVertexCache(ordered, welded_indices, index_count, welded_count); } - catch (...) + + // Each vertex's final place, in the order the reordered triangles + // first take it; one no triangle takes has none. + U32* place = scratch.allocate(welded_count); + size_t kept = 0; { - LL_WARNS_ONCE("LLVolume") << "Mikktspace::genTangSpace() failed" << LL_ENDL; - return false; + LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("cacheOptimize - fetch order"); + kept = meshopt_optimizeVertexFetchRemap(place, ordered, index_count, welded_count); } - - //re-weld - meshopt_Stream mos[] = + LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("cacheOptimize - write"); + LLVector4a* old_positions = mPositions; + const LLVector4a* old_normals = mNormals; + const LLVector2* old_tex_coords = mTexCoords; + LLVector4a* old_weights = mWeights; + LLVector4a* old_tangents = tangents ? nullptr : mTangents; + mPositions = nullptr; + mWeights = nullptr; + if (old_tangents) { - { &data.p[0], sizeof(LLVector3), sizeof(LLVector3) }, - { &data.n[0], sizeof(LLVector3), sizeof(LLVector3) }, - { &data.t[0], sizeof(LLVector4), sizeof(LLVector4) }, - { &data.tc[0], sizeof(LLVector2), sizeof(LLVector2) }, - { data.w.empty() ? nullptr : &data.w[0], sizeof(LLVector4), sizeof(LLVector4) } - }; + mTangents = nullptr; + } - std::vector remap; - try + resizeVertices((S32)kept); + if (!mPositions) { - remap.resize(data.p.size()); + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLVolume") << "Failed to allocate memory for resizeVertices(" << kept << ")" << LL_ENDL; } - catch (std::bad_alloc&) + if (old_weights) { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLVOLUME") << "Failed to allocate memory for remap: " << (S32)data.p.size() << LL_ENDL; + allocateWeights((S32)kept); } - - U32 stream_count = data.w.empty() ? 4 : 5; - - S32 vert_count = 0; - if (!data.p.empty()) + if (tangents || old_tangents) { - try - { - // providing mIndices should help avoid unused vertices - // but those should have been filtered out on upload - vert_count = static_cast(meshopt_generateVertexRemapMulti(&remap[0], nullptr, data.p.size(), data.p.size(), mos, stream_count)); - } - catch (std::bad_alloc&) - { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLVolume") << "Failed to allocate memory for VertexRemap: " << (S32)data.p.size() << LL_ENDL; - } + allocateTangents((S32)kept); } - // Probably should be using meshopt_remapVertexBuffer instead of remaping manually - if (vert_count < 65535 && vert_count > 0) + for (size_t u = 0; u < welded_count; ++u) { - //copy results back into volume - resizeVertices(vert_count); - if (mNumVertices == 0) + const U32 to = place[u]; + if (to == U32_MAX) { - LLError::LLUserWarningMsg::showOutOfMemory(); - LL_ERRS("LLVolume") << "Failed to allocate memory for resizeVertices(" << vert_count << ")" << LL_ENDL; + continue; } + const U32 corner = first_corner ? first_corner[u] : U32_MAX; + const U32 from = first_corner ? indices[corner] : (U32)u; - if (!data.w.empty()) + mPositions[to] = old_positions[from]; + mNormals[to] = old_normals[from]; + mTexCoords[to] = old_tex_coords[from]; + if (old_weights) { - allocateWeights(vert_count); + mWeights[to] = old_weights[from]; } - allocateTangents(mNumVertices); - - for (S32 i = 0; i < mNumIndices; ++i) + if (tangents) { - U32 src_idx = i; - U32 dst_idx = remap[i]; - if (dst_idx == U32_MAX) - { - // Unused indices? Probably need to resize mIndices - dst_idx = mNumVertices - 1; - llassert(false); - LL_DEBUGS_ONCE("LLVOLUME") << "U32_MAX destination index, substituting" << LL_ENDL; - } - else if (dst_idx >= (U32)mNumVertices) - { - dst_idx = mNumVertices - 1; - // Shouldn't happen, figure out what gets returned in remap and why. - llassert(false); - LL_DEBUGS_ONCE("LLVOLUME") << "Invalid destination index, substituting" << LL_ENDL; - } - if (src_idx >= (U32)data.p.size()) - { - // data.p.size() is supposed to be equal to mNumIndices - src_idx = (U32)(data.p.size() - 1); - llassert(false); - LL_DEBUGS_ONCE("LLVOLUME") << "Invalid source index, substituting" << LL_ENDL; - } - mIndices[i] = dst_idx; - - mPositions[dst_idx].load3(data.p[src_idx].mV); - mNormals[dst_idx].load3(data.n[src_idx].mV); - mTexCoords[dst_idx] = data.tc[src_idx]; - - mTangents[dst_idx].loadua(data.t[src_idx].mV); - - if (mWeights) - { - mWeights[dst_idx].loadua(data.w[src_idx].mV); - } + mTangents[to].load4a(corner_tangents + (size_t)corner * 4); } - - // put back in normalized coordinate frame: tangents lie in the surface and scale as positions do, - // normals take the inverse transpose - LLVector4a inv_scale(1.f/mNormalizedScale.mV[0], 1.f / mNormalizedScale.mV[1], 1.f / mNormalizedScale.mV[2]); - LLVector4a scale; - scale.load3(mNormalizedScale.mV); - scale.getF32ptr()[3] = 1.f; - - for (S32 i = 0; i < mNumVertices; ++i) + else if (old_tangents) { - mPositions[i].mul(inv_scale); - mNormals[i].mul(scale); - mNormals[i].normalize3(); - F32 w = mTangents[i].getF32ptr()[3]; - mTangents[i].mul(inv_scale); - mTangents[i].normalize3(); - mTangents[i].getF32ptr()[3] = w; + mTangents[to] = old_tangents[from]; } } - else + + for (size_t i = 0; i < index_count; ++i) { - if (vert_count <= 0) - { - LL_WARNS_ONCE("LLVOLUME") << "meshopt_generateVertexRemapMulti failed to process a model or model was invalid" << LL_ENDL; - } - // blew past the max vertex size limit, use legacy tangent generation which never adds verts; - // it takes unit normals, as the bump map and planar projection code do - for (S32 i = 0; i < mNumVertices; i++) - { - mNormals[i].normalize3fast(); - } - createTangents(); + mIndices[i] = (U16)place[ordered[i]]; } - } - - // cache optimize index buffer - // meshopt needs scratch space, do some pointer shuffling to avoid an extra index buffer copy - U16* src_indices = mIndices; - mIndices = nullptr; - resizeIndices(mNumIndices); - - meshopt_optimizeVertexCache(mIndices, src_indices, mNumIndices, mNumVertices); - - ll_aligned_free_16(src_indices); + ll_aligned_free<64>(old_positions); + ll_aligned_free_16(old_weights); + ll_aligned_free_16(old_tangents); + } + catch (std::bad_alloc&) + { + LLError::LLUserWarningMsg::showOutOfMemory(); + LL_ERRS("LLVolume") << "Bad memory allocation in cacheOptimize, " << vertex_count << " vertices, " << index_count << " indices" << LL_ENDL; + } return true; } @@ -6846,22 +6864,7 @@ void LLVolumeFace::createTangents() } mTangentsStale = false; - if (mOptimized) - { - // A face cacheOptimize has taken is a mesh LOD's, too large - // for its weld or not welded at all: Lengyel's tangents, which - // add no vertices. - LLVector4a* ptr = (LLVector4a*)mTangents; - - LLVector4a* end = mTangents + mNumVertices; - while (ptr < end) - { - (*ptr++).clear(); - } - - LLCalculateTangentArray(mNumVertices, mPositions, mNormals, mTexCoords, mNumIndices / 3, mIndices, mTangents); - } - else if (mFlexible && (mTypeMask & CAP_MASK)) + if (mFlexible && (mTypeMask & CAP_MASK)) { LLVector4a tangent; tangent.loadua(mCapTangent); @@ -7496,111 +7499,4 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) return true; } -//adapted from Lengyel, Eric. "Computing Tangent Space Basis Vectors for an Arbitrary Mesh". Terathon Software 3D Graphics Library, 2001. http://www.terathon.com/code/tangent.html -void LLCalculateTangentArray(U32 vertexCount, const LLVector4a *vertex, const LLVector4a *normal, - const LLVector2 *texcoord, U32 triangleCount, const U16* index_array, LLVector4a *tangent) -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_VOLUME; - - //LLVector4a *tan1 = new LLVector4a[vertexCount * 2]; - LLVector4a* tan1 = (LLVector4a*) ll_aligned_malloc_16(vertexCount*2*sizeof(LLVector4a)); - // new(tan1) LLVector4a; - - LLVector4a* tan2 = tan1 + vertexCount; - - U32 count = vertexCount * 2; - for (U32 i = 0; i < count; i++) - { - tan1[i].clear(); - } - - for (U32 a = 0; a < triangleCount; a++) - { - U32 i1 = *index_array++; - U32 i2 = *index_array++; - U32 i3 = *index_array++; - - const LLVector4a& v1 = vertex[i1]; - const LLVector4a& v2 = vertex[i2]; - const LLVector4a& v3 = vertex[i3]; - - const LLVector2& w1 = texcoord[i1]; - const LLVector2& w2 = texcoord[i2]; - const LLVector2& w3 = texcoord[i3]; - - const F32* v1ptr = v1.getF32ptr(); - const F32* v2ptr = v2.getF32ptr(); - const F32* v3ptr = v3.getF32ptr(); - - float x1 = v2ptr[0] - v1ptr[0]; - float x2 = v3ptr[0] - v1ptr[0]; - float y1 = v2ptr[1] - v1ptr[1]; - float y2 = v3ptr[1] - v1ptr[1]; - float z1 = v2ptr[2] - v1ptr[2]; - float z2 = v3ptr[2] - v1ptr[2]; - - float s1 = w2.mV[0] - w1.mV[0]; - float s2 = w3.mV[0] - w1.mV[0]; - float t1 = w2.mV[1] - w1.mV[1]; - float t2 = w3.mV[1] - w1.mV[1]; - - F32 rd = s1*t2-s2*t1; - - float r = ((rd*rd) > FLT_EPSILON) ? (1.0f / rd) - : ((rd > 0.0f) ? 1024.f : -1024.f); //some made up large ratio for division by zero - - llassert(llfinite(r)); - llassert(!llisnan(r)); - - LLVector4a sdir((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, - (t2 * z1 - t1 * z2) * r); - LLVector4a tdir((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, - (s1 * z2 - s2 * z1) * r); - - tan1[i1].add(sdir); - tan1[i2].add(sdir); - tan1[i3].add(sdir); - - tan2[i1].add(tdir); - tan2[i2].add(tdir); - tan2[i3].add(tdir); - } - - for (U32 a = 0; a < vertexCount; a++) - { - LLVector4a n = normal[a]; - - const LLVector4a& t = tan1[a]; - - LLVector4a ncrosst; - ncrosst.setCross3(n,t); - - // Gram-Schmidt orthogonalize - n.mul(n.dot3(t).getF32()); - - LLVector4a tsubn; - tsubn.setSub(t,n); - - if (tsubn.dot3(tsubn).getF32() > F_APPROXIMATELY_ZERO) - { - tsubn.normalize3fast(); - - // Calculate handedness - F32 handedness = ncrosst.dot3(tan2[a]).getF32() < 0.f ? -1.f : 1.f; - - tsubn.getF32ptr()[3] = handedness; - - tangent[a] = tsubn; - } - else - { - // Nothing gives the tangent a direction: an axis in the plane - // of the normal, as the prims' tangents take it. - tangent[a] = plane_axis(normal[a]); - } - } - - ll_aligned_free_16(tan1); -} - diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index 9366684537..04adfd4f0f 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -43,6 +43,11 @@ class LLVolume; class LLVolumeTriangle; class LLVolumeOctree; +namespace ll_test +{ + struct VolumeProbe; +} + #include "lluuid.h" #include "v4color.h" #include "v2math.h" @@ -1038,6 +1043,7 @@ class LLVolumeFace class LLVolume : public LLRefCount { friend class LLVolumeLODGroup; + friend struct ll_test::VolumeProbe; protected: virtual ~LLVolume(); // use unref @@ -1127,8 +1133,9 @@ class LLVolume : public LLRefCount void copyFacesTo(std::vector &faces) const; void copyFacesFrom(const std::vector &faces); - // use meshoptimizer to optimize index buffer for vertex shader cache - // gen_tangents - if true, generate MikkTSpace tangents if needed before optimizing index buffer + // Each face's vertices and triangles in the order a vertex cache and a + // fetch take them best, with MikkTSpace tangents where gen_tangents is + // set; see LLVolumeFace::cacheOptimize. bool cacheOptimize(bool gen_tangents = false); private: @@ -1145,6 +1152,8 @@ class LLVolume : public LLRefCount bool unpackVolumeFaces(U8* in_data, S32 size); private: bool unpackVolumeFacesInternal(const LLSD& mdl); + // A LOD block's faces as it holds them, before cacheOptimize. + bool decodeVolumeFaces(const LLSD& mdl); public: virtual void setMeshAssetLoaded(bool loaded); @@ -1179,8 +1188,6 @@ class LLVolume : public LLRefCount std::ostream& operator<<(std::ostream &s, const LLVolumeParams &volume_params); -void LLCalculateTangentArray(U32 vertexCount, const LLVector4a *vertex, const LLVector4a *normal, const LLVector2 *texcoord, U32 triangleCount, const U16* index_array, LLVector4a *tangent); - bool LLLineSegmentBoxIntersect(const F32* start, const F32* end, const F32* center, const F32* size); bool LLLineSegmentBoxIntersect(const LLVector3& start, const LLVector3& end, const LLVector3& center, const LLVector3& size); bool LLLineSegmentBoxIntersect(const LLVector4a& start, const LLVector4a& end, const LLVector4a& center, const LLVector4a& size); diff --git a/indra/llmath/tests/almeshcorpus.h b/indra/llmath/tests/almeshcorpus.h new file mode 100644 index 0000000000..2891133ba8 --- /dev/null +++ b/indra/llmath/tests/almeshcorpus.h @@ -0,0 +1,248 @@ +/** + * @file almeshcorpus.h + * @brief Real mesh assets from a viewer's cache, for the tests and + * benchmarks that want real content + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#ifndef AL_MESHCORPUS_H +#define AL_MESHCORPUS_H + +// The mesh repository's cache files under the directory AL_MESH_CORPUS_DIR +// names: a case that reads them skips where it is not set. A run takes a +// sample of AL_MESH_CORPUS_SAMPLE meshes, 2,000 unless set and every one +// for 0, in an order AL_MESH_CORPUS_SEED fixes, 1 unless set, the same on +// every machine. AL_MESH_CORPUS_SHARD, as k/n, keeps the k-th of every n +// files of that order, so that n runs share a corpus between them. Each LOD +// a mesh's file holds is handed over with its faces as they are decoded, +// before cacheOptimize, and whether the mesh is rigged. The assets are +// other people's: nothing from them goes into the tree, and what a case +// prints of them is counts and places in the sample. + +#include "llvolume.h" +#include "llvolumemgr.h" +#include "llsdserialize.h" +#include "lluuid.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ll_test +{ + struct VolumeProbe + { + // A LOD block's faces as the volume holds them, before cacheOptimize. + static bool decode(LLVolume& volume, const LLSD& mdl) { return volume.decodeVolumeFaces(mdl); } + }; +} + +namespace +{ + // One LOD of one mesh of the sample. + struct CorpusLOD + { + size_t mesh = 0; // the mesh's place in the sample + S32 lod = 0; // 0 for the lowest, 3 for the high + bool rigged = false; + LLPointer volume; + }; + + struct CorpusCounts + { + size_t files = 0; // cache files looked at + size_t meshes = 0; // of them, meshes with a header that reads + size_t not_mesh = 0; // without the mesh preamble: textures and the rest + size_t bad_header = 0; // with it, but no header that reads + size_t lods = 0; // LODs decoded + size_t lods_absent = 0; // LODs the header names that the file does not hold + size_t lods_bad = 0; // LODs that do not unzip or decode + }; + + class ALMeshCorpus + { + public: + static constexpr size_t DEFAULT_SAMPLE = 2000; + + // The corpus AL_MESH_CORPUS_DIR names, or none. + static std::optional fromEnvironment() + { + const char* dir = std::getenv("AL_MESH_CORPUS_DIR"); + if (!dir || !*dir || !std::filesystem::is_directory(dir)) + { + return std::nullopt; + } + ALMeshCorpus corpus; + corpus.mSample = number("AL_MESH_CORPUS_SAMPLE", DEFAULT_SAMPLE); + const U32 seed = (U32)number("AL_MESH_CORPUS_SEED", 1); + std::error_code ec; + for (std::filesystem::recursive_directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) + { + const std::string name = it->path().filename().string(); + if (it->is_regular_file(ec) && name.rfind("sl_cache_", 0) == 0 && name.size() > 17 && name.compare(name.size() - 8, 8, "_0.asset") == 0) + { + corpus.mFiles.push_back(it->path()); + } + } + // Sorted, then shuffled by the seed's own sequence, so that the + // order is the standard engine's and no library's shuffle. + std::sort(corpus.mFiles.begin(), corpus.mFiles.end()); + std::mt19937 rng(seed); + for (size_t i = corpus.mFiles.size(); i > 1; --i) + { + std::swap(corpus.mFiles[i - 1], corpus.mFiles[rng() % i]); + } + size_t shard = 0, shards = 1; + if (const char* value = std::getenv("AL_MESH_CORPUS_SHARD"); value && std::sscanf(value, "%zu/%zu", &shard, &shards) == 2 && shards > 1) + { + std::vector kept; + for (size_t i = shard; i < corpus.mFiles.size(); i += shards) + { + kept.push_back(corpus.mFiles[i]); + } + corpus.mFiles.swap(kept); + } + return corpus; + } + + // Each LOD of each mesh of the sample, in its order. + template + void forEachLOD(F&& visit) + { + for (const std::filesystem::path& path : mFiles) + { + if (mSample && mCounts.meshes >= mSample) + { + break; + } + readMesh(path, visit); + } + } + + const CorpusCounts& counts() const { return mCounts; } + + // The counts, as a case prints them. + std::string describe() const + { + return fmt::format("{} files: {} meshes, {} not meshes, {} without a header that reads; {} LODs decoded, {} named but absent, {} that " + "do not decode", + mCounts.files, mCounts.meshes, mCounts.not_mesh, mCounts.bad_header, mCounts.lods, mCounts.lods_absent, + mCounts.lods_bad); + } + + private: + static size_t number(const char* variable, size_t fallback) + { + const char* value = std::getenv(variable); + return value && *value ? (size_t)std::strtoull(value, nullptr, 10) : fallback; + } + + template + void readMesh(const std::filesystem::path& path, F& visit) + { + ++mCounts.files; + std::ifstream file(path, std::ios::binary); + std::vector data((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + + // The mesh repository's preamble: version, header bytes, and + // flags for what the file holds. + constexpr size_t PREAMBLE = sizeof(U32) * 3; + U32 preamble[3] = {}; + if (data.size() > PREAMBLE) + { + std::memcpy(preamble, data.data(), PREAMBLE); + } + if (preamble[0] != 1 || preamble[1] == 0 || PREAMBLE + preamble[1] > data.size()) + { + ++mCounts.not_mesh; + return; + } + + llssize header_bytes = preamble[1]; + char* header_start = strip_deprecated_header(reinterpret_cast(data.data() + PREAMBLE), header_bytes); + std::istringstream header_stream(std::string(header_start, (size_t)header_bytes)); + LLSD header; + if (LLSDSerialize::fromBinary(header, header_stream, header_bytes) <= 0 || !header.isMap() || header["version"].asInteger() > 999) + { + ++mCounts.bad_header; + return; + } + const size_t mesh = mCounts.meshes++; + const bool rigged = header.has("skin") && header["skin"]["size"].asInteger() > 0; + + const std::string name = path.filename().string(); + LLVolumeParams params; + params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + params.setSculptID(LLUUID(name.substr(9, name.size() - 17)), LL_SCULPT_TYPE_MESH); + + const char* lods[] = { "lowest_lod", "low_lod", "medium_lod", "high_lod" }; + for (S32 lod = 0; lod < 4; ++lod) + { + const S64 offset = header[lods[lod]]["offset"].asInteger(); + const S64 size = header[lods[lod]]["size"].asInteger(); + if (size <= 0) + { + continue; + } + // The file has room for every LOD, and its flags say which it + // holds, a bit a LOD; a file written before they did has + // none set, and holds what it has room for. + const S64 at = (S64)PREAMBLE + preamble[1] + offset; + const bool held = preamble[2] ? (preamble[2] & (1u << lod)) != 0 : true; + if (!held || offset < 0 || at + size > (S64)data.size()) + { + ++mCounts.lods_absent; + continue; + } + LLSD mdl; + CorpusLOD found; + found.mesh = mesh; + found.lod = lod; + found.rigged = rigged; + found.volume = new LLVolume(params, LLVolumeLODGroup::getVolumeScaleFromDetail(lod)); + if (LLUZipHelper::unzip_llsd(mdl, data.data() + at, (S32)size) != LLUZipHelper::ZR_OK || + !ll_test::VolumeProbe::decode(*found.volume, mdl)) + { + ++mCounts.lods_bad; + continue; + } + ++mCounts.lods; + visit(found); + } + } + + std::vector mFiles; + size_t mSample = DEFAULT_SAMPLE; + CorpusCounts mCounts; + }; +} + +#endif // AL_MESHCORPUS_H diff --git a/indra/llmath/tests/llmeshoptimizer_test.cpp b/indra/llmath/tests/llmeshoptimizer_test.cpp new file mode 100644 index 0000000000..b044a694aa --- /dev/null +++ b/indra/llmath/tests/llmeshoptimizer_test.cpp @@ -0,0 +1,87 @@ +/** + * @file llmeshoptimizer_test.cpp + * @brief meshoptimizer's scratch from the calling thread's arena. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llmeshoptimizer.h" +#include "alscratcharena.h" + +#include +#include + +#include +#include + +namespace +{ + // meshoptimizer on the arena while it lives, and on its own default, + // operator new and delete, after. + struct ArenaInstalled + { + static void* MESHOPTIMIZER_ALLOC_CALLCONV newBlock(size_t bytes) { return ::operator new(bytes); } + static void MESHOPTIMIZER_ALLOC_CALLCONV deleteBlock(void* block) { ::operator delete(block); } + + ArenaInstalled() { LLMeshOptimizer::initClass(); } + ~ArenaInstalled() { meshopt_setAllocator(newBlock, deleteBlock); } + }; + + // Vertex i is (3i, 3i + 1, 3i + 2) modulo 97 a coordinate: 97 vertices, + // each count times over. + void repeating(size_t count, std::vector& indices, std::vector& positions) + { + indices.resize(count); + std::iota(indices.begin(), indices.end(), 0); + positions.resize(count * 3); + for (size_t i = 0; i < positions.size(); ++i) + { + positions[i] = (F32)(i % 97); + } + } + + TEST_CASE("llmeshoptimizer: meshoptimizer takes its blocks from the arena", "[llmeshoptimizer]") + { + ArenaInstalled installed; + std::vector indices; + std::vector positions; + repeating(3000, indices, positions); + std::vector remap(indices.size()); + + ALScratchArena::resetCounters(); + CHECK(meshopt_generateVertexRemap(remap.data(), indices.data(), indices.size(), positions.data(), indices.size(), sizeof(F32) * 3) == 97); + ALScratchArena::Counters counters = ALScratchArena::counters(); + CHECK(counters.arena_blocks > 0); + CHECK(counters.heap_blocks == 0); + + // A hash table for more vertices than the arena holds comes from the + // heap, and the weld still comes out. + const size_t big = ALScratchArena::CAPACITY / sizeof(U32); + repeating(big, indices, positions); + remap.resize(big); + ALScratchArena::resetCounters(); + CHECK(meshopt_generateVertexRemap(remap.data(), indices.data(), indices.size(), positions.data(), big, sizeof(F32) * 3) == 97); + counters = ALScratchArena::counters(); + CHECK(counters.heap_blocks > 0); + CHECK(remap[97] == remap[0]); + } +} diff --git a/indra/llmath/tests/llvolume_bench.cpp b/indra/llmath/tests/llvolume_bench.cpp index 346d991744..16fb13d50d 100644 --- a/indra/llmath/tests/llvolume_bench.cpp +++ b/indra/llmath/tests/llvolume_bench.cpp @@ -2,7 +2,7 @@ * @file llvolume_bench.cpp * @brief What generating a prim or sculpt, its tangents, a flexible frame, * a mesh LOD's cacheOptimize and each index order cost, and how many - * blocks meshoptimizer allocates a call. + * blocks meshoptimizer takes a call from the arena and the heap. * * $LicenseInfo:firstyear=2026&license=viewerlgpl$ * Alchemy Viewer Source Code @@ -29,8 +29,8 @@ // face's tangents, one flexible frame, one LOD's cacheOptimize, one face's // index order. A row differs from the row beside it in one thing. The // figures that are not times, an index order's vertices transformed a -// triangle and meshoptimizer's allocations a call, are printed as a table -// by the case that measures them. +// triangle and meshoptimizer's blocks a call, are printed as a table by the +// case that measures them. // // The output is a table, not a verdict; a number is read against the row // next to it from the same run on the same quiet machine. @@ -38,7 +38,9 @@ #include "linden_common.h" #include "llvolume.h" +#include "alscratcharena.h" #include "llmath.h" +#include "llmeshoptimizer.h" #include "llquaternion.h" #include "m3math.h" #include "v2math.h" @@ -46,10 +48,13 @@ #include "llvector4a.h" #include "altestbenchmark.h" +#include "almeshcorpus.h" #include #include +#include +#include #include #include #include @@ -368,21 +373,6 @@ namespace } }; - // A face's tangents by Lengyel's generator, into a buffer of their - // own, allocated and cleared as createTangents did. - F32 lengyel(const LLVolumeFace& face) - { - LLVector4a* tangents = (LLVector4a*)ll_aligned_malloc_16(face.mNumVertices * sizeof(LLVector4a)); - for (S32 v = 0; v < face.mNumVertices; ++v) - { - tangents[v].clear(); - } - LLCalculateTangentArray(face.mNumVertices, face.mPositions, face.mNormals, face.mTexCoords, face.mNumIndices / 3, face.mIndices, tangents); - const F32 first = tangents[0][0]; - ll_aligned_free_16(tangents); - return first; - } - struct FaceClass { std::string name; @@ -512,58 +502,34 @@ namespace } // ------------------------------------------------------------------ - // meshoptimizer's allocations, counted through its allocator hook. + // meshoptimizer on the thread's arena, as the viewer runs it, while it + // lives, and on its own default, operator new and delete, after. - struct Allocations + struct ArenaInstalled { - static inline size_t blocks = 0; - static inline size_t bytes = 0; - - static void* MESHOPTIMIZER_ALLOC_CALLCONV allocate(size_t size) - { - ++blocks; - bytes += size; - return ::operator new(size); - } - - static void MESHOPTIMIZER_ALLOC_CALLCONV deallocate(void* p) - { - ::operator delete(p); - } + static void* MESHOPTIMIZER_ALLOC_CALLCONV newBlock(size_t bytes) { return ::operator new(bytes); } + static void MESHOPTIMIZER_ALLOC_CALLCONV deleteBlock(void* block) { ::operator delete(block); } - // meshoptimizer's own default: operator new and operator delete. - static void* MESHOPTIMIZER_ALLOC_CALLCONV defaultAllocate(size_t size) - { - return ::operator new(size); - } - - static void MESHOPTIMIZER_ALLOC_CALLCONV defaultDeallocate(void* p) - { - ::operator delete(p); - } + ArenaInstalled() { LLMeshOptimizer::initClass(); } + ~ArenaInstalled() { meshopt_setAllocator(newBlock, deleteBlock); } }; - // Counts while it lives, and puts meshoptimizer's default back however - // the case ends. + // The blocks a call takes from the arena and from the heap, counted by + // the arena, and the most of the arena it holds at once. struct CountAllocations { - CountAllocations() - { - meshopt_setAllocator(Allocations::allocate, Allocations::deallocate); - } - - ~CountAllocations() - { - meshopt_setAllocator(Allocations::defaultAllocate, Allocations::defaultDeallocate); - } + ArenaInstalled installed; + size_t arena_blocks = 0; template void row(const std::string& name, F&& call) { - Allocations::blocks = 0; - Allocations::bytes = 0; + ALScratchArena::resetCounters(); call(); - std::printf(" %-72s %4zu blocks %9zu bytes\n", name.c_str(), Allocations::blocks, Allocations::bytes); + const ALScratchArena::Counters counters = ALScratchArena::counters(); + arena_blocks += counters.arena_blocks; + std::printf(" %-72s %4zu arena %4zu heap %9zu bytes at most\n", name.c_str(), counters.arena_blocks, counters.heap_blocks, + counters.peak); } }; @@ -621,12 +587,11 @@ namespace } // createTangents sums meshoptimizer's default weighting a vertex at a - // time, over the grid on a side and over the indices elsewhere; Lengyel - // is the generator a face cacheOptimize has taken keeps, and the one - // createTangents was. meshoptimizer's default mode weights by angle - // times the edges' lengths a corner at a time, its compatible mode by - // angle alone, as MikkTSpace does. Each row makes one face's tangents a - // vertex, the allocation included where the generator makes one. + // time, over the grid on a side and over the indices elsewhere. + // meshoptimizer's default mode weights by angle times the edges' + // lengths a corner at a time, its compatible mode by angle alone, as + // MikkTSpace does. Each row makes one face's tangents a vertex, the + // allocation included where the generator makes one. TEST_CASE("llvolume_bench: one face's tangents from each generator", "[llvolume_bench][!benchmark]") { ll_test::skipUnlessOptimised(); @@ -640,10 +605,6 @@ namespace face.createTangents(); return face.mTangents[0][0]; }; - BENCHMARK(fmt::format("Lengyel, {}", face_class.name)) - { - return lengyel(face); - }; TangentInputs inputs(face); BENCHMARK(fmt::format("meshoptimizer default, {}", face_class.name)) { @@ -665,10 +626,6 @@ namespace face.createTangents(); return face.mTangents[0][0]; }; - BENCHMARK(fmt::format("Lengyel, {}", name)) - { - return lengyel(face); - }; TangentInputs inputs(face); BENCHMARK(fmt::format("meshoptimizer default, {}", name)) { @@ -713,11 +670,13 @@ namespace } // cacheOptimize as a mesh thread runs it on each decoded LOD: tangents, - // the weld, and the cache order. Each call takes a fresh copy of the - // decoded face, made outside the measurement. + // the weld, the cache order and the fetch order, its scratch and + // meshoptimizer's from the thread's arena. Each call takes a fresh copy + // of the decoded face, made outside the measurement. TEST_CASE("llvolume_bench: cacheOptimize over the mesh corpus", "[llvolume_bench][!benchmark]") { ll_test::skipUnlessOptimised(); + ArenaInstalled installed; std::vector all; for (const CorpusMesh& mesh : corpus()) { @@ -746,6 +705,71 @@ namespace }; } + // cacheOptimize over the real mesh corpus (almeshcorpus.h), where + // AL_MESH_CORPUS_DIR names one: every face of every LOD of the sample, + // one call each on a fresh copy, as a mesh thread makes it. Printed as + // a table: the time a face and a vertex, the arena's high-water marks + // against its size, and the vertex cache and fetch figures it leaves. + TEST_CASE("llvolume_bench: cacheOptimize over the real mesh corpus", "[llvolume_bench][!benchmark]") + { + ll_test::skipUnlessOptimised(); + std::optional real = ALMeshCorpus::fromEnvironment(); + if (!real) + { + SKIP("AL_MESH_CORPUS_DIR names no mesh cache"); + } + ArenaInstalled installed; + std::vector face_ns, peaks, acmr, overfetch; + F64 total_ns = 0; + size_t vertices = 0, over_arena = 0; + constexpr size_t VERTEX_BYTES = 2 * sizeof(LLVector4a) + sizeof(LLVector2); + real->forEachLOD( + [&](const CorpusLOD& lod) + { + for (S32 f = 0; f < lod.volume->getNumVolumeFaces(); ++f) + { + const LLVolumeFace& decoded = lod.volume->getVolumeFace(f); + if (decoded.mNumVertices <= 0 || decoded.mNumIndices <= 0) + { + continue; + } + LLVolumeFace face = decoded; + ALScratchArena::resetCounters(); + const auto start = std::chrono::steady_clock::now(); + const bool optimized = face.cacheOptimize(true); + const F64 ns = (F64)std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + if (!optimized) + { + continue; + } + const ALScratchArena::Counters counters = ALScratchArena::counters(); + face_ns.push_back(ns); + total_ns += ns; + vertices += decoded.mNumVertices; + peaks.push_back((F64)counters.peak); + over_arena += counters.heap_blocks > 0; + const std::vector indices(face.mIndices, face.mIndices + face.mNumIndices); + acmr.push_back(meshopt_analyzeVertexCache(indices.data(), indices.size(), face.mNumVertices, 16, 0, 0).acmr); + overfetch.push_back((F64)meshopt_analyzeVertexFetch(indices.data(), indices.size(), face.mNumVertices, VERTEX_BYTES).bytes_fetched / + ((F64)face.mNumVertices * VERTEX_BYTES)); + } + }); + REQUIRE(!face_ns.empty()); + auto quantile = [](std::vector values, F64 q) + { + std::sort(values.begin(), values.end()); + return values[std::min(values.size() - 1, (size_t)(q * (F64)(values.size() - 1) + 0.5))]; + }; + std::printf("cacheOptimize over the real mesh corpus: %s\n", real->describe().c_str()); + std::printf(" %zu faces, %zu decoded vertices: %.0f ns a face (median %.0f, 99th percentile %.0f), %.1f ns a decoded vertex\n", face_ns.size(), + vertices, total_ns / face_ns.size(), quantile(face_ns, 0.5), quantile(face_ns, 0.99), total_ns / vertices); + std::printf(" arena high-water mark: median %.0f, 99th percentile %.0f, most %.0f bytes of %zu; %zu faces larger than the arena\n", + quantile(peaks, 0.5), quantile(peaks, 0.99), quantile(peaks, 1.0), ALScratchArena::CAPACITY, over_arena); + std::printf(" vertices transformed a triangle: median %.3f, 90th percentile %.3f; overfetch: median %.4f, 90th percentile %.4f\n", + quantile(acmr, 0.5), quantile(acmr, 0.9), quantile(overfetch, 0.5), quantile(overfetch, 0.9)); + std::fflush(stdout); + } + // Each grid's index order three ways: createSide's rows, bands of seven // columns made the same way, and meshoptimizer's cache order over the // rows. The vertices each transforms a triangle in a 16-entry cache are @@ -792,13 +816,12 @@ namespace } } - // The blocks each meshoptimizer call takes from its allocator, counted - // through meshopt_setAllocator, and what cacheOptimize's calls take - // together. + // The blocks each meshoptimizer call takes, and what a cacheOptimize + // takes with its own scratch, from the arena and from the heap. TEST_CASE("llvolume_bench: meshoptimizer's allocations a call", "[llvolume_bench][!benchmark]") { ll_test::skipUnlessOptimised(); - std::printf("meshoptimizer allocations a call\n"); + std::printf("meshoptimizer blocks a call\n"); size_t blocks = 0; { CountAllocations count; @@ -823,8 +846,8 @@ namespace { LLVolumeFace face = mesh.face(); count.row(fmt::format("cacheOptimize, {}, {} vertices", mesh.name, face.mNumVertices), [&] { face.cacheOptimize(true); }); - blocks += Allocations::blocks; } + blocks = count.arena_blocks; } std::fflush(stdout); CHECK(blocks > 0); diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 207c9567a8..cc6a1da6fe 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -50,7 +50,11 @@ #include "v3math.h" #include "v4math.h" #include "llvector4a.h" +#include "alscratcharena.h" #include "alsimdkernels.h" +#include "llmeshoptimizer.h" +#include "almeshcorpus.h" +#include "altestfatal.h" #include #include @@ -1978,9 +1982,95 @@ namespace return mesh; } + // A curved grid whose vertices are each held twice, the same in every + // attribute: each quad's first triangle takes the first copies and its + // second the others. + CorpusMesh duplicatedGrid() + { + CorpusMesh mesh; + mesh.name = "grid of vertices held twice"; + constexpr S32 size = 6; + for (S32 copy = 0; copy < 2; ++copy) + { + for (S32 r = 0; r <= size; ++r) + { + for (S32 c = 0; c <= size; ++c) + { + const F32 x = (F32)c / size - 0.5f; + mesh.positions.emplace_back(x, (F32)r / size - 0.5f, 0.25f * x * x); + LLVector3 n(-0.5f * x, 0.f, 1.f); + n.normalize(); + mesh.normals.push_back(n); + mesh.uvs.emplace_back((F32)c / size, (F32)r / size); + } + } + } + const U16 copy = (U16)((size + 1) * (size + 1)); + for (S32 r = 0; r < size; ++r) + { + for (S32 c = 0; c < size; ++c) + { + const U16 a = (U16)(c + r * (size + 1)); + mesh.indices.insert(mesh.indices.end(), { a, (U16)(a + 1), (U16)(a + size + 2) }); + mesh.indices.insert(mesh.indices.end(), { (U16)(a + copy), (U16)(a + size + 2 + copy), (U16)(a + size + 1 + copy) }); + } + } + return mesh; + } + std::vector corpus() { - return { uvSphere(24, 12), splitCube(), mirroredStrip(), riggedSphere(), degenerateGrid(), uvSphere(64, 32) }; + return { uvSphere(24, 12), splitCube(), mirroredStrip(), riggedSphere(), degenerateGrid(), duplicatedGrid(), uvSphere(64, 32) }; + } + + // A strip of 32,760 columns by two rows whose U runs back and forth, a + // mirror seam at every column, gently curved: each vertex's corners take + // two tangents, which would split it into more vertices than 16-bit + // indices reach. Its last 16 vertices are copies of others, which its + // triangles take in place of them. + constexpr S32 ZIGZAG_COPIES = 16; + CorpusMesh zigzagStrip() + { + CorpusMesh mesh; + mesh.name = "strip of 32,760 mirrored columns"; + constexpr S32 columns = 32760; + for (S32 r = 0; r < 2; ++r) + { + for (S32 c = 0; c < columns; ++c) + { + const F32 x = c * 0.01f; + mesh.positions.emplace_back(x, (F32)r, 0.1f * sinf(x)); + LLVector3 n(-0.1f * cosf(x), 0.f, 1.f); + n.normalize(); + mesh.normals.push_back(n); + mesh.uvs.emplace_back((F32)(c % 2), (F32)r); + } + } + for (S32 c = 0; c + 1 < columns; ++c) + { + const U16 a = (U16)c; + const U16 b = (U16)(c + 1); + const U16 d = (U16)(c + columns); + const U16 e = (U16)(d + 1); + mesh.indices.insert(mesh.indices.end(), { a, b, e, a, e, d }); + } + for (S32 k = 0; k < ZIGZAG_COPIES; ++k) + { + const U16 original = (U16)(k * 997 + 3); + const U16 copy = (U16)mesh.positions.size(); + mesh.positions.push_back(mesh.positions[original]); + mesh.normals.push_back(mesh.normals[original]); + mesh.uvs.push_back(mesh.uvs[original]); + for (size_t i = (size_t)k * 6; i < mesh.indices.size(); ++i) + { + if (mesh.indices[i] == original) + { + mesh.indices[i] = copy; + break; + } + } + } + return mesh; } // A triangle by its corners' positions, rotated to start at the least, @@ -2009,10 +2099,60 @@ namespace return { corners[start], corners[(start + 1) % 3], corners[(start + 2) % 3] }; } + LLVector3 unitOrZero(LLVector3 v) + { + if (v.lengthSquared() > 0.f) + { + v.normalize(); + } + return v; + } + + // Whether an optimized corner keeps a decoded one's normal at unit + // length, UV and weights. + bool sameCorner(const LLVolumeFace& before, U16 i, const LLVolumeFace& after, U16 o) + { + if ((v3(after.mNormals[o]) - unitOrZero(v3(before.mNormals[i]))).length() > 1e-5f || !(after.mTexCoords[o] == before.mTexCoords[i])) + { + return false; + } + return !before.mWeights || + (after.mWeights && v3(after.mWeights[o]) == v3(before.mWeights[i]) && after.mWeights[o][3] == before.mWeights[i][3]); + } + + // Whether no two vertices of a face are the same in every attribute, + // the tangent among them where with_tangent says. + bool verticesDistinct(const LLVolumeFace& face, bool with_tangent) + { + using Key = std::array; + std::vector keys(face.mNumVertices); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + Key& key = keys[v]; + key.fill(0); + std::memcpy(&key[0], face.mPositions[v].getF32ptr(), 3 * sizeof(F32)); + std::memcpy(&key[3], face.mNormals[v].getF32ptr(), 3 * sizeof(F32)); + std::memcpy(&key[6], face.mTexCoords[v].mV, 2 * sizeof(F32)); + if (with_tangent && face.mTangents) + { + std::memcpy(&key[8], face.mTangents[v].getF32ptr(), 4 * sizeof(F32)); + } + if (face.mWeights) + { + std::memcpy(&key[12], face.mWeights[v].getF32ptr(), 4 * sizeof(F32)); + } + } + std::sort(keys.begin(), keys.end()); + return std::adjacent_find(keys.begin(), keys.end()) == keys.end(); + } + // cacheOptimize keeps every triangle, and every corner's position, - // normal, UV and weights, while it welds and reorders; it gives every - // vertex a unit tangent orthogonal to its normal, whose w follows the - // UV winding of each triangle the vertex is in. + // normal at unit length, UV and weights, while it welds and reorders; + // it gives every vertex a unit tangent orthogonal to its normal, whose + // w follows the UV winding of each triangle the vertex is in, as + // MikkTSpace's handedness does. A face decoded with a normal of no + // length keeps it so. Triangles are matched by their corners' + // positions, and among those of the same positions by the rest. void checkOptimized(const LLVolumeFace& before, const LLVolumeFace& after, Report& report, const std::string& where) { if (after.mNumIndices != before.mNumIndices) @@ -2055,6 +2195,13 @@ namespace return; } + // A face too large for its weld, with one of each vertex, gives each + // its first corner's tangent, so that a vertex of triangles winding + // both ways, or whose first triangle's UVs have no winding, follows + // only some of them: all but a hundredth of its vertices follow one. + const bool unsplit = before.mNumIndices >= 65535 && verticesDistinct(after, false); + std::vector windings(after.mNumVertices, 0); + for (S32 t = 0; t < (S32)out.size(); ++t) { if (out[t].degenerate) @@ -2063,38 +2210,50 @@ namespace } S32 out_first = 0; const TriangleKey key = keyOf(after, t, &out_first); - const S32 source = by_key.find(key)->second; - S32 in_first = 0; - keyOf(before, source, &in_first); - const LLVector2 uv0 = after.mTexCoords[after.mIndices[t * 3]]; - const LLVector2 uv1 = after.mTexCoords[after.mIndices[t * 3 + 1]]; - const LLVector2 uv2 = after.mTexCoords[after.mIndices[t * 3 + 2]]; - const F32 uv_area = (uv1.mV[0] - uv0.mV[0]) * (uv2.mV[1] - uv0.mV[1]) - (uv2.mV[0] - uv0.mV[0]) * (uv1.mV[1] - uv0.mV[1]); - for (S32 k = 0; k < 3; ++k) + bool kept = false; + for (auto [it, end] = by_key.equal_range(key); it != end && !kept; ++it) { - const U16 o = after.mIndices[t * 3 + (out_first + k) % 3]; - const U16 i = before.mIndices[source * 3 + (in_first + k) % 3]; - if ((v3(after.mNormals[o]) - v3(before.mNormals[i])).length() > 1e-5f || !(after.mTexCoords[o] == before.mTexCoords[i])) - { - report.fail(fmt::format("{}: triangle {} corner {} lost its normal or UV", where, t, k)); - return; - } - if (before.mWeights && !(after.mWeights && v3(after.mWeights[o]) == v3(before.mWeights[i]) && after.mWeights[o][3] == before.mWeights[i][3])) + S32 in_first = 0; + keyOf(before, it->second, &in_first); + kept = true; + for (S32 k = 0; k < 3 && kept; ++k) { - report.fail(fmt::format("{}: triangle {} corner {} lost its weights", where, t, k)); - return; + kept = sameCorner(before, before.mIndices[it->second * 3 + (in_first + k) % 3], after, after.mIndices[t * 3 + (out_first + k) % 3]); } } + if (!kept) + { + report.fail(fmt::format("{}: triangle {} lost a corner's normal, UV or weights", where, t)); + return; + } + const LLVector2 uv0 = after.mTexCoords[after.mIndices[t * 3]]; + const LLVector2 uv1 = after.mTexCoords[after.mIndices[t * 3 + 1]]; + const LLVector2 uv2 = after.mTexCoords[after.mIndices[t * 3 + 2]]; if (!after.mTangents) { report.fail(fmt::format("{}: no tangents", where)); return; } - const F32 geometric = out[t].normal * v3(after.mNormals[after.mIndices[t * 3]]); - const F32 expected_w = (uv_area > 0.f) == (geometric > 0.f) ? 1.f : -1.f; - for (S32 k = 0; k < 3 && fabsf(uv_area) > 1e-6f; ++k) + // The UVs' winding from their differences at a float, as + // meshoptimizer takes them, and their products exact. A triangle + // whose UVs are all but in a line, against their size, wind by + // less than MikkTSpace's least area, or have products past a + // float's range, has no winding to follow. + const F32 d1u = uv1.mV[0] - uv0.mV[0], d1v = uv1.mV[1] - uv0.mV[1]; + const F32 d2u = uv2.mV[0] - uv0.mV[0], d2v = uv2.mV[1] - uv0.mV[1]; + const F64 along = (F64)d1u * d2v, across = (F64)d2u * d1v; + const F64 uv_area = along - across; + const F64 edges = sqrt(((F64)d1u * d1u + (F64)d1v * d1v) * ((F64)d2u * d2u + (F64)d2v * d2v)); + const F32 expected_w = uv_area > 0. ? 1.f : -1.f; + const bool winds = fabs(along) <= FLT_MAX && fabs(across) <= FLT_MAX && fabs(uv_area) > llmax(1e-5 * edges, (F64)FLT_MIN); + for (S32 k = 0; k < 3 && winds; ++k) { const U16 o = after.mIndices[t * 3 + k]; + if (unsplit) + { + windings[o] |= expected_w > 0.f ? 1 : 2; + continue; + } if (after.mTangents[o][3] != expected_w) { report.fail(fmt::format("{}: triangle {}'s UVs wind for w = {} but vertex {} has {}", where, t, expected_w, o, after.mTangents[o][3])); @@ -2103,13 +2262,32 @@ namespace } } + if (unsplit) + { + S32 against = 0; + for (S32 v = 0; v < after.mNumVertices; ++v) + { + against += windings[v] && !(windings[v] & (after.mTangents[v][3] > 0.f ? 1 : 2)); + } + if (against * 100 > after.mNumVertices) + { + report.fail(fmt::format("{}: {} of {} vertices have a w none of their triangles' UVs wind for", where, against, after.mNumVertices)); + return; + } + } + + bool zero_in = false; + for (S32 v = 0; v < before.mNumVertices; ++v) + { + zero_in |= v3(before.mNormals[v]).lengthSquared() == 0.f; + } for (S32 v = 0; v < after.mNumVertices; ++v) { const LLVector3 n = v3(after.mNormals[v]); const LLVector3 t = after.mTangents ? v3(after.mTangents[v]) : LLVector3::zero; const F32 w = after.mTangents ? after.mTangents[v][3] : 0.f; - if (!(fabsf(n.length() - 1.f) <= UNIT_TOLERANCE) || !(fabsf(t.length() - 1.f) <= UNIT_TOLERANCE) || !(w == 1.f || w == -1.f) || - !(fabsf(n * t) <= 2e-3f)) + const bool unit_n = fabsf(n.length() - 1.f) <= UNIT_TOLERANCE || (zero_in && n.lengthSquared() == 0.f); + if (!unit_n || !(fabsf(t.length() - 1.f) <= UNIT_TOLERANCE) || !(w == 1.f || w == -1.f) || !(fabsf(n * t) <= 2e-3f)) { report.fail(fmt::format("{}: vertex {} has normal length {}, tangent length {}, w {} and n.t {}", where, v, n.length(), t.length(), w, n * t)); return; @@ -2117,6 +2295,142 @@ namespace } } + // The tangent cacheOptimize gives each corner of a decoded face: + // meshoptimizer's compatible one over the face as it was authored, its + // positions scaled out of the unit cube and its unit normals by the + // inverse, taken back into the face's frame as a direction along its + // surface. A corner that gives no direction is (0, 0, 0, w). + std::vector expectedTangents(const LLVolumeFace& face) + { + const LLVector3& scale = face.mNormalizedScale; + const LLVector3 inv_scale(1.f / scale.mV[0], 1.f / scale.mV[1], 1.f / scale.mV[2]); + std::vector positions(face.mNumVertices), normals(face.mNumVertices); + for (S32 v = 0; v < face.mNumVertices; ++v) + { + LLVector3 p = v3(face.mPositions[v]); + p.scaleVec(scale); + positions[v].load3(p.mV); + LLVector3 n = v3(face.mNormals[v]); + n.normalize(); + n.scaleVec(inv_scale); + n.normalize(); + normals[v].load3(n.mV); + } + const std::vector indices(face.mIndices, face.mIndices + face.mNumIndices); + std::vector corners(indices.size() * 4); + meshopt_generateTangents(corners.data(), indices.data(), indices.size(), positions[0].getF32ptr(), positions.size(), sizeof(LLVector4a), + normals[0].getF32ptr(), sizeof(LLVector4a), face.mTexCoords[0].mV, sizeof(LLVector2), + meshopt_TangentCompatible | meshopt_TangentZeroFallback); + std::vector out(indices.size()); + for (size_t c = 0; c < indices.size(); ++c) + { + LLVector3 t(&corners[c * 4]); + t.scaleVec(inv_scale); + if (t.lengthSquared() > 0.f) + { + t.normalize(); + } + out[c].set(t.mV[0], t.mV[1], t.mV[2], corners[c * 4 + 3]); + } + return out; + } + + // The optimized face's tangents against expectedTangents, corner for + // corner over its real triangles, each matched to the decoded triangle + // of the same corners. A corner that gives no direction takes one of + // unit length in the plane of its normal. + void checkTangentsExpected(const LLVolumeFace& before, const LLVolumeFace& after, Report& report, const std::string& where) + { + const std::vector expected = expectedTangents(before); + const std::vector in = trianglesOf(before); + const std::vector out = trianglesOf(after); + std::multimap by_key; + for (S32 t = 0; t < (S32)in.size(); ++t) + { + if (!in[t].degenerate) + { + by_key.emplace(keyOf(before, t), t); + } + } + F32 worst = 0.f; + for (S32 t = 0; t < (S32)out.size(); ++t) + { + if (out[t].degenerate) + { + continue; + } + S32 out_first = 0; + const auto found = by_key.find(keyOf(after, t, &out_first)); + if (found == by_key.end()) + { + report.fail(fmt::format("{}: triangle {} is no triangle of the decoded face", where, t)); + return; + } + S32 in_first = 0; + keyOf(before, found->second, &in_first); + for (S32 k = 0; k < 3; ++k) + { + const U16 o = after.mIndices[t * 3 + (out_first + k) % 3]; + const LLVector4a& want = expected[found->second * 3 + (in_first + k) % 3]; + const LLVector3 got = v3(after.mTangents[o]); + if (after.mTangents[o][3] != want[3]) + { + report.fail(fmt::format("{}: vertex {} has w {} where its corner's is {}", where, o, after.mTangents[o][3], want[3])); + return; + } + if (v3(want).lengthSquared() == 0.f) + { + if (!(fabsf(got.length() - 1.f) <= UNIT_TOLERANCE) || !(fabsf(got * v3(after.mNormals[o])) <= 2e-3f)) + { + report.fail(fmt::format("{}: vertex {}, of a corner with no direction, has tangent length {} and n.t {}", where, o, + got.length(), got * v3(after.mNormals[o]))); + return; + } + continue; + } + // By the sine as well as the cosine: acos alone cannot tell + // apart directions a float's rounding apart. + const LLVector3 w3 = v3(want); + worst = llmax(worst, atan2f((got % w3).length(), got * w3) * RAD_TO_DEG); + } + } + if (!(worst <= 0.01f)) + { + report.fail(fmt::format("{}: a tangent is {:.4f} degrees from meshoptimizer's", where, worst)); + } + } + + // No two vertices of an optimized face are the same in every attribute, + // so a weld of position, normal, UV, weights and tangent together would + // leave as many. + void checkWeldedOnce(const LLVolumeFace& face, Report& report, const std::string& where) + { + if (!verticesDistinct(face, true)) + { + report.fail(fmt::format("{}: two vertices are the same in every attribute", where)); + } + } + + // A face as a mesh LOD whose authored shape is the corpus mesh's + // positions times scale, with that shape's normals. + LLVolumeFace scaledFace(const CorpusMesh& mesh, const LLVector3& scale) + { + LLVolumeFace face = mesh.face(); + face.mNormalizedScale = scale; + return face; + } + + // meshoptimizer on the thread's arena while it lives, and on its own + // default, operator new and delete, after. + struct ArenaInstalled + { + static void* MESHOPTIMIZER_ALLOC_CALLCONV newBlock(size_t bytes) { return ::operator new(bytes); } + static void MESHOPTIMIZER_ALLOC_CALLCONV deleteBlock(void* block) { ::operator delete(block); } + + ArenaInstalled() { LLMeshOptimizer::initClass(); } + ~ArenaInstalled() { meshopt_setAllocator(newBlock, deleteBlock); } + }; + // ------------------------------------------------------------------ // Vertex cache and fetch figures, as meshoptimizer models them: a // 16-entry FIFO, and fetches of whole 64-byte lines. @@ -2564,6 +2878,7 @@ namespace REQUIRE(after.cacheOptimize(true)); Report report; checkOptimized(before, after, report, mesh.name); + checkWeldedOnce(after, report, mesh.name); INFO(report.text()); CHECK(report.failures == 0); } @@ -2579,6 +2894,147 @@ namespace CHECK(face.mNumVertices == (S32)mesh.positions.size() + 7); } + // Each mesh scaled unevenly out of its unit cube, as most authored + // meshes are: its tangents are taken in the authored shape and stored + // as directions in the face's frame, where they lie in the plane of the + // face's own normals. + std::vector unevenScales() + { + return { LLVector3(2.f, 0.5f, 1.f), LLVector3(0.25f, 3.f, 1.5f), LLVector3(1.f, 1.f, 4.f) }; + } + + std::string scaledName(const CorpusMesh& mesh, const LLVector3& scale) + { + return fmt::format("{} scaled {} by {} by {}", mesh.name, scale.mV[0], scale.mV[1], scale.mV[2]); + } + + TEST_CASE("llvolume: cacheOptimize keeps every triangle and corner of a mesh scaled unevenly", "[llvolume]") + { + for (const CorpusMesh& mesh : corpus()) + { + for (const LLVector3& scale : unevenScales()) + { + const LLVolumeFace before = scaledFace(mesh, scale); + LLVolumeFace after = before; + REQUIRE(after.cacheOptimize(true)); + Report report; + checkOptimized(before, after, report, scaledName(mesh, scale)); + INFO(report.text()); + CHECK(report.failures == 0); + } + } + } + + TEST_CASE("llvolume: cacheOptimize's tangents are meshoptimizer's MikkTSpace-compatible ones", "[llvolume]") + { + for (const CorpusMesh& mesh : corpus()) + { + std::vector scales = unevenScales(); + scales.emplace_back(1.f, 1.f, 1.f); + for (const LLVector3& scale : scales) + { + const LLVolumeFace before = scaledFace(mesh, scale); + LLVolumeFace after = before; + REQUIRE(after.cacheOptimize(true)); + Report report; + checkTangentsExpected(before, after, report, scaledName(mesh, scale)); + INFO(report.text()); + CHECK(report.failures == 0); + } + } + } + + // A weld of every attribute of every corner, tangent included, as + // cacheOptimize made before it welded each vertex by its copies and its + // tangents: the vertices that gave the corpus. A vertex the face holds + // twice is one either way. + TEST_CASE("llvolume: cacheOptimize makes no more vertices than a weld of every attribute", "[llvolume]") + { + struct Count + { + const char* name; + S32 vertices; + }; + const Count counts[] = { + { "UV sphere 24 by 12", 323 }, { "cube with split normals", 24 }, { "strip with mirrored UVs", 98 }, + { "rigged UV sphere", 151 }, { "grid with degenerate triangles", 58 }, { "grid of vertices held twice", 49 }, + { "UV sphere 64 by 32", 2143 }, + }; + for (const CorpusMesh& mesh : corpus()) + { + LLVolumeFace face = mesh.face(); + REQUIRE(face.cacheOptimize(true)); + const Count* count = std::find_if(std::begin(counts), std::end(counts), [&](const Count& c) { return mesh.name == c.name; }); + REQUIRE(count != std::end(counts)); + INFO(fmt::format("{}: {} vertices", mesh.name, face.mNumVertices)); + CHECK(face.mNumVertices <= count->vertices); + } + } + + // Too many vertices once split for 16-bit indices: the face keeps one of + // each vertex it has, its copies welded, each with the tangent of its + // first corner, and every triangle. Its scratch is larger than the + // arena, which hands its largest blocks to the heap. + TEST_CASE("llvolume: a face too large for its weld keeps one of each vertex and gets a tangent at each", "[llvolume]") + { + ArenaInstalled installed; + const CorpusMesh mesh = zigzagStrip(); + const LLVolumeFace before = mesh.face(); + LLVolumeFace after = before; + ALScratchArena::resetCounters(); + REQUIRE(after.cacheOptimize(true)); + const ALScratchArena::Counters counters = ALScratchArena::counters(); + CHECK(counters.heap_blocks > 0); + + CHECK(after.mNumVertices == before.mNumVertices - ZIGZAG_COPIES); + REQUIRE(after.mNumIndices == before.mNumIndices); + REQUIRE(after.mTangents); + std::vector in_keys, out_keys; + for (S32 t = 0; t < before.mNumIndices / 3; ++t) + { + in_keys.push_back(keyOf(before, t)); + out_keys.push_back(keyOf(after, t)); + } + std::sort(in_keys.begin(), in_keys.end()); + std::sort(out_keys.begin(), out_keys.end()); + CHECK(in_keys == out_keys); + + Report report; + for (S32 v = 0; v < after.mNumVertices; ++v) + { + const LLVector3 n = v3(after.mNormals[v]); + const LLVector3 t = v3(after.mTangents[v]); + const F32 w = after.mTangents[v][3]; + if (!(fabsf(t.length() - 1.f) <= UNIT_TOLERANCE) || !(fabsf(n * t) <= 2e-3f) || !(w == 1.f || w == -1.f)) + { + report.fail(fmt::format("vertex {} has tangent length {}, n.t {} and w {}", v, t.length(), n * t, w)); + } + } + checkWeldedOnce(after, report, mesh.name); + INFO(report.text()); + CHECK(report.failures == 0); + } + + // The corpus, the largest of it 8,385 vertices, through cacheOptimize + // with meshoptimizer on the arena: everything from the arena. + TEST_CASE("llvolume: cacheOptimize takes nothing from the heap for a face its arena holds", "[llvolume]") + { + ArenaInstalled installed; + std::vector meshes = corpus(); + meshes.push_back(uvSphere(128, 64)); + for (const CorpusMesh& mesh : meshes) + { + LLVolumeFace face = mesh.face(); + ALScratchArena::resetCounters(); + REQUIRE(face.cacheOptimize(true)); + const ALScratchArena::Counters counters = ALScratchArena::counters(); + INFO(fmt::format("{}: {} blocks from the arena, {} from the heap, {} bytes at most", mesh.name, counters.arena_blocks, + counters.heap_blocks, counters.peak)); + CHECK(counters.arena_blocks > 0); + CHECK(counters.heap_blocks == 0); + } + } + // Today's figures for each class, so that no later change orders a // class's indices or vertices worse than it is ordered now. TEST_CASE("llvolume: vertex cache and fetch figures stay within today's bounds", "[llvolume]") @@ -2636,10 +3092,14 @@ namespace { "cube with split normals", 2.0000, 1.0000 }, { "strip with mirrored UVs", 0.7361, 1.0122 }, { "rigged UV sphere", 0.7366, 1.0066 }, - { "grid with degenerate triangles", 0.8158, 1.0207 }, + { "grid with degenerate triangles", 0.7368, 1.0154 }, + { "grid of vertices held twice", 0.7361, 1.0122 }, { "UV sphere 64 by 32", 0.6557, 1.0005 }, + { "UV sphere 128 by 64", 0.6425, 1.0001 }, }; - for (const CorpusMesh& mesh : corpus()) + std::vector meshes = corpus(); + meshes.push_back(uvSphere(128, 64)); + for (const CorpusMesh& mesh : meshes) { LLVolumeFace face = mesh.face(); REQUIRE(face.cacheOptimize(true)); @@ -2802,6 +3262,71 @@ namespace } } + // The real mesh corpus (almeshcorpus.h), where AL_MESH_CORPUS_DIR names + // one: every face of every LOD of the sample through cacheOptimize, with + // meshoptimizer on the arena, keeps every triangle and corner and makes + // no vertex twice, and nothing it is handed fails but what its guards + // refuse, which are counted by what they refused. + TEST_CASE("llvolume: cacheOptimize keeps every triangle and corner of the real mesh corpus", "[llvolume][corpus][.]") + { + std::optional real = ALMeshCorpus::fromEnvironment(); + if (!real) + { + SKIP("AL_MESH_CORPUS_DIR names no mesh cache"); + } + ArenaInstalled installed; + Report report; + size_t faces = 0, rigged = 0, past_vertices = 0, no_scale = 0, refused = 0, fatal = 0, over_arena = 0; + real->forEachLOD( + [&](const CorpusLOD& lod) + { + for (S32 f = 0; f < lod.volume->getNumVolumeFaces(); ++f) + { + const LLVolumeFace& before = lod.volume->getVolumeFace(f); + if (before.mNumVertices <= 0 || before.mNumIndices <= 0) + { + continue; + } + ++faces; + rigged += before.mWeights != nullptr; + const std::string where = fmt::format("mesh {} LOD {} face {}", lod.mesh, lod.lod, f); + LLVolumeFace after = before; + ALScratchArena::resetCounters(); + bool optimized = false; + try + { + optimized = after.cacheOptimize(true); + } + catch (const ll_test::FatalError& error) + { + ++fatal; + report.fail(fmt::format("{}: {}", where, error.what())); + continue; + } + over_arena += ALScratchArena::counters().heap_blocks > 0; + if (!optimized) + { + const bool past = std::any_of(before.mIndices, before.mIndices + before.mNumIndices, + [&](U16 index) { return index >= before.mNumVertices; }); + const LLVector3& scale = before.mNormalizedScale; + const bool flat = scale.mV[0] == 0.f || scale.mV[1] == 0.f || scale.mV[2] == 0.f; + ++(past ? past_vertices : (flat ? no_scale : refused)); + continue; + } + checkOptimized(before, after, report, where); + checkWeldedOnce(after, report, where); + } + }); + INFO(real->describe()); + INFO(fmt::format("{} faces, {} of them rigged; refused: {} for an index past their vertices, {} for a scale of no size, {} " + "otherwise; {} fatal; {} faces larger than the arena", + faces, rigged, past_vertices, no_scale, refused, fatal, over_arena)); + INFO(report.text()); + CHECK(faces > 0); + CHECK(refused == 0); + CHECK(report.failures == 0); + } + // ------------------------------------------------------------------ // Defects, a case each: tagged [!shouldfail] while the generator has one @@ -3185,36 +3710,60 @@ namespace CHECK(report.failures == 0); } - // Lengyel's tangents, which a mesh LOD too large for meshoptimizer's - // weld still takes, give a vertex no triangle gives a direction an axis + // A card drawn both ways, the one triangle twice sharing two vertices, + // its third corner mirrored in U a hair off true, and turned off the + // axes: at the shared vertices the two triangles' tangents all but + // cancel, and what MikkTSpace's sum leaves of them is rounding, off the + // normal's plane. The tangent cacheOptimize keeps is in the plane. + TEST_CASE("llvolume: a mesh tangent whose triangles all but cancel lies in its normal's plane", "[llvolume]") + { + const LLQuaternion turn(0.7f, LLVector3(0.3f, -0.5f, 0.8f)); + for (F32 hair : { 1e-6f, 3e-6f, 1e-5f, 3e-5f, 1e-4f }) + { + CorpusMesh mesh; + for (const LLVector3& corner : { LLVector3(0.f, 0.f, 0.f), LLVector3(1.f, 0.f, 0.f), LLVector3(0.f, 1.f, 0.f), LLVector3(0.f, 0.f, 0.f) }) + { + mesh.positions.push_back(corner * turn); + mesh.normals.push_back(LLVector3(0.f, 0.f, 1.f) * turn); + } + mesh.uvs = { { 0.f, 0.f }, { 1.f, 0.f }, { 0.f, 1.f }, { 2.f, hair } }; + mesh.indices = { 0, 1, 2, 2, 1, 3 }; + const LLVolumeFace before = mesh.face(); + LLVolumeFace after = before; + REQUIRE(after.cacheOptimize(true)); + Report report; + checkOptimized(before, after, report, fmt::format("card a hair of {} off its mirror", hair)); + INFO(report.text()); + CHECK(report.failures == 0); + } + } + + // A mesh vertex no triangle gives a tangent direction takes an axis // put in the plane of its normal: unit length and orthogonal to it, // whichever way the normal points. - TEST_CASE("llvolume: Lengyel's tangent with no direction lies in the plane of its normal", "[llvolume]") + TEST_CASE("llvolume: a mesh tangent with no direction lies in the plane of its normal", "[llvolume]") { // Two triangles whose UVs are one point, so that neither gives a // tangent, with a normal along each axis and between them. - const LLVector3 positions[] = { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, { 1, 1, 0 } }; - const LLVector2 uvs[] = { { 0.5f, 0.5f }, { 0.5f, 0.5f }, { 0.5f, 0.5f }, { 0.5f, 0.5f } }; - const U16 indices[] = { 0, 1, 2, 2, 1, 3 }; const LLVector3 normals[] = { { 0, 0, 1 }, { 1, 0, 0 }, { 0, 1, 0 }, { 0.6f, 0.f, 0.8f }, { 0.8f, -0.6f, 0.f } }; for (const LLVector3& normal : normals) { - LLVector4a positions4[4], normals4[4], tangents[4]; - for (S32 v = 0; v < 4; ++v) - { - positions4[v].load3(positions[v].mV); - normals4[v].load3(normal.mV); - tangents[v].clear(); - } - LLCalculateTangentArray(4, positions4, normals4, uvs, 2, indices, tangents); - for (S32 v = 0; v < 4; ++v) + CorpusMesh mesh; + mesh.positions = { { 0, 0, 0 }, { 1, 0, 0 }, { 0, 1, 0 }, { 1, 1, 0 } }; + mesh.normals.assign(4, normal); + mesh.uvs.assign(4, LLVector2(0.5f, 0.5f)); + mesh.indices = { 0, 1, 2, 2, 1, 3 }; + LLVolumeFace face = mesh.face(); + REQUIRE(face.cacheOptimize(true)); + REQUIRE(face.mTangents); + for (S32 v = 0; v < face.mNumVertices; ++v) { - const LLVector3 t = v3(tangents[v]); + const LLVector3 t = v3(face.mTangents[v]); INFO(fmt::format("normal ({}, {}, {}): vertex {} has the tangent ({}, {}, {}, {})", normal.mV[0], normal.mV[1], normal.mV[2], v, - t.mV[0], t.mV[1], t.mV[2], tangents[v][3])); + t.mV[0], t.mV[1], t.mV[2], face.mTangents[v][3])); CHECK(fabsf(t.length() - 1.f) <= UNIT_TOLERANCE); CHECK(fabsf(t * normal) <= 2e-3f); - CHECK((tangents[v][3] == 1.f || tangents[v][3] == -1.f)); + CHECK((face.mTangents[v][3] == 1.f || face.mTangents[v][3] == -1.f)); } } } diff --git a/indra/newview/gltf/primitive.cpp b/indra/newview/gltf/primitive.cpp index b744cb61ad..d0e8c8810f 100644 --- a/indra/newview/gltf/primitive.cpp +++ b/indra/newview/gltf/primitive.cpp @@ -30,15 +30,13 @@ #include "buffer_util.h" #include "../llviewershadermgr.h" -#include "mikktspace/mikktspace.hh" - #include using namespace LL::GLTF; -// Mesh data useful for Mikktspace tangent generation (and flat normal generation) +// A primitive unrolled into a triangle list, for its tangents and flat normals struct MikktMesh { std::vector p; //positions @@ -187,11 +185,23 @@ struct MikktMesh } } + // MikkTSpace's tangents, as meshoptimizer's compatible mode computes + // them. GLTF's V runs down the texture, the other way from the V a + // tangent's handedness is taken against, so the tangents are those of + // the UVs as they are with w the other way. void genTangents() { t.resize(p.size()); - mikk::Mikktspace ctx(*this); - ctx.genTangSpace(); + if (p.empty()) + { + return; + } + meshopt_generateTangents(t[0].mV, nullptr, p.size(), p[0].mV, p.size(), sizeof(LLVector3), n[0].mV, sizeof(LLVector3), tc0[0].mV, + sizeof(LLVector2), meshopt_TangentCompatible); + for (LLVector4& tangent : t) + { + tangent.mV[3] = -tangent.mV[3]; + } } // write to target primitive as an indexed triangle list @@ -270,40 +280,6 @@ struct MikktMesh prim->mGLMode = LLRender::TRIANGLES; } - - uint32_t GetNumFaces() - { - return uint32_t(p.size()/3); - } - - uint32_t GetNumVerticesOfFace(const uint32_t face_num) - { - return 3; - } - - mikk::float3 GetPosition(const uint32_t face_num, const uint32_t vert_num) - { - F32* v = p[face_num * 3 + vert_num].mV; - return mikk::float3(v); - } - - mikk::float3 GetTexCoord(const uint32_t face_num, const uint32_t vert_num) - { - F32* uv = tc0[face_num * 3 + vert_num].mV; - return mikk::float3(uv[0], 1.f-uv[1], 1.0f); - } - - mikk::float3 GetNormal(const uint32_t face_num, const uint32_t vert_num) - { - F32* normal = n[face_num * 3 + vert_num].mV; - return mikk::float3(normal); - } - - void SetTangentSpace(const uint32_t face_num, const uint32_t vert_num, mikk::float3 T, bool orientation) - { - S32 i = face_num * 3 + vert_num; - t[i].set(T.x, T.y, T.z, orientation ? 1.0f : -1.0f); - } }; @@ -459,7 +435,6 @@ bool Primitive::prep(Asset& asset) if (mTangents.empty() && !unlit) { // NOTE: must be done last because tangent generation rewrites the other arrays - // adapted from usage of Mikktspace in llvolume.cpp if (mMode == Mode::POINTS || mMode == Mode::LINES || mMode == Mode::LINE_LOOP || mMode == Mode::LINE_STRIP) { // for points and lines, just make sure tangent is perpendicular to normal diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index c6bf02130b..938bd8b681 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -97,6 +97,7 @@ #include "lltoast.h" #include "llsdutil_math.h" #include "lllocationhistory.h" +#include "llmeshoptimizer.h" #include "llvector4a.h" #include "llviewermenufile.h" #include "llvoicechannel.h" @@ -810,6 +811,9 @@ bool LLAppViewer::init() // initialize SSE options LLVector4a::initClass(); + // meshoptimizer's scratch from each thread's own arena + LLMeshOptimizer::initClass(); + //initialize particle index pool LLVOPartGroup::initClass(); From 9197d2333508407da157e216fa9fac934a514271 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 03:03:35 -0400 Subject: [PATCH 3/7] The registry's baseline moves to meshoptimizer 1.3#1 meshoptimizer now resolves from the Alchemy registry, whose 1.3#1 patches its compatible tangents to group, weigh and fall back as the reference MikkTSpace does: one reversed partner per edge, triangles with coincident corners left out, the reference's thresholds for a good triangle, its double-precision acos and its split of exactly opposite tangents. Over the mesh cache they are 0.0047% of corners past 0.1 degree from the reference's, against 0.53% for 1.3 as it was. The baseline moves to a6132ca, which adds the port and changes no other; the manifest resolves meshoptimizer 1.3#1 from the registry. Co-Authored-By: Claude Opus 5.5 --- indra/vcpkg-configuration.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index 4e00c279e7..eff7739f30 100644 --- a/indra/vcpkg-configuration.json +++ b/indra/vcpkg-configuration.json @@ -2,7 +2,7 @@ "registries": [ { "kind": "git", - "baseline": "e189a7d10ef4269b7fbe7f91bf667bcb139cdc4f", + "baseline": "a6132ca341959dc989c88cd2aac992399a6d12bb", "reference": "main", "repository": "https://github.com/AlchemyViewer/alchemy-registry", "packages": [ @@ -23,6 +23,7 @@ "libndofdev", "lsl-definitions", "md4c", + "meshoptimizer", "minizip", "nvapi", "open-libndofdev", From 895a5a0da9ed4b4bfbd7bcc0d5e0371ff77270d7 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 21 Oct 2026 03:21:28 -0400 Subject: [PATCH 4/7] A mesh tangent's handedness is checked only where MikkTSpace gives one meshoptimizer 1.3#1 groups, weighs and falls back as the reference MikkTSpace does, and the tests follow it in two places. A triangle MikkTSpace does not take as one with a tangent space of its own (coincident corners, no UV area past FLT_MIN, or a tangent or bitangent whose length over that area is not past it) takes its orientation from the triangles about it, whatever its UVs wind. That happens on faces with garbage UVs, where a tangent's squares underflow: about a dozen faces in the mesh cache. mikkGood asks MikkTSpace's own test of the triangle as cacheOptimize hands it over, in its float arithmetic, compiled without contraction or reassociation, and the handedness check passes over the triangles it refuses. The grid with degenerate triangles has 55 distinct vertex and tangent pairs under the reference's grouping where 1.3 gave it 52, so its vertex cache and fetch figures rise to 0.7763 and 1.0182. GCC and Clang on ARM, building meshoptimizer with fused multiply-adds, group its degenerate triangles' tangents otherwise, at 0.8158 and 1.0207: the bound is those. Over the whole cache every face passes again. cacheOptimize spends about 20% more cycles than on 1.3, still about a quarter fewer than before the move to meshoptimizer. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llvolume_test.cpp | 74 ++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index cc6a1da6fe..f2d10600f9 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -2146,6 +2146,66 @@ namespace return std::adjacent_find(keys.begin(), keys.end()) == keys.end(); } +#if defined(_MSC_VER) || defined(__clang__) +#pragma float_control(precise, on, push) +#elif defined(__GNUC__) +#pragma GCC push_options +#pragma GCC optimize("fp-contract=off", "no-associative-math", "no-reciprocal-math") +#endif + // Whether MikkTSpace takes a triangle of a decoded face as one with a + // tangent space of its own, by its own float arithmetic over the face as + // authored, the positions scaled out of the unit cube: distinct corners, + // a UV area, and a tangent and bitangent whose lengths over that area + // exceed FLT_MIN. A triangle that is not takes its orientation from the + // triangles about it. This compiles precise, as meshoptimizer does, so + // that the squares and products round as there. + bool mikkGood(const LLVolumeFace& face, S32 t) + { + const LLVector3& scale = face.mNormalizedScale; + F32 p[3][3]; + F32 uv[3][2]; + for (S32 k = 0; k < 3; ++k) + { + const U16 v = face.mIndices[t * 3 + k]; + for (S32 i = 0; i < 3; ++i) + { + p[k][i] = face.mPositions[v][i] * scale.mV[i]; + } + uv[k][0] = face.mTexCoords[v].mV[0]; + uv[k][1] = face.mTexCoords[v].mV[1]; + } + for (S32 a = 0; a < 3; ++a) + { + const S32 b = (a + 1) % 3; + if (p[a][0] == p[b][0] && p[a][1] == p[b][1] && p[a][2] == p[b][2]) + { + return false; + } + } + const F32 t21x = uv[1][0] - uv[0][0], t21y = uv[1][1] - uv[0][1]; + const F32 t31x = uv[2][0] - uv[0][0], t31y = uv[2][1] - uv[0][1]; + const F32 area = fabsf(t21x * t31y - t21y * t31x); + if (!(area > FLT_MIN)) + { + return false; + } + F32 os[3], ot[3]; + for (S32 i = 0; i < 3; ++i) + { + const F32 d1 = p[1][i] - p[0][i], d2 = p[2][i] - p[0][i]; + os[i] = t31y * d1 - t21y * d2; + ot[i] = -t31x * d1 + t21x * d2; + } + const F32 los = sqrtf(os[0] * os[0] + os[1] * os[1] + os[2] * os[2]); + const F32 lot = sqrtf(ot[0] * ot[0] + ot[1] * ot[1] + ot[2] * ot[2]); + return los / area > FLT_MIN && lot / area > FLT_MIN; + } +#if defined(_MSC_VER) || defined(__clang__) +#pragma float_control(pop) +#elif defined(__GNUC__) +#pragma GCC pop_options +#endif + // cacheOptimize keeps every triangle, and every corner's position, // normal at unit length, UV and weights, while it welds and reorders; // it gives every vertex a unit tangent orthogonal to its normal, whose @@ -2211,6 +2271,7 @@ namespace S32 out_first = 0; const TriangleKey key = keyOf(after, t, &out_first); bool kept = false; + S32 source = -1; for (auto [it, end] = by_key.equal_range(key); it != end && !kept; ++it) { S32 in_first = 0; @@ -2220,6 +2281,7 @@ namespace { kept = sameCorner(before, before.mIndices[it->second * 3 + (in_first + k) % 3], after, after.mIndices[t * 3 + (out_first + k) % 3]); } + source = kept ? it->second : -1; } if (!kept) { @@ -2238,14 +2300,17 @@ namespace // meshoptimizer takes them, and their products exact. A triangle // whose UVs are all but in a line, against their size, wind by // less than MikkTSpace's least area, or have products past a - // float's range, has no winding to follow. + // float's range, has no winding to follow, nor has one that + // MikkTSpace, taking the triangle as it was decoded, gives no + // tangent space of its own. const F32 d1u = uv1.mV[0] - uv0.mV[0], d1v = uv1.mV[1] - uv0.mV[1]; const F32 d2u = uv2.mV[0] - uv0.mV[0], d2v = uv2.mV[1] - uv0.mV[1]; const F64 along = (F64)d1u * d2v, across = (F64)d2u * d1v; const F64 uv_area = along - across; const F64 edges = sqrt(((F64)d1u * d1u + (F64)d1v * d1v) * ((F64)d2u * d2u + (F64)d2v * d2v)); const F32 expected_w = uv_area > 0. ? 1.f : -1.f; - const bool winds = fabs(along) <= FLT_MAX && fabs(across) <= FLT_MAX && fabs(uv_area) > llmax(1e-5 * edges, (F64)FLT_MIN); + const bool winds = fabs(along) <= FLT_MAX && fabs(across) <= FLT_MAX && fabs(uv_area) > llmax(1e-5 * edges, (F64)FLT_MIN) && + mikkGood(before, source); for (S32 k = 0; k < 3 && winds; ++k) { const U16 o = after.mIndices[t * 3 + k]; @@ -3092,7 +3157,10 @@ namespace { "cube with split normals", 2.0000, 1.0000 }, { "strip with mirrored UVs", 0.7361, 1.0122 }, { "rigged UV sphere", 0.7366, 1.0066 }, - { "grid with degenerate triangles", 0.7368, 1.0154 }, + // Its degenerate triangles' tangents group as the compiler that + // built meshoptimizer rounds its tests of them: MSVC's build gives + // 0.7763 and 1.0182, GCC's and Clang's on ARM 0.8158 and 1.0207. + { "grid with degenerate triangles", 0.8158, 1.0207 }, { "grid of vertices held twice", 0.7361, 1.0122 }, { "UV sphere 64 by 32", 0.6557, 1.0005 }, { "UV sphere 128 by 64", 0.6425, 1.0001 }, From 8cbd13aad6ccd9e0121a4faa7ed29a26ca0b6026 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:15:30 -0500 Subject: [PATCH 5/7] The arena's refusal of an impossible size is checked where new can throw alscratcharena_test checks that a size near the top of size_t, as meshoptimizer asks for when a count overflows, goes to the heap and comes back as std::bad_alloc. Under AddressSanitizer, the Sanitizers workflow's asan row, operator new is the sanitizer's: it reports the size as allocation-size-too-big and ends the process, so the entry and llcommon's combined run failed. ThreadSanitizer's does the same, and allocator_may_return_null=1 changes only the report, to out-of-memory. ll_test::SANITIZER_ALLOCATOR (altestsanitizer.h) says operator new is AddressSanitizer's or ThreadSanitizer's, and the case is skipped under it. UBSan alone leaves operator new as it is, and the case still runs there. Co-Authored-By: Claude Opus 5.5 --- doc/TESTING.md | 2 +- indra/llcommon/tests/alscratcharena_test.cpp | 5 +++++ indra/test/altestsanitizer.h | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/doc/TESTING.md b/doc/TESTING.md index 01607c4150..329b1facbf 100644 --- a/doc/TESTING.md +++ b/doc/TESTING.md @@ -90,7 +90,7 @@ The **state guard** compares a set of process facts after every case with what t - **An `LL_ERRS`** throws `ll_test::FatalError` in a test, so `REQUIRE_THROWS_AS(..., ll_test::FatalError)` checks one. - **A child process of the test's own making:** register it with `AL_TEST_HELPER(name)`, and launch the binary with `ll_test::Helper::argument(name)`. The main then runs only that helper. - **The running case's name:** `ll_test::currentTestName()`. -- **Built with ThreadSanitizer:** `ll_test::THREAD_SANITIZER` (`altestsanitizer.h`). Its own call stack holds `THREAD_SANITIZER_FRAMES` frames, past which a process falls over with no report, so a case that recurses deeper on purpose is passed over under it. +- **Built with ThreadSanitizer:** `ll_test::THREAD_SANITIZER` (`altestsanitizer.h`). Its own call stack holds `THREAD_SANITIZER_FRAMES` frames, past which a process falls over with no report, so a case that recurses deeper on purpose is passed over under it. `ll_test::SANITIZER_ALLOCATOR` says `operator new` is AddressSanitizer's or ThreadSanitizer's, which ends the process on a size it cannot serve where the standard's throws `std::bad_alloc`, so a case that checks for the throw is passed over under them. ## Binaries diff --git a/indra/llcommon/tests/alscratcharena_test.cpp b/indra/llcommon/tests/alscratcharena_test.cpp index 7ffa0f7710..5f99b2a8fb 100644 --- a/indra/llcommon/tests/alscratcharena_test.cpp +++ b/indra/llcommon/tests/alscratcharena_test.cpp @@ -26,6 +26,7 @@ #include "linden_common.h" #include "alscratcharena.h" +#include "altestsanitizer.h" #include @@ -122,6 +123,10 @@ namespace // its size would overflow, so that the heap refuses it. TEST_CASE("alscratcharena: a size no arena could hold is the heap's to refuse", "[alscratcharena]") { + if constexpr (ll_test::SANITIZER_ALLOCATOR) + { + SKIP("a sanitizer's operator new ends the process rather than throw"); + } CHECK_THROWS_AS(ALScratchArena::allocate(size_t(-1)), std::bad_alloc); void* p = ALScratchArena::allocate(32); void* q = ALScratchArena::allocate(32); diff --git a/indra/test/altestsanitizer.h b/indra/test/altestsanitizer.h index 397b5f8495..41323a347a 100644 --- a/indra/test/altestsanitizer.h +++ b/indra/test/altestsanitizer.h @@ -48,6 +48,23 @@ namespace ll_test // deep on purpose -- a script as long as the grid takes, through LL's // compiler -- is past what it can follow. inline constexpr unsigned THREAD_SANITIZER_FRAMES = 65536; + + // Whether operator new is a sanitizer's, as AddressSanitizer and + // ThreadSanitizer make it. Asked for more than it could ever serve, it + // reports and ends the process where the standard's throws + // std::bad_alloc, whatever allocator_may_return_null says, so a case + // that checks for the throw is passed over under it. +#if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__) + inline constexpr bool SANITIZER_ALLOCATOR = true; +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) || __has_feature(thread_sanitizer) + inline constexpr bool SANITIZER_ALLOCATOR = true; +#else + inline constexpr bool SANITIZER_ALLOCATOR = false; +#endif +#else + inline constexpr bool SANITIZER_ALLOCATOR = false; +#endif } #endif // AL_ALTESTSANITIZER_H From 7dc2043127a0cf5c23965cb4b48a84c2253b7640 Mon Sep 17 00:00:00 2001 From: Zanibar <91260002+taylnos@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:48:06 -0500 Subject: [PATCH 6/7] A mesh corpus that is there is read in full or fails its case ALMeshCorpus::fromEnvironment walked AL_MESH_CORPUS_DIR on one error code, which the walk and each entry's status read shared, and stopped at the first error with the files it had. A folder it could not list gave the [corpus] case and the corpus benchmark a part of the corpus, which passed: of two folders of 300 cache files with an unlistable one between them, it read 300. An error in the walk now fails the case with the directory's path, and an error reading an entry's status with the entry's. A file gone by the time its status is read, as a viewer running on the cache may evict one, is passed over: libstdc++ reports a dangling entry so. Only names that look like cache files have their status read. Reported by CodeRabbit on #559. Co-Authored-By: Claude Opus 5.5 --- doc/TESTING.md | 2 +- indra/llmath/tests/almeshcorpus.h | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/doc/TESTING.md b/doc/TESTING.md index 329b1facbf..74e4edb74e 100644 --- a/doc/TESTING.md +++ b/doc/TESTING.md @@ -18,7 +18,7 @@ ctest --test-dir build-- --output-on-failure -LE manual - **In another order:** `--order rand --rng-seed `. Cases run in declared order by default. - **Every case in a process of its own:** configure with `-DAL_TEST_PER_CASE=ON`. Each case is then its own entry, found when CTest runs. It is for chasing a case that passes alone and fails with the others. - **The log:** `LOGTEST=DEBUG` logs everything at that level as it happens. `LOGFAIL=DEBUG` keeps it and prints a failing case's log only. `--al-log` also writes the log to `..log`. -- **Real mesh assets:** llmath's hidden `[corpus]` case and its corpus benchmark read the meshes of a viewer's cache from the directory `AL_MESH_CORPUS_DIR` names, a seeded sample of them (`indra/llmath/tests/almeshcorpus.h` says how to size and split it), and skip where it is not set: `llmath_tests "[corpus]"`. The assets are other people's, so nothing from them goes into the tree. +- **Real mesh assets:** llmath's hidden `[corpus]` case and its corpus benchmark read the meshes of a viewer's cache from the directory `AL_MESH_CORPUS_DIR` names, a seeded sample of them (`indra/llmath/tests/almeshcorpus.h` says how to size and split it), skip where it is not set, and fail where the directory is there but its walk meets an error: `llmath_tests "[corpus]"`. The assets are other people's, so nothing from them goes into the tree. ## Writing a test diff --git a/indra/llmath/tests/almeshcorpus.h b/indra/llmath/tests/almeshcorpus.h index 2891133ba8..9cfbf51f74 100644 --- a/indra/llmath/tests/almeshcorpus.h +++ b/indra/llmath/tests/almeshcorpus.h @@ -54,6 +54,7 @@ #include #include #include +#include #include namespace ll_test @@ -103,14 +104,32 @@ namespace ALMeshCorpus corpus; corpus.mSample = number("AL_MESH_CORPUS_SAMPLE", DEFAULT_SAMPLE); const U32 seed = (U32)number("AL_MESH_CORPUS_SEED", 1); + // A directory that is there is read in full: an error in the walk + // fails the case rather than leave it a part of the corpus. A + // viewer running on the cache may evict a file between the walk + // listing it and its status being read, and that file is passed + // over. std::error_code ec; for (std::filesystem::recursive_directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec)) { const std::string name = it->path().filename().string(); - if (it->is_regular_file(ec) && name.rfind("sl_cache_", 0) == 0 && name.size() > 17 && name.compare(name.size() - 8, 8, "_0.asset") == 0) + if (name.rfind("sl_cache_", 0) != 0 || name.size() <= 17 || name.compare(name.size() - 8, 8, "_0.asset") != 0) + { + continue; + } + std::error_code status; + if (it->is_regular_file(status)) { corpus.mFiles.push_back(it->path()); } + else if (status && status != std::errc::no_such_file_or_directory) + { + throw std::filesystem::filesystem_error("AL_MESH_CORPUS_DIR", it->path(), status); + } + } + if (ec) + { + throw std::filesystem::filesystem_error("AL_MESH_CORPUS_DIR", dir, ec); } // Sorted, then shuffled by the seed's own sequence, so that the // order is the standard engine's and no library's shuffle. From 3a28009ebc9a91b6f83142a7038d817d033f39d4 Mon Sep 17 00:00:00 2001 From: Rye Date: Sat, 24 Oct 2026 10:23:37 -0400 Subject: [PATCH 7/7] A mesh corpus named where there is no directory fails its case ALMeshCorpus::fromEnvironment took an AL_MESH_CORPUS_DIR that is not a directory as no corpus at all, so a mistyped path skipped the [corpus] case and the corpus benchmark as though nothing had been asked for. An unset or empty variable still means no corpus; a name that is not a directory now fails the case with its path and the reason. Reported by CodeRabbit on #559. Co-Authored-By: Claude Opus 5.5 --- doc/TESTING.md | 2 +- indra/llmath/tests/almeshcorpus.h | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/TESTING.md b/doc/TESTING.md index 74e4edb74e..51199c2731 100644 --- a/doc/TESTING.md +++ b/doc/TESTING.md @@ -18,7 +18,7 @@ ctest --test-dir build-- --output-on-failure -LE manual - **In another order:** `--order rand --rng-seed `. Cases run in declared order by default. - **Every case in a process of its own:** configure with `-DAL_TEST_PER_CASE=ON`. Each case is then its own entry, found when CTest runs. It is for chasing a case that passes alone and fails with the others. - **The log:** `LOGTEST=DEBUG` logs everything at that level as it happens. `LOGFAIL=DEBUG` keeps it and prints a failing case's log only. `--al-log` also writes the log to `..log`. -- **Real mesh assets:** llmath's hidden `[corpus]` case and its corpus benchmark read the meshes of a viewer's cache from the directory `AL_MESH_CORPUS_DIR` names, a seeded sample of them (`indra/llmath/tests/almeshcorpus.h` says how to size and split it), skip where it is not set, and fail where the directory is there but its walk meets an error: `llmath_tests "[corpus]"`. The assets are other people's, so nothing from them goes into the tree. +- **Real mesh assets:** llmath's hidden `[corpus]` case and its corpus benchmark read the meshes of a viewer's cache from the directory `AL_MESH_CORPUS_DIR` names, a seeded sample of them (`indra/llmath/tests/almeshcorpus.h` says how to size and split it), skip where it is not set, and fail where it names no directory or the directory's walk meets an error: `llmath_tests "[corpus]"`. The assets are other people's, so nothing from them goes into the tree. ## Writing a test diff --git a/indra/llmath/tests/almeshcorpus.h b/indra/llmath/tests/almeshcorpus.h index 9cfbf51f74..dced53fceb 100644 --- a/indra/llmath/tests/almeshcorpus.h +++ b/indra/llmath/tests/almeshcorpus.h @@ -93,14 +93,22 @@ namespace public: static constexpr size_t DEFAULT_SAMPLE = 2000; - // The corpus AL_MESH_CORPUS_DIR names, or none. + // The corpus AL_MESH_CORPUS_DIR names, or none where it names none. + // A name that is not a directory fails the case, as a mistyped one + // would otherwise pass it as though no corpus had been asked for. static std::optional fromEnvironment() { const char* dir = std::getenv("AL_MESH_CORPUS_DIR"); - if (!dir || !*dir || !std::filesystem::is_directory(dir)) + if (!dir || !*dir) { return std::nullopt; } + std::error_code is_dir; + if (!std::filesystem::is_directory(dir, is_dir)) + { + throw std::filesystem::filesystem_error("AL_MESH_CORPUS_DIR", dir, + is_dir ? is_dir : std::make_error_code(std::errc::not_a_directory)); + } ALMeshCorpus corpus; corpus.mSample = number("AL_MESH_CORPUS_SAMPLE", DEFAULT_SAMPLE); const U32 seed = (U32)number("AL_MESH_CORPUS_SEED", 1);