feat: LOD 网格分级——meshopt 解析期简化 + 按距离/级联选级
vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh 多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间 选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半, 相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。 SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod): 主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联 按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。 A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off): - 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异 - 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异 结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施 保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy GPU 时间线定位真实瓶颈。 坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加 (buffer 无重复数据,累加必越界——VUID-08798 抓的)。
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
#include "../src/meshoptimizer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
|
||||
double timestamp()
|
||||
{
|
||||
return emscripten_get_now() * 1e-3;
|
||||
}
|
||||
#elif defined(_WIN32)
|
||||
struct LARGE_INTEGER
|
||||
{
|
||||
__int64 QuadPart;
|
||||
};
|
||||
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(LARGE_INTEGER* lpPerformanceCount);
|
||||
extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency);
|
||||
|
||||
double timestamp()
|
||||
{
|
||||
LARGE_INTEGER freq, counter;
|
||||
QueryPerformanceFrequency(&freq);
|
||||
QueryPerformanceCounter(&counter);
|
||||
return double(counter.QuadPart) / double(freq.QuadPart);
|
||||
}
|
||||
#else
|
||||
double timestamp()
|
||||
{
|
||||
timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return double(ts.tv_sec) + 1e-9 * double(ts.tv_nsec);
|
||||
}
|
||||
#endif
|
||||
|
||||
struct Vertex
|
||||
{
|
||||
uint16_t data[16];
|
||||
};
|
||||
|
||||
uint32_t murmur3(uint32_t h)
|
||||
{
|
||||
h ^= h >> 16;
|
||||
h *= 0x85ebca6bu;
|
||||
h ^= h >> 13;
|
||||
h *= 0xc2b2ae35u;
|
||||
h ^= h >> 16;
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
void benchCodecs(const std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices, double& bestvd, double& bestid, bool verbose)
|
||||
{
|
||||
std::vector<Vertex> vb(vertices.size());
|
||||
std::vector<unsigned int> ib(indices.size());
|
||||
|
||||
std::vector<unsigned char> vc(meshopt_encodeVertexBufferBound(vertices.size(), sizeof(Vertex)));
|
||||
std::vector<unsigned char> ic(meshopt_encodeIndexBufferBound(indices.size(), vertices.size()));
|
||||
|
||||
if (verbose)
|
||||
printf("source: vertex data %d bytes, index data %d bytes\n", int(vertices.size() * sizeof(Vertex)), int(indices.size() * 4));
|
||||
|
||||
meshopt_optimizeVertexCache(&ib[0], &indices[0], indices.size(), vertices.size());
|
||||
|
||||
meshopt_optimizeVertexFetch(&vb[0], &ib[0], indices.size(), &vertices[0], vertices.size(), sizeof(Vertex));
|
||||
|
||||
vc.resize(vc.capacity());
|
||||
vc.resize(meshopt_encodeVertexBuffer(&vc[0], vc.size(), &vb[0], vertices.size(), sizeof(Vertex)));
|
||||
|
||||
ic.resize(ic.capacity());
|
||||
ic.resize(meshopt_encodeIndexBuffer(&ic[0], ic.size(), &ib[0], indices.size()));
|
||||
|
||||
if (verbose)
|
||||
printf("encode: vertex data %d bytes, index data %d bytes\n", int(vc.size()), int(ic.size()));
|
||||
|
||||
for (int attempt = 0; attempt < 50; ++attempt)
|
||||
{
|
||||
double t0 = timestamp();
|
||||
|
||||
int rv = meshopt_decodeVertexBuffer(&vb[0], vertices.size(), sizeof(Vertex), &vc[0], vc.size());
|
||||
assert(rv == 0);
|
||||
(void)rv;
|
||||
|
||||
double t1 = timestamp();
|
||||
|
||||
int ri = meshopt_decodeIndexBuffer(&ib[0], indices.size(), 4, &ic[0], ic.size());
|
||||
assert(ri == 0);
|
||||
(void)ri;
|
||||
|
||||
double t2 = timestamp();
|
||||
|
||||
if (verbose)
|
||||
printf("decode: vertex %.2f ms (%.2f GB/sec), index %.2f ms (%.2f GB/sec)\n",
|
||||
(t1 - t0) * 1000, double(vertices.size() * sizeof(Vertex)) / 1e9 / (t1 - t0),
|
||||
(t2 - t1) * 1000, double(indices.size() * 4) / 1e9 / (t2 - t1));
|
||||
|
||||
bestvd = std::max(bestvd, double(vertices.size() * sizeof(Vertex)) / 1e9 / (t1 - t0));
|
||||
bestid = std::max(bestid, double(indices.size() * 4) / 1e9 / (t2 - t1));
|
||||
}
|
||||
}
|
||||
|
||||
void benchFilters(size_t count, double& besto8, double& besto12, double& bestq12, double& bestc8, double& bestc12, double& bestexp, bool verbose)
|
||||
{
|
||||
// note: the filters are branchless so we just run them on runs of zeroes
|
||||
size_t count4 = (count + 3) & ~3;
|
||||
std::vector<unsigned char> d4(count4 * 4);
|
||||
std::vector<unsigned char> d8(count4 * 8);
|
||||
|
||||
if (verbose)
|
||||
printf("filters: oct8 data %d bytes, oct12/quat12 data %d bytes\n", int(d4.size()), int(d8.size()));
|
||||
|
||||
for (int attempt = 0; attempt < 50; ++attempt)
|
||||
{
|
||||
double t0 = timestamp();
|
||||
|
||||
meshopt_decodeFilterOct(&d4[0], count4, 4);
|
||||
|
||||
double t1 = timestamp();
|
||||
|
||||
meshopt_decodeFilterOct(&d8[0], count4, 8);
|
||||
|
||||
double t2 = timestamp();
|
||||
|
||||
meshopt_decodeFilterQuat(&d8[0], count4, 8);
|
||||
|
||||
double t3 = timestamp();
|
||||
|
||||
meshopt_decodeFilterColor(&d4[0], count4, 4);
|
||||
|
||||
double t4 = timestamp();
|
||||
|
||||
meshopt_decodeFilterColor(&d8[0], count4, 8);
|
||||
|
||||
double t5 = timestamp();
|
||||
|
||||
meshopt_decodeFilterExp(&d8[0], count4, 8);
|
||||
|
||||
double t6 = timestamp();
|
||||
|
||||
if (verbose)
|
||||
printf("filter: oct8 %.2f ms (%.2f GB/sec), oct12 %.2f ms (%.2f GB/sec), quat12 %.2f ms (%.2f GB/sec), col8 %.2f ms (%.2f GB/sec), col12 %.2f ms (%.2f GB/sec), exp %.2f ms (%.2f GB/sec)\n",
|
||||
(t1 - t0) * 1000, double(d4.size()) / 1e9 / (t1 - t0),
|
||||
(t2 - t1) * 1000, double(d8.size()) / 1e9 / (t2 - t1),
|
||||
(t3 - t2) * 1000, double(d8.size()) / 1e9 / (t3 - t2),
|
||||
(t4 - t3) * 1000, double(d4.size()) / 1e9 / (t4 - t3),
|
||||
(t5 - t4) * 1000, double(d8.size()) / 1e9 / (t5 - t4),
|
||||
(t6 - t5) * 1000, double(d8.size()) / 1e9 / (t6 - t5));
|
||||
|
||||
besto8 = std::max(besto8, double(d4.size()) / 1e9 / (t1 - t0));
|
||||
besto12 = std::max(besto12, double(d8.size()) / 1e9 / (t2 - t1));
|
||||
bestq12 = std::max(bestq12, double(d8.size()) / 1e9 / (t3 - t2));
|
||||
bestc8 = std::max(bestc8, double(d4.size()) / 1e9 / (t4 - t3));
|
||||
bestc12 = std::max(bestc12, double(d8.size()) / 1e9 / (t5 - t4));
|
||||
bestexp = std::max(bestexp, double(d8.size()) / 1e9 / (t6 - t5));
|
||||
}
|
||||
}
|
||||
|
||||
void benchMeshlets(const std::vector<float>& positions, const std::vector<unsigned int>& indices, bool encoderefs, double& bestml, bool verbose)
|
||||
{
|
||||
const size_t max_vertices = 64;
|
||||
const size_t max_triangles = 96;
|
||||
|
||||
size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles);
|
||||
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
|
||||
std::vector<unsigned int> meshlet_vertices(indices.size());
|
||||
std::vector<unsigned char> meshlet_triangles(indices.size());
|
||||
|
||||
meshlets.resize(meshopt_buildMeshlets(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(),
|
||||
indices.data(), indices.size(), positions.data(), positions.size() / 3, sizeof(float) * 3, max_vertices, max_triangles, 0.f));
|
||||
|
||||
const meshopt_Meshlet& last = meshlets.back();
|
||||
|
||||
meshlet_vertices.resize(last.vertex_offset + last.vertex_count);
|
||||
meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3);
|
||||
|
||||
std::vector<unsigned char> cbuf(meshopt_encodeMeshletBound(max_vertices, max_triangles));
|
||||
|
||||
// optimize each meshlet for locality before encoding
|
||||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||||
meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlets[i].vertex_offset], meshlets[i].vertex_count, &meshlet_triangles[meshlets[i].triangle_offset], meshlets[i].triangle_count, 3);
|
||||
|
||||
// optimize meshlet_vertices for locality (requires vertex reorder)
|
||||
std::vector<float> positionscopy(positions.size());
|
||||
meshopt_optimizeVertexFetch(&positionscopy[0], &meshlet_vertices[0], meshlet_vertices.size(), &positions[0], positions.size() / 3, sizeof(float) * 3);
|
||||
|
||||
#if 0 // TODO: disabled for now to make meshlet decoder tuning more accurate; might make sense to re-enable in the future for SOL timings
|
||||
// pack meshlets in decreasing triangle count order to reduce branch mispredictions
|
||||
// (this matters less on real-world meshes but is more prominent on regular grids?)
|
||||
std::sort(meshlets.begin(), meshlets.end(), [](const meshopt_Meshlet& lhs, const meshopt_Meshlet& rhs)
|
||||
{ return lhs.triangle_count > rhs.triangle_count; });
|
||||
#endif
|
||||
|
||||
std::vector<unsigned char> packed;
|
||||
|
||||
size_t totals = 0;
|
||||
|
||||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||||
{
|
||||
const meshopt_Meshlet& meshlet = meshlets[i];
|
||||
|
||||
size_t mvc = encoderefs ? meshlet.vertex_count : 0;
|
||||
|
||||
size_t mbs = meshopt_encodeMeshlet(&cbuf[0], cbuf.size(), &meshlet_vertices[meshlet.vertex_offset], mvc, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count);
|
||||
assert(mbs > 0);
|
||||
|
||||
packed.push_back((unsigned char)mvc);
|
||||
packed.push_back((unsigned char)meshlet.triangle_count);
|
||||
packed.push_back((unsigned char)(mbs & 0xff));
|
||||
packed.push_back((unsigned char)((mbs >> 8) & 0xff));
|
||||
packed.insert(packed.end(), cbuf.begin(), cbuf.begin() + mbs);
|
||||
|
||||
totals += size_t(meshlet.triangle_count) * 3 + size_t(mvc) * 4;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
printf("meshlets (%d/%d): %d meshlets, packed %d bytes (%.1f bits/triangle)\n",
|
||||
int(max_vertices), int(max_triangles), int(meshlets.size()), int(packed.size()), double(packed.size() * 8) / double(indices.size() / 3));
|
||||
|
||||
unsigned int rv[256];
|
||||
unsigned char rt[256 * 3];
|
||||
|
||||
for (int attempt = 0; attempt < 50; ++attempt)
|
||||
{
|
||||
double t0 = timestamp();
|
||||
|
||||
const unsigned char* p = &packed[0];
|
||||
|
||||
for (size_t i = 0; i < meshlets.size(); ++i)
|
||||
{
|
||||
size_t mbs = p[2] | (p[3] << 8);
|
||||
int rc = meshopt_decodeMeshlet(rv, p[0], rt, p[1], p + 4, mbs);
|
||||
assert(rc == 0);
|
||||
(void)rc;
|
||||
p += 4 + mbs;
|
||||
}
|
||||
|
||||
assert(p == &packed[0] + packed.size());
|
||||
|
||||
double t1 = timestamp();
|
||||
|
||||
if (verbose)
|
||||
printf("meshlet decode: %.2f ms (%.2f GB/sec)\n", (t1 - t0) * 1000, double(totals) / 1e9 / (t1 - t0));
|
||||
|
||||
bestml = std::max(bestml, double(totals) / 1e9 / (t1 - t0));
|
||||
}
|
||||
}
|
||||
|
||||
struct File
|
||||
{
|
||||
std::vector<unsigned char> data;
|
||||
size_t stride;
|
||||
size_t count;
|
||||
bool index;
|
||||
};
|
||||
|
||||
File readFile(const char* path)
|
||||
{
|
||||
FILE* file = fopen(path, "rb");
|
||||
assert(file);
|
||||
|
||||
const char* name = strrchr(path, '/');
|
||||
name = name ? name + 1 : path;
|
||||
|
||||
std::vector<unsigned char> input;
|
||||
unsigned char buffer[4096];
|
||||
size_t bytes_read;
|
||||
|
||||
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0)
|
||||
input.insert(input.end(), buffer, buffer + bytes_read);
|
||||
|
||||
fclose(file);
|
||||
|
||||
int vcnt, vsz, icnt;
|
||||
|
||||
if (sscanf(name, "v%d_s%d_", &vcnt, &vsz) == 2)
|
||||
{
|
||||
File result = {};
|
||||
result.count = vcnt;
|
||||
result.stride = vsz;
|
||||
|
||||
std::vector<unsigned char> decoded(result.count * result.stride);
|
||||
int res = meshopt_decodeVertexBuffer(&decoded[0], result.count, result.stride, &input[0], input.size());
|
||||
if (res != 0 && input.size() == decoded.size())
|
||||
{
|
||||
// some files are not encoded
|
||||
memcpy(decoded.data(), input.data(), decoded.size());
|
||||
}
|
||||
|
||||
result.data.resize(meshopt_encodeVertexBufferBound(result.count, result.stride));
|
||||
result.data.resize(meshopt_encodeVertexBuffer(result.data.data(), result.data.size(), decoded.data(), result.count, result.stride));
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (sscanf(name, "i%d_", &icnt) == 1)
|
||||
{
|
||||
File result = {};
|
||||
result.count = icnt;
|
||||
result.stride = icnt <= 0xffff ? 2 : 4; // note: technically 2-byte indices can be used for larger meshes in some cases, but we simplify
|
||||
result.index = true;
|
||||
result.data = input;
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(!"Unrecognized file name");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
bool verbose = false;
|
||||
bool loop = false;
|
||||
bool inputs = false;
|
||||
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (strcmp(argv[i], "-v") == 0)
|
||||
verbose = true;
|
||||
if (strcmp(argv[i], "-l") == 0)
|
||||
loop = true;
|
||||
if (argv[i][0] != '-')
|
||||
inputs = true;
|
||||
}
|
||||
|
||||
if (inputs)
|
||||
{
|
||||
std::vector<File> files;
|
||||
for (int i = 1; i < argc; ++i)
|
||||
if (argv[i][0] != '-')
|
||||
files.push_back(readFile(argv[i]));
|
||||
|
||||
size_t max_size = 0;
|
||||
size_t total_size = 0;
|
||||
size_t total_comp = 0;
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
max_size = std::max(max_size, files[i].count * files[i].stride);
|
||||
total_size += files[i].count * files[i].stride;
|
||||
total_comp += files[i].data.size();
|
||||
}
|
||||
|
||||
printf("%d files, ratio %.2f (%.2f MB => %.2f MB)\n",
|
||||
int(files.size()), double(total_comp) / double(total_size), double(total_size) / 1024 / 1024, double(total_comp) / 1024 / 1024);
|
||||
|
||||
std::vector<unsigned char> buffer(max_size);
|
||||
|
||||
for (int l = 0; l < (loop ? 100 : 1); ++l)
|
||||
{
|
||||
double bestvd = 0;
|
||||
|
||||
for (int attempt = 0; attempt < 50; ++attempt)
|
||||
{
|
||||
double t0 = timestamp();
|
||||
|
||||
for (const File& file : files)
|
||||
{
|
||||
int rc =
|
||||
file.index
|
||||
? meshopt_decodeIndexBuffer(&buffer[0], file.count, int(file.stride), &file.data[0], file.data.size())
|
||||
: meshopt_decodeVertexBuffer(&buffer[0], file.count, file.stride, &file.data[0], file.data.size());
|
||||
|
||||
if (rc != 0)
|
||||
{
|
||||
fprintf(stderr, "Failed to decode %zu bytes into %zu elements with stride %d (index %d)\n", file.data.size(), file.count, int(file.stride), int(file.index));
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
double t1 = timestamp();
|
||||
|
||||
bestvd = std::max(bestvd, double(total_size) / 1e9 / (t1 - t0));
|
||||
}
|
||||
|
||||
printf("Score (GB/s):\t%.2f\n", bestvd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int N = 1000;
|
||||
|
||||
std::vector<Vertex> vertices;
|
||||
vertices.reserve((N + 1) * (N + 1));
|
||||
|
||||
std::vector<float> positions((N + 1) * (N + 1) * 3);
|
||||
|
||||
for (int x = 0; x <= N; ++x)
|
||||
{
|
||||
for (int y = 0; y <= N; ++y)
|
||||
{
|
||||
Vertex v;
|
||||
|
||||
for (int k = 0; k < 16; ++k)
|
||||
{
|
||||
uint32_t h = murmur3((x * (N + 1) + y) * 16 + k);
|
||||
|
||||
// use random k-bit sequence for each word to test all encoding types
|
||||
// note: this doesn't stress the sentinel logic too much but it's all branchless so it's probably fine?
|
||||
v.data[k] = h & ((1 << (k + 1)) - 1);
|
||||
}
|
||||
|
||||
size_t index = vertices.size();
|
||||
|
||||
positions[index * 3 + 0] = float(x);
|
||||
positions[index * 3 + 1] = float(y);
|
||||
positions[index * 3 + 2] = 0.f;
|
||||
|
||||
vertices.push_back(v);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned int> indices;
|
||||
indices.reserve(N * N * 6);
|
||||
|
||||
for (int x = 0; x < N; ++x)
|
||||
{
|
||||
for (int y = 0; y < N; ++y)
|
||||
{
|
||||
indices.push_back((x + 0) * (N + 1) + (y + 0));
|
||||
indices.push_back((x + 1) * (N + 1) + (y + 0));
|
||||
indices.push_back((x + 0) * (N + 1) + (y + 1));
|
||||
|
||||
indices.push_back((x + 0) * (N + 1) + (y + 1));
|
||||
indices.push_back((x + 1) * (N + 1) + (y + 0));
|
||||
indices.push_back((x + 1) * (N + 1) + (y + 1));
|
||||
}
|
||||
}
|
||||
|
||||
printf("Codec:\tvtx\tidx\tmlet\tmletΔ\toct8\toct12\tquat12\tcol8\tcol12\texp\n");
|
||||
|
||||
for (int l = 0; l < (loop ? 100 : 1); ++l)
|
||||
{
|
||||
double bestvd = 0, bestid = 0;
|
||||
benchCodecs(vertices, indices, bestvd, bestid, verbose);
|
||||
|
||||
double bestml = 0, bestmt = 0;
|
||||
benchMeshlets(positions, indices, /* encoderefs= */ true, bestml, verbose);
|
||||
benchMeshlets(positions, indices, /* encoderefs= */ false, bestmt, verbose);
|
||||
|
||||
double besto8 = 0, besto12 = 0, bestq12 = 0, bestc8 = 0, bestc12 = 0, bestexp = 0;
|
||||
benchFilters(8 * N * N, besto8, besto12, bestq12, bestc8, bestc12, bestexp, verbose);
|
||||
|
||||
printf("GB/s :\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\n",
|
||||
bestvd, bestid, bestml, bestmt, besto8, besto12, bestq12, bestc8, bestc12, bestexp);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user