Files
2025-11-30 08:35:03 +02:00

383 lines
13 KiB
C#

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Assertions;
namespace Meshoptimizer
{
[BurstCompile]
unsafe struct DecodeVertexJob : IJob
{
#region Constants
const float k_SqrtHalf = 0.707106781186548f;
const byte k_VertexHeader = 0xa0;
const uint k_KTailMaxSize = 32;
#endregion
// [WriteOnly] // TODO: Make filtering a separate job and add WriteOnly attribute
public NativeArray<byte> destination;
[ReadOnly]
public NativeSlice<byte> source;
public uint vertexCount;
public uint vertexSize;
public Filter filter;
[WriteOnly]
[NativeDisableContainerSafetyRestriction]
public NativeSlice<int> returnCode;
public void Execute()
{
Assert.IsTrue(vertexSize > 0 && vertexSize <= 256);
Assert.AreEqual(0, vertexSize % 4);
var vertexData = (byte*)destination.GetUnsafePtr();
var data = (byte*)source.GetUnsafeReadOnlyPtr();
var dataEnd = data + source.Length;
if ((uint)(dataEnd - data) < 1 + vertexSize)
{
returnCode[0] = -2;
return;
}
var dataHeader = *data++;
if ((dataHeader & 0xf0) != k_VertexHeader)
{
returnCode[0] = -1;
return;
}
var version = dataHeader & 0x0f;
if (version > 0)
{
returnCode[0] = -1;
return;
}
var lastVertex = new NativeArray<byte>(256, Allocator.Temp);
UnsafeUtility.MemCpy(lastVertex.GetUnsafePtr(), dataEnd - vertexSize, vertexSize);
var vertexBlockSize = GetVertexBlockSize(vertexSize);
uint vertexOffset = 0;
while (vertexOffset < vertexCount)
{
var blockSize = (vertexOffset + vertexBlockSize < vertexCount) ? vertexBlockSize : vertexCount - vertexOffset;
data = DecodeBlock(
data,
dataEnd,
vertexData + vertexOffset * vertexSize,
blockSize,
vertexSize,
(byte*)lastVertex.GetUnsafePtr()
);
if (data == null)
{
returnCode[0] = -2;
return;
}
vertexOffset += blockSize;
}
lastVertex.Dispose();
var tailSize = vertexSize < k_KTailMaxSize ? k_KTailMaxSize : vertexSize;
if ((uint)(dataEnd - data) != tailSize)
{
returnCode[0] = -3;
return;
}
switch (filter)
{
// Filters - only applied if filter isn't undefined or NONE
case Filter.Octahedral:
Assert.IsTrue(vertexSize == 4 || vertexSize == 8);
if (vertexSize == 4)
{
ApplyOctahedralFilterOct8(destination, vertexCount);
}
else
{
ApplyOctahedralFilterOct12(destination, vertexCount);
}
break;
case Filter.Quaternion:
Assert.AreEqual(vertexSize, 8);
ApplyQuaternionFilter(destination, vertexCount);
break;
case Filter.Exponential:
Assert.AreEqual(0x00, (vertexSize & 0x03));
ApplyExponentialFilter(destination, vertexCount, vertexSize);
break;
case Filter.None:
case Filter.Undefined:
break;
default:
returnCode[0] = -4;
return;
}
returnCode[0] = 0;
}
static byte* DecodeBytesGroup(byte* data, byte* buffer, int bitsLog2)
{
byte v;
byte enc;
byte encV;
byte* dataVar;
void Read()
{
v = *data++;
}
void Next(byte bits)
{
enc = (byte)(v >> (8 - bits));
v <<= bits;
encV = *dataVar;
*buffer++ = (enc == (1 << bits) - 1) ? encV : enc;
dataVar += (enc == (1 << bits) - 1) ? 1 : 0;
}
switch (bitsLog2)
{
case 0:
UnsafeUtility.MemSet(buffer, 0, Decode.kByteGroupSize);
return data;
case 1:
dataVar = data + 4;
// 4 groups with 4 2-bit values in each byte
Read(); Next(2); Next(2); Next(2); Next(2);
Read(); Next(2); Next(2); Next(2); Next(2);
Read(); Next(2); Next(2); Next(2); Next(2);
Read(); Next(2); Next(2); Next(2); Next(2);
return dataVar;
case 2:
dataVar = data + 8;
// 8 groups with 2 4-bit values in each byte
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
Read(); Next(4); Next(4);
return dataVar;
case 3:
UnsafeUtility.MemCpy(buffer, data, Decode.kByteGroupSize);
return data + Decode.kByteGroupSize;
default:
return null;
}
}
static byte* DecodeBytes(byte* data, byte* dataEnd, byte* buffer, uint bufferSize)
{
Assert.AreEqual(0, bufferSize % Decode.kByteGroupSize);
var header = data;
// round number of groups to 4 to get number of header bytes
var headerSize = (bufferSize / Decode.kByteGroupSize + 3) / 4;
if ((uint)(dataEnd - data) < headerSize)
return null;
data += headerSize;
for (uint i = 0; i < bufferSize; i += Decode.kByteGroupSize)
{
if ((uint)(dataEnd - data) < Decode.kByteGroupDecodeLimit)
return null;
var headerOffset = i / Decode.kByteGroupSize;
var bitsLog2 = (header[headerOffset / 4] >> (int)(((headerOffset % 4) * 2)) & 3);
data = DecodeBytesGroup(data, buffer + i, bitsLog2);
if (data == null)
{
return null;
}
}
return data;
}
static byte* DecodeBlock(byte* data, byte* dataEnd, byte* vertexData, uint vertexCount, uint vertexSize, byte* lastVertex)
{
Assert.IsTrue(vertexCount > 0 && vertexCount <= Decode.kVertexBlockMaxSize);
var buffer = new NativeArray<byte>((int)Decode.kVertexBlockMaxSize, Allocator.Temp);
var transposed = new NativeArray<byte>((int)Decode.kVertexBlockSizeBytes, Allocator.Temp);
var vertexCountAligned = (vertexCount + Decode.kByteGroupSize - 1) & ~(Decode.kByteGroupSize - 1);
for (uint k = 0; k < vertexSize; ++k)
{
data = DecodeBytes(data, dataEnd, (byte*)buffer.GetUnsafePtr(), vertexCountAligned);
if (data == null)
{
buffer.Dispose();
transposed.Dispose();
return null;
}
var vertexOffset = k;
var p = lastVertex[k];
for (uint i = 0; i < vertexCount; ++i)
{
var v = (byte)(Decode.UnZigZag8(buffer[(int)i]) + p);
transposed[(int)vertexOffset] = v;
p = v;
vertexOffset += vertexSize;
}
}
var transposedPtr = (byte*)transposed.GetUnsafeReadOnlyPtr();
UnsafeUtility.MemCpy(vertexData, transposedPtr, vertexCount * vertexSize);
UnsafeUtility.MemCpy(lastVertex, transposedPtr + vertexSize * (vertexCount - 1), vertexSize);
buffer.Dispose();
transposed.Dispose();
return data;
}
static uint GetVertexBlockSize(uint vertexSize)
{
// make sure the entire block fits into the scratch buffer
var result = Decode.kVertexBlockSizeBytes / vertexSize;
// align to byte group size; we encode each byte as a byte group
// if vertex block is misaligned, it results in wasted bytes, so just truncate the block size
result &= ~(Decode.kByteGroupSize - 1);
return (result < Decode.kVertexBlockMaxSize) ? result : Decode.kVertexBlockMaxSize;
}
internal static void ApplyExponentialFilter(NativeArray<byte> target, uint vertexCount, uint vertexSize)
{
var src = target.Reinterpret<uint>(sizeof(byte));
var dst = target.Reinterpret<float>(sizeof(byte));
for (var i = 0; i < (vertexSize * vertexCount) / 4; i++)
{
var v = src[i];
var exp = (int)v >> 24;
var mantissa = (int)(v << 8) >> 8;
dst[i] = math.pow(2.0f, exp) * mantissa;
}
}
internal static void ApplyQuaternionFilter(NativeArray<byte> target, uint vertexCount)
{
var dst = target.Reinterpret<short>(sizeof(byte));
for (var i = 0; i < vertexCount; ++i)
{
// recover scale from the high byte of the component
var sf = dst[i * 4 + 3] | 3;
var ss = k_SqrtHalf / sf;
// convert x/y/z to [-1..1] (scaled...)
var x = dst[i * 4 + 0] * ss;
var y = dst[i * 4 + 1] * ss;
var z = dst[i * 4 + 2] * ss;
// reconstruct w as a square root; we clamp to 0f to avoid NaN due to precision errors
var ww = 1f - x * x - y * y - z * z;
var w = math.sqrt(math.max(0, ww));
// rounded signed float->int
var xf = (int)(x * 32767f + (x >= 0f ? .5f : -.5f));
var yf = (int)(y * 32767f + (y >= 0f ? .5f : -.5f));
var zf = (int)(z * 32767f + (z >= 0f ? .5f : -.5f));
var wf = (int)(w * 32767f + 0.5f);
var qc = dst[i * 4 + 3] & 3;
// output order is dictated by input index
dst[i * 4 + ((qc + 1) & 3)] = (short)xf;
dst[i * 4 + ((qc + 2) & 3)] = (short)yf;
dst[i * 4 + ((qc + 3) & 3)] = (short)zf;
dst[i * 4 + ((qc + 0) & 3)] = (short)wf;
}
}
internal static void ApplyOctahedralFilterOct8(NativeArray<byte> target, uint vertexSize)
{
var dst = target.Reinterpret<sbyte>(sizeof(byte));
for (var i = 0; i < 4 * vertexSize; i += 4)
{
var x = (float)dst[i + 0];
var y = (float)dst[i + 1];
var one = (float)dst[i + 2];
x /= one;
y /= one;
var z = 1.0f - math.abs(x) - math.abs(y);
var t = math.max(-z, 0.0f);
x -= (x >= 0) ? t : -t;
y -= (y >= 0) ? t : -t;
var h = sbyte.MaxValue / math.sqrt(x * x + y * y + z * z);
dst[i + 0] = (sbyte)math.round(x * h);
dst[i + 1] = (sbyte)math.round(y * h);
dst[i + 2] = (sbyte)math.round(z * h);
// keep dst[i + 3] as is
}
}
internal static void ApplyOctahedralFilterOct12(NativeArray<byte> target, uint vertexSize)
{
var dst = target.Reinterpret<short>(sizeof(byte));
for (var i = 0; i < 4 * vertexSize; i += 4)
{
var x = (float)dst[i + 0];
var y = (float)dst[i + 1];
var one = (float)dst[i + 2];
x /= one;
y /= one;
var z = 1.0f - math.abs(x) - math.abs(y);
var t = math.max(-z, 0.0f);
x -= (x >= 0) ? t : -t;
y -= (y >= 0) ? t : -t;
var h = short.MaxValue / math.sqrt(x * x + y * y + z * z);
dst[i + 0] = (short)math.round(x * h);
dst[i + 1] = (short)math.round(y * h);
dst[i + 2] = (short)math.round(z * h);
// keep dst[i + 3] as is
}
}
}
}