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,209 @@
|
||||
/**
|
||||
* meshletdec.slang - an example GPU decoder for meshlet data encoded using meshopt_encodeMeshlet
|
||||
* This is intended to be used as a starting point for applications that want to decode meshlet data on the GPU.
|
||||
*
|
||||
* The shader exposes an entrypoint, decodeMeshlets, that decodes a set of meshlets; each meshlet is decoded independently,
|
||||
* and the output vertex/triangle data is written as uint32 per element (triangle data is written as 0xccbbaa).
|
||||
* This matches the output format for meshopt_decodeMeshlet with vertex_size=4 triangle_size=4. If alternative formats are
|
||||
* needed, the code should be changed to output them; note that for triangle data, it may make sense to output data to shared
|
||||
* memory to be able to use larger aligned 32-bit writes to global memory after that.
|
||||
*
|
||||
* Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
|
||||
* This code is distributed under the MIT License. See notice at the end of this file.
|
||||
*/
|
||||
|
||||
struct MeshletDesc
|
||||
{
|
||||
uint stream_offset;
|
||||
uint output_offset;
|
||||
uint16_t encoded_size;
|
||||
uint8_t vertex_count;
|
||||
uint8_t triangle_count;
|
||||
};
|
||||
|
||||
[[vk::binding(0)]]
|
||||
StructuredBuffer<uint8_t> gStream : register(t0);
|
||||
[[vk::binding(1)]]
|
||||
StructuredBuffer<MeshletDesc> gMeshlets : register(t1);
|
||||
[[vk::binding(2)]]
|
||||
RWStructuredBuffer<uint> gOutput : register(u2);
|
||||
[[vk::binding(3)]]
|
||||
cbuffer MeshletConfigCB : register(b3) { uint gMeshletCount; }
|
||||
|
||||
uint decodeVertices(uint out_vertices, uint ctrl, uint data, uint bound, uint vertex_count)
|
||||
{
|
||||
uint last = ~0u;
|
||||
|
||||
for (uint i = 0; i < vertex_count; i += 4)
|
||||
{
|
||||
if (data > bound)
|
||||
return ~0u;
|
||||
|
||||
uint code4 = uint(gStream[ctrl + i / 4]);
|
||||
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
int code = ((code4 >> k) & 1) | ((code4 >> (k + 3)) & 2);
|
||||
int length = code4 == 0xff ? 4 : code;
|
||||
|
||||
// branchlessly read up to 4 bytes
|
||||
uint mask = (length == 4) ? ~0u : (1 << (8 * length)) - 1;
|
||||
uint v = (uint(gStream[data + 0]) | (uint(gStream[data + 1]) << 8) | (uint(gStream[data + 2]) << 16) | (uint(gStream[data + 3]) << 24)) & mask;
|
||||
|
||||
// unzigzag + 1
|
||||
uint d = (v >> 1) ^ -int(v & 1);
|
||||
uint r = last + d + 1;
|
||||
|
||||
if (i + k < vertex_count)
|
||||
gOutput[out_vertices + i + k] = r;
|
||||
|
||||
data += length;
|
||||
last = r;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
uint decodeTriangle(uint code, uint extra0, uint extra1, uint extra2, inout uint fifo0, inout uint fifo1, inout uint fifo2, inout uint next, inout uint extra)
|
||||
{
|
||||
// reuse: 0-1 extra vertices
|
||||
uint fifo = code < 4 ? fifo0 : (code < 8 ? fifo1 : fifo2);
|
||||
uint edge = fifo >> ((code << 3) & 16); // shift by 16 if bit 1 is set (odd edge for each triangle)
|
||||
uint c_reuse = (code & 1) == 1 ? extra0 : next;
|
||||
|
||||
// restart: 0-3 extra vertices
|
||||
uint extran = code & 3;
|
||||
uint a = extran > 0 ? extra0 : next;
|
||||
uint b = extran > 1 ? extra1 : next + (1 - extran);
|
||||
uint c = extran > 2 ? extra2 : next + (2 - extran);
|
||||
|
||||
// select between reuse and restart and repack triangle into edge format (0xcbac)
|
||||
a = code >= 12 ? a : (edge >> 8) & 0xff;
|
||||
b = code >= 12 ? b : edge & 0xff;
|
||||
c = code >= 12 ? c : c_reuse;
|
||||
|
||||
uint tri = c | (a << 8) | (b << 16) | (c << 24);
|
||||
|
||||
// advance next/extra; reuse codes use 1 lsb for extra count, restart codes use 2 lsbs
|
||||
uint extrab = code < 12 ? 1 : 3;
|
||||
next += extrab - (code & extrab);
|
||||
extra += code & extrab;
|
||||
|
||||
// rotate fifo
|
||||
fifo2 = fifo1;
|
||||
fifo1 = fifo0;
|
||||
fifo0 = tri;
|
||||
|
||||
// output triangle is stored without extra edge vertex (0xcbac => 0xcba)
|
||||
return tri >> 8;
|
||||
}
|
||||
|
||||
uint decodeTriangles(uint out_triangles, uint codes, uint extra, uint bound, uint triangle_count)
|
||||
{
|
||||
uint next = 0;
|
||||
uint fifo0 = 0, fifo1 = 0, fifo2 = 0; // two edge fifo entries in one uint: 0xcbac
|
||||
|
||||
for (uint i = 0; i < triangle_count; i += 2)
|
||||
{
|
||||
if (extra > bound)
|
||||
return ~0u;
|
||||
|
||||
uint codeg = uint(gStream[codes + i / 2]);
|
||||
|
||||
// first triangle
|
||||
uint extra0 = uint(gStream[extra + 0]);
|
||||
uint extra1 = uint(gStream[extra + 1]);
|
||||
uint extra2 = uint(gStream[extra + 2]);
|
||||
uint tri = decodeTriangle(codeg & 15, extra0, extra1, extra2, fifo0, fifo1, fifo2, next, extra);
|
||||
|
||||
gOutput[out_triangles + i] = tri;
|
||||
|
||||
// second triangle, if any
|
||||
extra0 = uint(gStream[extra + 0]);
|
||||
extra1 = uint(gStream[extra + 1]);
|
||||
extra2 = uint(gStream[extra + 2]);
|
||||
tri = decodeTriangle(codeg >> 4, extra0, extra1, extra2, fifo0, fifo1, fifo2, next, extra);
|
||||
|
||||
if (i + 1 < triangle_count)
|
||||
gOutput[out_triangles + i + 1] = tri;
|
||||
}
|
||||
|
||||
return extra;
|
||||
}
|
||||
|
||||
int decodeMeshlet(uint out_vertices, uint vertex_count, uint out_triangles, uint triangle_count, uint buffer, uint buffer_size)
|
||||
{
|
||||
uint codes_size = (triangle_count + 1) / 2;
|
||||
uint ctrl_size = (vertex_count + 3) / 4;
|
||||
uint gap_size = (codes_size + ctrl_size < 16) ? 16 - (codes_size + ctrl_size) : 0;
|
||||
|
||||
if (buffer_size < codes_size + ctrl_size + gap_size)
|
||||
return -2;
|
||||
|
||||
uint end = buffer + buffer_size;
|
||||
uint codes = end - codes_size;
|
||||
uint ctrl = codes - ctrl_size;
|
||||
uint data = buffer;
|
||||
|
||||
// gap ensures we have at least 16 bytes available after bound; this allows decoder to over-read safely
|
||||
uint bound = ctrl - gap_size;
|
||||
|
||||
data = decodeVertices(out_vertices, ctrl, data, bound, vertex_count);
|
||||
if (data == ~0u)
|
||||
return -2;
|
||||
|
||||
data = decodeTriangles(out_triangles, codes, data, bound, triangle_count);
|
||||
if (data == ~0u)
|
||||
return -2;
|
||||
|
||||
return (data == bound) ? 0 : -3;
|
||||
}
|
||||
|
||||
[shader("compute")]
|
||||
[numthreads(32, 1, 1)]
|
||||
void decodeMeshlets(uint3 dispatch_thread_id: SV_DispatchThreadID)
|
||||
{
|
||||
uint meshlet_count = gMeshletCount;
|
||||
|
||||
uint tid = dispatch_thread_id.x;
|
||||
if (tid >= meshlet_count)
|
||||
return;
|
||||
|
||||
MeshletDesc desc = gMeshlets[tid];
|
||||
uint out_vertices = desc.output_offset;
|
||||
uint out_triangles = desc.output_offset + uint(desc.vertex_count);
|
||||
|
||||
int rc = decodeMeshlet(out_vertices, uint(desc.vertex_count), out_triangles, uint(desc.triangle_count), desc.stream_offset, uint(desc.encoded_size));
|
||||
|
||||
// if decoding failed, we write 0xff.. to the first word of the output data
|
||||
// this can be adjusted arbitrarily; for example, a separate buffer with a single status for the entire stream could be used
|
||||
// note that decoding fails only if the input data is corrupt; so this may not be required at all depending on the requirements
|
||||
if (rc < 0)
|
||||
gOutput[desc.output_offset] = ~0u;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copyright (c) 2016-2026 Arseny Kapoulkine
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person
|
||||
* obtaining a copy of this software and associated documentation
|
||||
* 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