diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index 016f1df11c5..06e4c773aa9 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 1bb1bc80642..51199c27313 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), 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 @@ -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. @@ -88,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/cmake/Dependencies.cmake b/indra/cmake/Dependencies.cmake index 22725ff8284..f2f3bf4168e 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 175d4f96d9b..21f903a83ad 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 64930abb73f..0688ad19e1c 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 86b1a0ba773..00000000000 --- 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 470315995d6..00000000000 --- 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 b0b34e9fde3..00000000000 --- 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 bff35a3b1af..00000000000 --- 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 9f1761c504c..00000000000 --- 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/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index ed355dd2821..e0cc0c5b23a 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 00000000000..01b5322376a --- /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 00000000000..467db078c67 --- /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 00000000000..5f99b2a8fbe --- /dev/null +++ b/indra/llcommon/tests/alscratcharena_test.cpp @@ -0,0 +1,177 @@ +/** + * @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 "altestsanitizer.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]") + { + 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); + 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); + } +} diff --git a/indra/llmath/CMakeLists.txt b/indra/llmath/CMakeLists.txt index 82c83e5e6c7..f3560b8b8d8 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 bee9d71e1a7..ce0899cfa45 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 59a2f86b4c6..f5d41699414 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 0eead48ebe8..5b916242817 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 93666845372..04adfd4f0f2 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 00000000000..dced53fceba --- /dev/null +++ b/indra/llmath/tests/almeshcorpus.h @@ -0,0 +1,275 @@ +/** + * @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 +#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 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) + { + 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); + // 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 (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. + 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 00000000000..b044a694aab --- /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 346d9917443..16fb13d50da 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 068178db92b..9e6e27f831a 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 @@ -2002,9 +2006,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, @@ -2033,10 +2123,120 @@ 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(); + } + +#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, 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) @@ -2079,6 +2279,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) @@ -2087,38 +2294,55 @@ 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) - { - 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])) + bool kept = false; + S32 source = -1; + for (auto [it, end] = by_key.equal_range(key); it != end && !kept; ++it) + { + 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 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])) - { - 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]); } + source = kept ? it->second : -1; } + 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, 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) && + mikkGood(before, source); + 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])); @@ -2127,13 +2351,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; @@ -2141,6 +2384,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. @@ -2588,6 +2967,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); } @@ -2603,6 +2983,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]") @@ -2660,10 +3181,17 @@ namespace { "cube with split normals", 2.0000, 1.0000 }, { "strip with mirrored UVs", 0.7361, 1.0122 }, { "rigged UV sphere", 0.7366, 1.0066 }, + // 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 }, }; - 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)); @@ -2826,6 +3354,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 @@ -3209,36 +3802,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 b744cb61adc..d0e8c8810f9 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 c6bf02130b9..938bd8b681a 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(); diff --git a/indra/test/altestsanitizer.h b/indra/test/altestsanitizer.h index 397b5f84952..41323a347af 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 diff --git a/indra/vcpkg-configuration.json b/indra/vcpkg-configuration.json index 4e00c279e70..eff7739f301 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",