mirror of
https://github.com/korenkonder/ReDIVA.git
synced 2026-10-05 21:28:13 +03:00
Added stripifier for X PV obj set output
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* meshoptimizer - version 0.18
|
||||
*
|
||||
* Copyright (C) 2016-2023, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
|
||||
* Report bugs and download new versions at https://github.com/zeux/meshoptimizer
|
||||
*
|
||||
* This library is distributed under the MIT License. See notice at the end of this file.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Version macro; major * 1000 + minor * 10 + patch */
|
||||
#define MESHOPTIMIZER_VERSION 180 /* 0.18 */
|
||||
|
||||
/* If no API is defined, assume default */
|
||||
#ifndef MESHOPTIMIZER_API
|
||||
#define MESHOPTIMIZER_API
|
||||
#endif
|
||||
|
||||
/* Set the calling-convention for alloc/dealloc function pointers */
|
||||
#ifndef MESHOPTIMIZER_ALLOC_CALLCONV
|
||||
#ifdef _MSC_VER
|
||||
#define MESHOPTIMIZER_ALLOC_CALLCONV __cdecl
|
||||
#else
|
||||
#define MESHOPTIMIZER_ALLOC_CALLCONV
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Experimental APIs have unstable interface and might have implementation that's not fully tested or optimized */
|
||||
#define MESHOPTIMIZER_EXPERIMENTAL MESHOPTIMIZER_API
|
||||
|
||||
/* C interface */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Vertex attribute stream
|
||||
* Each element takes size bytes, beginning at data, with stride controlling the spacing between successive elements (stride >= size).
|
||||
*/
|
||||
struct meshopt_Stream
|
||||
{
|
||||
const void* data;
|
||||
size_t size;
|
||||
size_t stride;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mesh stripifier
|
||||
* Converts a previously vertex cache optimized triangle list to triangle strip, stitching strips using restart index or degenerate triangles
|
||||
* Returns the number of indices in the resulting strip, with destination containing new index data
|
||||
* For maximum efficiency the index buffer being converted has to be optimized for vertex cache first.
|
||||
* Using restart indices can result in ~10% smaller index buffers, but on some GPUs restart indices may result in decreased performance.
|
||||
*
|
||||
* destination must contain enough space for the target index buffer, worst case can be computed with meshopt_stripifyBound
|
||||
* restart_index should be 0xffff or 0xffffffff depending on index size, or 0 to use degenerate triangles
|
||||
*/
|
||||
MESHOPTIMIZER_API size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index);
|
||||
MESHOPTIMIZER_API size_t meshopt_stripifyBound(size_t index_count);
|
||||
|
||||
/**
|
||||
* Mesh unstripifier
|
||||
* Converts a triangle strip to a triangle list
|
||||
* Returns the number of indices in the resulting list, with destination containing new index data
|
||||
*
|
||||
* destination must contain enough space for the target index buffer, worst case can be computed with meshopt_unstripifyBound
|
||||
*/
|
||||
MESHOPTIMIZER_API size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index);
|
||||
MESHOPTIMIZER_API size_t meshopt_unstripifyBound(size_t index_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
/* Internal implementation helpers */
|
||||
#ifdef __cplusplus
|
||||
class meshopt_Allocator
|
||||
{
|
||||
public:
|
||||
template <typename T>
|
||||
struct StorageT
|
||||
{
|
||||
static void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t);
|
||||
static void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*);
|
||||
};
|
||||
|
||||
typedef StorageT<void> Storage;
|
||||
|
||||
meshopt_Allocator()
|
||||
: blocks()
|
||||
, count(0)
|
||||
{
|
||||
}
|
||||
|
||||
~meshopt_Allocator()
|
||||
{
|
||||
for (size_t i = count; i > 0; --i)
|
||||
Storage::deallocate(blocks[i - 1]);
|
||||
}
|
||||
|
||||
template <typename T> T* allocate(size_t size)
|
||||
{
|
||||
assert(count < sizeof(blocks) / sizeof(blocks[0]));
|
||||
T* result = static_cast<T*>(Storage::allocate(size > size_t(-1) / sizeof(T) ? size_t(-1) : size * sizeof(T)));
|
||||
blocks[count++] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void* blocks[24];
|
||||
size_t count;
|
||||
};
|
||||
|
||||
// This makes sure that allocate/deallocate are lazily generated in translation units that need them and are deduplicated by the linker
|
||||
template <typename T> void* (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT<T>::allocate)(size_t) = operator new;
|
||||
template <typename T> void (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT<T>::deallocate)(void*) = operator delete;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Copyright (c) 2016-2023 Arseny Kapoulkine
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
|
||||
#include "meshoptimizer.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
// This work is based on:
|
||||
// Francine Evans, Steven Skiena and Amitabh Varshney. Optimizing Triangle Strips for Fast Rendering. 1996
|
||||
namespace meshopt
|
||||
{
|
||||
|
||||
static unsigned int findStripFirst(const unsigned int buffer[][3], unsigned int buffer_size, const unsigned int* valence)
|
||||
{
|
||||
unsigned int index = 0;
|
||||
unsigned int iv = ~0u;
|
||||
|
||||
for (size_t i = 0; i < buffer_size; ++i)
|
||||
{
|
||||
unsigned int va = valence[buffer[i][0]], vb = valence[buffer[i][1]], vc = valence[buffer[i][2]];
|
||||
unsigned int v = (va < vb && va < vc) ? va : (vb < vc) ? vb : vc;
|
||||
|
||||
if (v < iv)
|
||||
{
|
||||
index = unsigned(i);
|
||||
iv = v;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
static int findStripNext(const unsigned int buffer[][3], unsigned int buffer_size, unsigned int e0, unsigned int e1)
|
||||
{
|
||||
for (size_t i = 0; i < buffer_size; ++i)
|
||||
{
|
||||
unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2];
|
||||
|
||||
if (e0 == a && e1 == b)
|
||||
return (int(i) << 2) | 2;
|
||||
else if (e0 == b && e1 == c)
|
||||
return (int(i) << 2) | 0;
|
||||
else if (e0 == c && e1 == a)
|
||||
return (int(i) << 2) | 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace meshopt
|
||||
|
||||
size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index)
|
||||
{
|
||||
assert(destination != indices);
|
||||
assert(index_count % 3 == 0);
|
||||
|
||||
using namespace meshopt;
|
||||
|
||||
meshopt_Allocator allocator;
|
||||
|
||||
const size_t buffer_capacity = 8;
|
||||
|
||||
unsigned int buffer[buffer_capacity][3] = {};
|
||||
unsigned int buffer_size = 0;
|
||||
|
||||
size_t index_offset = 0;
|
||||
|
||||
unsigned int strip[2] = {};
|
||||
unsigned int parity = 0;
|
||||
|
||||
size_t strip_size = 0;
|
||||
|
||||
// compute vertex valence; this is used to prioritize starting triangle for strips
|
||||
unsigned int* valence = allocator.allocate<unsigned int>(vertex_count);
|
||||
memset(valence, 0, vertex_count * sizeof(unsigned int));
|
||||
|
||||
for (size_t i = 0; i < index_count; ++i)
|
||||
{
|
||||
unsigned int index = indices[i];
|
||||
assert(index < vertex_count);
|
||||
|
||||
valence[index]++;
|
||||
}
|
||||
|
||||
int next = -1;
|
||||
|
||||
while (buffer_size > 0 || index_offset < index_count)
|
||||
{
|
||||
assert(next < 0 || (size_t(next >> 2) < buffer_size && (next & 3) < 3));
|
||||
|
||||
// fill triangle buffer
|
||||
while (buffer_size < buffer_capacity && index_offset < index_count)
|
||||
{
|
||||
buffer[buffer_size][0] = indices[index_offset + 0];
|
||||
buffer[buffer_size][1] = indices[index_offset + 1];
|
||||
buffer[buffer_size][2] = indices[index_offset + 2];
|
||||
|
||||
buffer_size++;
|
||||
index_offset += 3;
|
||||
}
|
||||
|
||||
assert(buffer_size > 0);
|
||||
|
||||
if (next >= 0)
|
||||
{
|
||||
unsigned int i = next >> 2;
|
||||
unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2];
|
||||
unsigned int v = buffer[i][next & 3];
|
||||
|
||||
// ordered removal from the buffer
|
||||
memmove(buffer[i], buffer[i + 1], (buffer_size - i - 1) * sizeof(buffer[0]));
|
||||
buffer_size--;
|
||||
|
||||
// update vertex valences for strip start heuristic
|
||||
valence[a]--;
|
||||
valence[b]--;
|
||||
valence[c]--;
|
||||
|
||||
// find next triangle (note that edge order flips on every iteration)
|
||||
// in some cases we need to perform a swap to pick a different outgoing triangle edge
|
||||
// for [a b c], the default strip edge is [b c], but we might want to use [a c]
|
||||
int cont = findStripNext(buffer, buffer_size, parity ? strip[1] : v, parity ? v : strip[1]);
|
||||
int swap = cont < 0 ? findStripNext(buffer, buffer_size, parity ? v : strip[0], parity ? strip[0] : v) : -1;
|
||||
|
||||
if (cont < 0 && swap >= 0)
|
||||
{
|
||||
// [a b c] => [a b a c]
|
||||
destination[strip_size++] = strip[0];
|
||||
destination[strip_size++] = v;
|
||||
|
||||
// next strip has same winding
|
||||
// ? a b => b a v
|
||||
strip[1] = v;
|
||||
|
||||
next = swap;
|
||||
}
|
||||
else
|
||||
{
|
||||
// emit the next vertex in the strip
|
||||
destination[strip_size++] = v;
|
||||
|
||||
// next strip has flipped winding
|
||||
strip[0] = strip[1];
|
||||
strip[1] = v;
|
||||
parity ^= 1;
|
||||
|
||||
next = cont;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we didn't find anything, we need to find the next new triangle
|
||||
// we use a heuristic to maximize the strip length
|
||||
unsigned int i = findStripFirst(buffer, buffer_size, &valence[0]);
|
||||
unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2];
|
||||
|
||||
// ordered removal from the buffer
|
||||
memmove(buffer[i], buffer[i + 1], (buffer_size - i - 1) * sizeof(buffer[0]));
|
||||
buffer_size--;
|
||||
|
||||
// update vertex valences for strip start heuristic
|
||||
valence[a]--;
|
||||
valence[b]--;
|
||||
valence[c]--;
|
||||
|
||||
// we need to pre-rotate the triangle so that we will find a match in the existing buffer on the next iteration
|
||||
int ea = findStripNext(buffer, buffer_size, c, b);
|
||||
int eb = findStripNext(buffer, buffer_size, a, c);
|
||||
int ec = findStripNext(buffer, buffer_size, b, a);
|
||||
|
||||
// in some cases we can have several matching edges; since we can pick any edge, we pick the one with the smallest
|
||||
// triangle index in the buffer. this reduces the effect of stripification on ACMR and additionally - for unclear
|
||||
// reasons - slightly improves the stripification efficiency
|
||||
int mine = INT_MAX;
|
||||
mine = (ea >= 0 && mine > ea) ? ea : mine;
|
||||
mine = (eb >= 0 && mine > eb) ? eb : mine;
|
||||
mine = (ec >= 0 && mine > ec) ? ec : mine;
|
||||
|
||||
if (ea == mine)
|
||||
{
|
||||
// keep abc
|
||||
next = ea;
|
||||
}
|
||||
else if (eb == mine)
|
||||
{
|
||||
// abc -> bca
|
||||
unsigned int t = a;
|
||||
a = b, b = c, c = t;
|
||||
|
||||
next = eb;
|
||||
}
|
||||
else if (ec == mine)
|
||||
{
|
||||
// abc -> cab
|
||||
unsigned int t = c;
|
||||
c = b, b = a, a = t;
|
||||
|
||||
next = ec;
|
||||
}
|
||||
|
||||
if (restart_index)
|
||||
{
|
||||
if (strip_size)
|
||||
destination[strip_size++] = restart_index;
|
||||
|
||||
destination[strip_size++] = a;
|
||||
destination[strip_size++] = b;
|
||||
destination[strip_size++] = c;
|
||||
|
||||
// new strip always starts with the same edge winding
|
||||
strip[0] = b;
|
||||
strip[1] = c;
|
||||
parity = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (strip_size)
|
||||
{
|
||||
// connect last strip using degenerate triangles
|
||||
destination[strip_size++] = strip[1];
|
||||
destination[strip_size++] = a;
|
||||
}
|
||||
|
||||
// note that we may need to flip the emitted triangle based on parity
|
||||
// we always end up with outgoing edge "cb" in the end
|
||||
unsigned int e0 = parity ? c : b;
|
||||
unsigned int e1 = parity ? b : c;
|
||||
|
||||
destination[strip_size++] = a;
|
||||
destination[strip_size++] = e0;
|
||||
destination[strip_size++] = e1;
|
||||
|
||||
strip[0] = e0;
|
||||
strip[1] = e1;
|
||||
parity ^= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strip_size;
|
||||
}
|
||||
|
||||
size_t meshopt_stripifyBound(size_t index_count)
|
||||
{
|
||||
assert(index_count % 3 == 0);
|
||||
|
||||
// worst case without restarts is 2 degenerate indices and 3 indices per triangle
|
||||
// worst case with restarts is 1 restart index and 3 indices per triangle
|
||||
return (index_count / 3) * 5;
|
||||
}
|
||||
|
||||
size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index)
|
||||
{
|
||||
assert(destination != indices);
|
||||
|
||||
size_t offset = 0;
|
||||
size_t start = 0;
|
||||
|
||||
for (size_t i = 0; i < index_count; ++i)
|
||||
{
|
||||
if (restart_index && indices[i] == restart_index)
|
||||
{
|
||||
start = i + 1;
|
||||
}
|
||||
else if (i - start >= 2)
|
||||
{
|
||||
unsigned int a = indices[i - 2], b = indices[i - 1], c = indices[i];
|
||||
|
||||
// flip winding for odd triangles
|
||||
if ((i - start) & 1)
|
||||
{
|
||||
unsigned int t = a;
|
||||
a = b, b = t;
|
||||
}
|
||||
|
||||
// although we use restart indices, strip swaps still produce degenerate triangles, so skip them
|
||||
if (a != b && a != c && b != c)
|
||||
{
|
||||
destination[offset + 0] = a;
|
||||
destination[offset + 1] = b;
|
||||
destination[offset + 2] = c;
|
||||
offset += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
size_t meshopt_unstripifyBound(size_t index_count)
|
||||
{
|
||||
assert(index_count == 0 || index_count >= 3);
|
||||
|
||||
return (index_count == 0) ? 0 : (index_count - 2) * 3;
|
||||
}
|
||||
+22
-16
@@ -1402,16 +1402,24 @@ static void obj_classic_read_index(obj_sub_mesh* sub_mesh, prj::shared_ptr<prj::
|
||||
uint32_t* index_array = alloc->allocate<uint32_t>(num_index);
|
||||
switch (sub_mesh->index_format) {
|
||||
case OBJ_INDEX_U8:
|
||||
for (uint32_t i = 0; i < num_index; i++) {
|
||||
uint8_t idx = s.read_uint8_t();
|
||||
index_array[i] = tri_strip && idx == 0xFF ? 0xFFFFFFFF : idx;
|
||||
}
|
||||
if (tri_strip)
|
||||
for (uint32_t i = 0; i < num_index; i++) {
|
||||
uint8_t idx = s.read_uint8_t();
|
||||
index_array[i] = idx == 0xFF ? 0xFFFFFFFF : idx;
|
||||
}
|
||||
else
|
||||
for (uint32_t i = 0; i < num_index; i++)
|
||||
index_array[i] = s.read_uint8_t();
|
||||
break;
|
||||
case OBJ_INDEX_U16:
|
||||
for (uint32_t i = 0; i < num_index; i++) {
|
||||
uint16_t idx = s.read_uint16_t();
|
||||
index_array[i] = tri_strip && idx == 0xFFFF ? 0xFFFFFFFF : idx;
|
||||
}
|
||||
if (tri_strip)
|
||||
for (uint32_t i = 0; i < num_index; i++) {
|
||||
uint16_t idx = s.read_uint16_t();
|
||||
index_array[i] = idx == 0xFFFF ? 0xFFFFFFFF : idx;
|
||||
}
|
||||
else
|
||||
for (uint32_t i = 0; i < num_index; i++)
|
||||
index_array[i] = s.read_uint16_t();
|
||||
break;
|
||||
case OBJ_INDEX_U32:
|
||||
for (uint32_t i = 0; i < num_index; i++)
|
||||
@@ -1609,8 +1617,7 @@ static void obj_classic_write_model(obj* obj, stream& s, int64_t base_offset) {
|
||||
s.write_int32_t(sub_mesh->num_index);
|
||||
s.write_uint32_t((uint32_t)smh.index_array_offset);
|
||||
s.write_uint32_t(sub_mesh->attrib.w);
|
||||
s.write(0x18);
|
||||
s.write_uint32_t(sub_mesh->index_offset);
|
||||
s.write(0x1C);
|
||||
s.position_pop();
|
||||
}
|
||||
}
|
||||
@@ -1634,7 +1641,7 @@ static void obj_classic_write_model(obj* obj, stream& s, int64_t base_offset) {
|
||||
for (int64_t& j : mh->vertex)
|
||||
s.write_uint32_t((uint32_t)j);
|
||||
|
||||
s.write_uint32_t(mesh->attrib.w);
|
||||
s.write_uint32_t(mesh->attrib.w & 0x7FFFFFFF);
|
||||
s.write_uint32_t(mh->vertex_format_index);
|
||||
s.write_uint32_t(mesh->reserved[0]);
|
||||
s.write_uint32_t(mesh->reserved[1]);
|
||||
@@ -1703,7 +1710,7 @@ static void obj_classic_read_model_mesh(obj_mesh* mesh,
|
||||
for (int64_t& i : mh.vertex)
|
||||
i = s.read_uint32_t();
|
||||
|
||||
mesh->attrib.w = s.read_uint32_t();
|
||||
mesh->attrib.w = s.read_uint32_t() & 0x7FFFFFFF;
|
||||
mh.vertex_format_index = s.read_uint32_t();
|
||||
mesh->reserved[0] = s.read_uint32_t();
|
||||
mesh->reserved[1] = s.read_uint32_t();
|
||||
@@ -1751,8 +1758,7 @@ static void obj_classic_read_model_sub_mesh(obj_sub_mesh* sub_mesh,
|
||||
sub_mesh->bounding_box.size.x = sub_mesh->bounding_sphere.radius * 2.0f;
|
||||
sub_mesh->bounding_box.size.y = sub_mesh->bounding_sphere.radius * 2.0f;
|
||||
sub_mesh->bounding_box.size.z = sub_mesh->bounding_sphere.radius * 2.0f;
|
||||
s.read(0, 0x18);
|
||||
sub_mesh->index_offset = s.read_uint32_t();
|
||||
s.read(0, 0x1C);
|
||||
|
||||
if (sub_mesh->bones_per_vertex == 4 && smh.bone_index_array_offset) {
|
||||
sub_mesh->bone_index_array = alloc->allocate<uint16_t>(sub_mesh->num_bone_index);
|
||||
@@ -4850,7 +4856,7 @@ static void obj_modern_write_model(obj* obj, stream& s,
|
||||
for (uint32_t j = 0; j < 20; j++)
|
||||
s.write_offset_x(mh->vertex[j]);
|
||||
|
||||
s.write_uint32_t_reverse_endianness(mesh->attrib.w);
|
||||
s.write_uint32_t_reverse_endianness(mesh->attrib.w & 0x7FFFFFFF);
|
||||
s.write_uint32_t_reverse_endianness(mh->vertex_format_index);
|
||||
s.write_uint32_t_reverse_endianness(mesh->reserved[0]);
|
||||
s.write_uint32_t_reverse_endianness(mesh->reserved[1]);
|
||||
@@ -4973,7 +4979,7 @@ static void obj_modern_read_model_mesh(obj_mesh* mesh,
|
||||
for (int64_t& i : mh.vertex)
|
||||
i = s.read_offset_x();
|
||||
|
||||
mesh->attrib.w = s.read_uint32_t_reverse_endianness();
|
||||
mesh->attrib.w = s.read_uint32_t_reverse_endianness() & 0x7FFFFFFF;
|
||||
mh.vertex_format_index = s.read_uint32_t_reverse_endianness();
|
||||
mesh->reserved[0] = s.read_uint32_t_reverse_endianness();
|
||||
mesh->reserved[1] = s.read_uint32_t_reverse_endianness();
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* meshoptimizer - version 0.18
|
||||
*
|
||||
* Copyright (C) 2016-2023, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
|
||||
* Report bugs and download new versions at https://github.com/zeux/meshoptimizer
|
||||
*
|
||||
* This library is distributed under the MIT License. See notice at the end of this file.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Version macro; major * 1000 + minor * 10 + patch */
|
||||
#define MESHOPTIMIZER_VERSION 180 /* 0.18 */
|
||||
|
||||
/* If no API is defined, assume default */
|
||||
#ifndef MESHOPTIMIZER_API
|
||||
#define MESHOPTIMIZER_API
|
||||
#endif
|
||||
|
||||
/* Set the calling-convention for alloc/dealloc function pointers */
|
||||
#ifndef MESHOPTIMIZER_ALLOC_CALLCONV
|
||||
#ifdef _MSC_VER
|
||||
#define MESHOPTIMIZER_ALLOC_CALLCONV __cdecl
|
||||
#else
|
||||
#define MESHOPTIMIZER_ALLOC_CALLCONV
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Experimental APIs have unstable interface and might have implementation that's not fully tested or optimized */
|
||||
#define MESHOPTIMIZER_EXPERIMENTAL MESHOPTIMIZER_API
|
||||
|
||||
/* C interface */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Vertex attribute stream
|
||||
* Each element takes size bytes, beginning at data, with stride controlling the spacing between successive elements (stride >= size).
|
||||
*/
|
||||
struct meshopt_Stream
|
||||
{
|
||||
const void* data;
|
||||
size_t size;
|
||||
size_t stride;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mesh stripifier
|
||||
* Converts a previously vertex cache optimized triangle list to triangle strip, stitching strips using restart index or degenerate triangles
|
||||
* Returns the number of indices in the resulting strip, with destination containing new index data
|
||||
* For maximum efficiency the index buffer being converted has to be optimized for vertex cache first.
|
||||
* Using restart indices can result in ~10% smaller index buffers, but on some GPUs restart indices may result in decreased performance.
|
||||
*
|
||||
* destination must contain enough space for the target index buffer, worst case can be computed with meshopt_stripifyBound
|
||||
* restart_index should be 0xffff or 0xffffffff depending on index size, or 0 to use degenerate triangles
|
||||
*/
|
||||
MESHOPTIMIZER_API size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index);
|
||||
MESHOPTIMIZER_API size_t meshopt_stripifyBound(size_t index_count);
|
||||
|
||||
/**
|
||||
* Mesh unstripifier
|
||||
* Converts a triangle strip to a triangle list
|
||||
* Returns the number of indices in the resulting list, with destination containing new index data
|
||||
*
|
||||
* destination must contain enough space for the target index buffer, worst case can be computed with meshopt_unstripifyBound
|
||||
*/
|
||||
MESHOPTIMIZER_API size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index);
|
||||
MESHOPTIMIZER_API size_t meshopt_unstripifyBound(size_t index_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
/* Internal implementation helpers */
|
||||
#ifdef __cplusplus
|
||||
class meshopt_Allocator
|
||||
{
|
||||
public:
|
||||
template <typename T>
|
||||
struct StorageT
|
||||
{
|
||||
static void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t);
|
||||
static void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*);
|
||||
};
|
||||
|
||||
typedef StorageT<void> Storage;
|
||||
|
||||
meshopt_Allocator()
|
||||
: blocks()
|
||||
, count(0)
|
||||
{
|
||||
}
|
||||
|
||||
~meshopt_Allocator()
|
||||
{
|
||||
for (size_t i = count; i > 0; --i)
|
||||
Storage::deallocate(blocks[i - 1]);
|
||||
}
|
||||
|
||||
template <typename T> T* allocate(size_t size)
|
||||
{
|
||||
assert(count < sizeof(blocks) / sizeof(blocks[0]));
|
||||
T* result = static_cast<T*>(Storage::allocate(size > size_t(-1) / sizeof(T) ? size_t(-1) : size * sizeof(T)));
|
||||
blocks[count++] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void* blocks[24];
|
||||
size_t count;
|
||||
};
|
||||
|
||||
// This makes sure that allocate/deallocate are lazily generated in translation units that need them and are deduplicated by the linker
|
||||
template <typename T> void* (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT<T>::allocate)(size_t) = operator new;
|
||||
template <typename T> void (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT<T>::deallocate)(void*) = operator delete;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Copyright (c) 2016-2023 Arseny Kapoulkine
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following
|
||||
* conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
* OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
@@ -0,0 +1,295 @@
|
||||
// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
|
||||
#include "meshoptimizer.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
// This work is based on:
|
||||
// Francine Evans, Steven Skiena and Amitabh Varshney. Optimizing Triangle Strips for Fast Rendering. 1996
|
||||
namespace meshopt
|
||||
{
|
||||
|
||||
static unsigned int findStripFirst(const unsigned int buffer[][3], unsigned int buffer_size, const unsigned int* valence)
|
||||
{
|
||||
unsigned int index = 0;
|
||||
unsigned int iv = ~0u;
|
||||
|
||||
for (size_t i = 0; i < buffer_size; ++i)
|
||||
{
|
||||
unsigned int va = valence[buffer[i][0]], vb = valence[buffer[i][1]], vc = valence[buffer[i][2]];
|
||||
unsigned int v = (va < vb && va < vc) ? va : (vb < vc) ? vb : vc;
|
||||
|
||||
if (v < iv)
|
||||
{
|
||||
index = unsigned(i);
|
||||
iv = v;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
static int findStripNext(const unsigned int buffer[][3], unsigned int buffer_size, unsigned int e0, unsigned int e1)
|
||||
{
|
||||
for (size_t i = 0; i < buffer_size; ++i)
|
||||
{
|
||||
unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2];
|
||||
|
||||
if (e0 == a && e1 == b)
|
||||
return (int(i) << 2) | 2;
|
||||
else if (e0 == b && e1 == c)
|
||||
return (int(i) << 2) | 0;
|
||||
else if (e0 == c && e1 == a)
|
||||
return (int(i) << 2) | 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace meshopt
|
||||
|
||||
size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index)
|
||||
{
|
||||
assert(destination != indices);
|
||||
assert(index_count % 3 == 0);
|
||||
|
||||
using namespace meshopt;
|
||||
|
||||
meshopt_Allocator allocator;
|
||||
|
||||
const size_t buffer_capacity = 8;
|
||||
|
||||
unsigned int buffer[buffer_capacity][3] = {};
|
||||
unsigned int buffer_size = 0;
|
||||
|
||||
size_t index_offset = 0;
|
||||
|
||||
unsigned int strip[2] = {};
|
||||
unsigned int parity = 0;
|
||||
|
||||
size_t strip_size = 0;
|
||||
|
||||
// compute vertex valence; this is used to prioritize starting triangle for strips
|
||||
unsigned int* valence = allocator.allocate<unsigned int>(vertex_count);
|
||||
memset(valence, 0, vertex_count * sizeof(unsigned int));
|
||||
|
||||
for (size_t i = 0; i < index_count; ++i)
|
||||
{
|
||||
unsigned int index = indices[i];
|
||||
assert(index < vertex_count);
|
||||
|
||||
valence[index]++;
|
||||
}
|
||||
|
||||
int next = -1;
|
||||
|
||||
while (buffer_size > 0 || index_offset < index_count)
|
||||
{
|
||||
assert(next < 0 || (size_t(next >> 2) < buffer_size && (next & 3) < 3));
|
||||
|
||||
// fill triangle buffer
|
||||
while (buffer_size < buffer_capacity && index_offset < index_count)
|
||||
{
|
||||
buffer[buffer_size][0] = indices[index_offset + 0];
|
||||
buffer[buffer_size][1] = indices[index_offset + 1];
|
||||
buffer[buffer_size][2] = indices[index_offset + 2];
|
||||
|
||||
buffer_size++;
|
||||
index_offset += 3;
|
||||
}
|
||||
|
||||
assert(buffer_size > 0);
|
||||
|
||||
if (next >= 0)
|
||||
{
|
||||
unsigned int i = next >> 2;
|
||||
unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2];
|
||||
unsigned int v = buffer[i][next & 3];
|
||||
|
||||
// ordered removal from the buffer
|
||||
memmove(buffer[i], buffer[i + 1], (buffer_size - i - 1) * sizeof(buffer[0]));
|
||||
buffer_size--;
|
||||
|
||||
// update vertex valences for strip start heuristic
|
||||
valence[a]--;
|
||||
valence[b]--;
|
||||
valence[c]--;
|
||||
|
||||
// find next triangle (note that edge order flips on every iteration)
|
||||
// in some cases we need to perform a swap to pick a different outgoing triangle edge
|
||||
// for [a b c], the default strip edge is [b c], but we might want to use [a c]
|
||||
int cont = findStripNext(buffer, buffer_size, parity ? strip[1] : v, parity ? v : strip[1]);
|
||||
int swap = cont < 0 ? findStripNext(buffer, buffer_size, parity ? v : strip[0], parity ? strip[0] : v) : -1;
|
||||
|
||||
if (cont < 0 && swap >= 0)
|
||||
{
|
||||
// [a b c] => [a b a c]
|
||||
destination[strip_size++] = strip[0];
|
||||
destination[strip_size++] = v;
|
||||
|
||||
// next strip has same winding
|
||||
// ? a b => b a v
|
||||
strip[1] = v;
|
||||
|
||||
next = swap;
|
||||
}
|
||||
else
|
||||
{
|
||||
// emit the next vertex in the strip
|
||||
destination[strip_size++] = v;
|
||||
|
||||
// next strip has flipped winding
|
||||
strip[0] = strip[1];
|
||||
strip[1] = v;
|
||||
parity ^= 1;
|
||||
|
||||
next = cont;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we didn't find anything, we need to find the next new triangle
|
||||
// we use a heuristic to maximize the strip length
|
||||
unsigned int i = findStripFirst(buffer, buffer_size, &valence[0]);
|
||||
unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2];
|
||||
|
||||
// ordered removal from the buffer
|
||||
memmove(buffer[i], buffer[i + 1], (buffer_size - i - 1) * sizeof(buffer[0]));
|
||||
buffer_size--;
|
||||
|
||||
// update vertex valences for strip start heuristic
|
||||
valence[a]--;
|
||||
valence[b]--;
|
||||
valence[c]--;
|
||||
|
||||
// we need to pre-rotate the triangle so that we will find a match in the existing buffer on the next iteration
|
||||
int ea = findStripNext(buffer, buffer_size, c, b);
|
||||
int eb = findStripNext(buffer, buffer_size, a, c);
|
||||
int ec = findStripNext(buffer, buffer_size, b, a);
|
||||
|
||||
// in some cases we can have several matching edges; since we can pick any edge, we pick the one with the smallest
|
||||
// triangle index in the buffer. this reduces the effect of stripification on ACMR and additionally - for unclear
|
||||
// reasons - slightly improves the stripification efficiency
|
||||
int mine = INT_MAX;
|
||||
mine = (ea >= 0 && mine > ea) ? ea : mine;
|
||||
mine = (eb >= 0 && mine > eb) ? eb : mine;
|
||||
mine = (ec >= 0 && mine > ec) ? ec : mine;
|
||||
|
||||
if (ea == mine)
|
||||
{
|
||||
// keep abc
|
||||
next = ea;
|
||||
}
|
||||
else if (eb == mine)
|
||||
{
|
||||
// abc -> bca
|
||||
unsigned int t = a;
|
||||
a = b, b = c, c = t;
|
||||
|
||||
next = eb;
|
||||
}
|
||||
else if (ec == mine)
|
||||
{
|
||||
// abc -> cab
|
||||
unsigned int t = c;
|
||||
c = b, b = a, a = t;
|
||||
|
||||
next = ec;
|
||||
}
|
||||
|
||||
if (restart_index)
|
||||
{
|
||||
if (strip_size)
|
||||
destination[strip_size++] = restart_index;
|
||||
|
||||
destination[strip_size++] = a;
|
||||
destination[strip_size++] = b;
|
||||
destination[strip_size++] = c;
|
||||
|
||||
// new strip always starts with the same edge winding
|
||||
strip[0] = b;
|
||||
strip[1] = c;
|
||||
parity = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (strip_size)
|
||||
{
|
||||
// connect last strip using degenerate triangles
|
||||
destination[strip_size++] = strip[1];
|
||||
destination[strip_size++] = a;
|
||||
}
|
||||
|
||||
// note that we may need to flip the emitted triangle based on parity
|
||||
// we always end up with outgoing edge "cb" in the end
|
||||
unsigned int e0 = parity ? c : b;
|
||||
unsigned int e1 = parity ? b : c;
|
||||
|
||||
destination[strip_size++] = a;
|
||||
destination[strip_size++] = e0;
|
||||
destination[strip_size++] = e1;
|
||||
|
||||
strip[0] = e0;
|
||||
strip[1] = e1;
|
||||
parity ^= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strip_size;
|
||||
}
|
||||
|
||||
size_t meshopt_stripifyBound(size_t index_count)
|
||||
{
|
||||
assert(index_count % 3 == 0);
|
||||
|
||||
// worst case without restarts is 2 degenerate indices and 3 indices per triangle
|
||||
// worst case with restarts is 1 restart index and 3 indices per triangle
|
||||
return (index_count / 3) * 5;
|
||||
}
|
||||
|
||||
size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index)
|
||||
{
|
||||
assert(destination != indices);
|
||||
|
||||
size_t offset = 0;
|
||||
size_t start = 0;
|
||||
|
||||
for (size_t i = 0; i < index_count; ++i)
|
||||
{
|
||||
if (restart_index && indices[i] == restart_index)
|
||||
{
|
||||
start = i + 1;
|
||||
}
|
||||
else if (i - start >= 2)
|
||||
{
|
||||
unsigned int a = indices[i - 2], b = indices[i - 1], c = indices[i];
|
||||
|
||||
// flip winding for odd triangles
|
||||
if ((i - start) & 1)
|
||||
{
|
||||
unsigned int t = a;
|
||||
a = b, b = t;
|
||||
}
|
||||
|
||||
// although we use restart indices, strip swaps still produce degenerate triangles, so skip them
|
||||
if (a != b && a != c && b != c)
|
||||
{
|
||||
destination[offset + 0] = a;
|
||||
destination[offset + 1] = b;
|
||||
destination[offset + 2] = c;
|
||||
offset += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
size_t meshopt_unstripifyBound(size_t index_count)
|
||||
{
|
||||
assert(index_count == 0 || index_count >= 3);
|
||||
|
||||
return (index_count == 0) ? 0 : (index_count - 2) * 3;
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "../KKdLib/interpolation.hpp"
|
||||
#include "../KKdLib/sort.hpp"
|
||||
#include "../KKdLib/str_utils.hpp"
|
||||
#include <meshoptimizer/meshoptimizer.h>
|
||||
#if BAKE_PNG
|
||||
#include <lodepng/lodepng.h>
|
||||
#endif
|
||||
@@ -10181,11 +10182,19 @@ static void x_pv_game_split_auth_3d_material_list(x_pv_game* xpvgm,
|
||||
set->obj_num = _obj_num;
|
||||
|
||||
obj_vertex_buffer* vertex_buffer_data = handler->vertex_buffer_data;
|
||||
for (uint32_t j = 0; j < obj_num; j++)
|
||||
vertex_buffer_data[j].unload();
|
||||
delete[] vertex_buffer_data;
|
||||
|
||||
handler->vertex_buffer_num = _obj_num;
|
||||
obj_vertex_buffer* vertex_buffer_data_new = new obj_vertex_buffer[_obj_num];
|
||||
handler->vertex_buffer_data = vertex_buffer_data_new;
|
||||
|
||||
obj_index_buffer* index_buffer_data = handler->index_buffer_data;
|
||||
for (uint32_t j = 0; j < obj_num; j++)
|
||||
index_buffer_data[j].unload();
|
||||
delete[] index_buffer_data;
|
||||
|
||||
handler->index_buffer_num = _obj_num;
|
||||
obj_index_buffer* index_buffer_data_new = new obj_index_buffer[_obj_num];
|
||||
handler->index_buffer_data = index_buffer_data_new;
|
||||
@@ -10194,11 +10203,38 @@ static void x_pv_game_split_auth_3d_material_list(x_pv_game* xpvgm,
|
||||
vertex_buffer_data_new++, index_buffer_data_new++) {
|
||||
*obj_data_new = *obj_data;
|
||||
|
||||
bool init_buffer = false;
|
||||
*vertex_buffer_data_new = vertex_buffer_data[j];
|
||||
*index_buffer_data_new = index_buffer_data[j];
|
||||
|
||||
obj* obj = *obj_data;
|
||||
obj_mesh* mesh_array = obj->mesh_array;
|
||||
uint32_t num_mesh = obj->num_mesh;
|
||||
for (uint32_t m = 0; m < num_mesh; m++) {
|
||||
obj_mesh* mesh = &mesh_array[m];
|
||||
|
||||
obj_sub_mesh* submesh_array = mesh->submesh_array;
|
||||
uint32_t num_submesh = mesh->num_submesh;
|
||||
for (uint32_t n = 0; n < num_submesh; n++) {
|
||||
obj_sub_mesh* submesh = &submesh_array[n];
|
||||
|
||||
if (submesh->primitive_type != OBJ_PRIMITIVE_TRIANGLES)
|
||||
continue;
|
||||
|
||||
submesh->primitive_type = OBJ_PRIMITIVE_TRIANGLE_STRIP;
|
||||
|
||||
uint32_t* index_array = submesh->index_array;
|
||||
submesh->index_array = alloc->allocate<uint32_t>(
|
||||
meshopt_stripifyBound(submesh->num_index));
|
||||
submesh->num_index = (uint32_t)meshopt_stripify(submesh->index_array,
|
||||
index_array, submesh->num_index, mesh->num_vertex, 0xFFFFFFFF);
|
||||
|
||||
submesh->first_index = 0;
|
||||
submesh->last_index = 0;
|
||||
submesh->index_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool init_buffer = false;
|
||||
vertex_buffer_data_new->load(*obj_data_new);
|
||||
index_buffer_data_new->load(*obj_data_new);
|
||||
|
||||
for (auto& k : material_colors) {
|
||||
uint32_t material_index = -1;
|
||||
uint32_t num_material = obj->num_material;
|
||||
@@ -10376,9 +10412,6 @@ static void x_pv_game_split_auth_3d_material_list(x_pv_game* xpvgm,
|
||||
|
||||
obj_db.update();
|
||||
|
||||
delete[] vertex_buffer_data;
|
||||
delete[] index_buffer_data;
|
||||
|
||||
obj_data = set->obj_data;
|
||||
|
||||
handler->obj_id_data.clear();
|
||||
|
||||
@@ -318,3 +318,29 @@ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
meshoptimizer
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-2023 Arseny Kapoulkine
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Reference in New Issue
Block a user