Files
ppsspp/unittest/TestSplineTessellation.cpp
T

629 lines
24 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program 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 General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
// Correctness and speed of the software Bezier/spline tessellation in GPU/Common/SplineCommon.cpp,
// so that it can be optimized with a quick way to see that it still gets the right answer.
//
// The reference evaluates the surfaces the textbook way, in double precision: Bernstein polynomials
// for Bezier patches, and Cox-de Boor for the cubic B-splines, with the knots clamped at an "open"
// edge. Normals come from finite differences, taken a hair inside the patch so that they also come
// out as the limit at a pole. It shares nothing with the tessellator on purpose.
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <vector>
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "GPU/Common/SplineCommon.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/GPUState.h"
#include "GPU/ge_constants.h"
#include "unittest/UnitTest.h"
using namespace Spline;
namespace {
struct D3 {
double x = 0.0, y = 0.0, z = 0.0;
D3 operator+(const D3 &o) const { return { x + o.x, y + o.y, z + o.z }; }
D3 operator-(const D3 &o) const { return { x - o.x, y - o.y, z - o.z }; }
D3 operator*(double s) const { return { x * s, y * s, z * s }; }
double Length() const { return sqrt(x * x + y * y + z * z); }
};
D3 Cross(const D3 &a, const D3 &b) {
return { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x };
}
// Everything a control point or a tessellated vertex carries, as doubles.
struct RefVertex {
D3 pos;
double uv[2]{};
double color[4]{}; // 0-255
};
double Bernstein(int i, double t) {
const double s = 1.0 - t;
switch (i) {
case 0: return s * s * s;
case 1: return 3.0 * t * s * s;
case 2: return 3.0 * t * t * s;
default: return t * t * t;
}
}
// The knots of a cubic B-spline with count control points: uniform integers, so that the curve runs
// over [0, count - 3]. An open edge repeats its end knot four times, pulling the curve through the
// end control point. type bit 0 opens the first edge, bit 1 the last.
std::vector<double> SplineKnots(int count, int type) {
const int n = count - 3;
std::vector<double> knots(count + 4);
for (int j = 0; j < (int)knots.size(); j++) {
knots[j] = j - 3;
}
if (type & 1) {
for (int j = 0; j < 4; j++) {
knots[j] = 0.0;
}
}
if (type & 2) {
for (int j = n + 3; j < n + 7; j++) {
knots[j] = n;
}
}
return knots;
}
// Cox-de Boor, for one basis function.
double BSplineBasis(const std::vector<double> &knots, int i, int degree, double t) {
if (degree == 0) {
return knots[i] <= t && t < knots[i + 1] ? 1.0 : 0.0;
}
double result = 0.0;
const double d1 = knots[i + degree] - knots[i];
if (d1 != 0.0) {
result += (t - knots[i]) / d1 * BSplineBasis(knots, i, degree - 1, t);
}
const double d2 = knots[i + degree + 1] - knots[i + 1];
if (d2 != 0.0) {
result += (knots[i + degree + 1] - t) / d2 * BSplineBasis(knots, i + 1, degree - 1, t);
}
return result;
}
// The four control points that affect a parameter along one axis, and their weights. Only these
// are evaluated, and nothing is allocated, so the test stays quick in a debug build.
struct Weights4 {
int first;
double w[4];
};
// A surface to evaluate: either one Bezier patch (4x4 control points), or a whole spline.
struct RefSurface {
bool bezier = true;
int pointsU = 4, pointsV = 4;
std::vector<RefVertex> points; // pointsU * pointsV, u fastest.
int firstU = 0, firstV = 0; // Bezier: the patch's first control point.
std::vector<double> knotsU, knotsV; // Spline.
double endU = 1.0, endV = 1.0; // The parameter range is [0, end].
Weights4 Weights(bool alongU, double t) const {
Weights4 out;
if (bezier) {
out.first = alongU ? firstU : firstV;
for (int i = 0; i < 4; i++) {
out.w[i] = Bernstein(i, t);
}
return out;
}
// The basis is half-open, so step back from the very end of the range.
const double end = alongU ? endU : endV;
const double tt = std::min(t, end - 1e-9);
const std::vector<double> &knots = alongU ? knotsU : knotsV;
// The span tt is in: knots[span] <= tt < knots[span + 1]. Basis functions span - 3 to span
// are the only ones that aren't zero there.
int span = 3;
while (span + 1 < (int)knots.size() - 4 && knots[span + 1] <= tt) {
span++;
}
out.first = span - 3;
for (int i = 0; i < 4; i++) {
out.w[i] = BSplineBasis(knots, out.first + i, 3, tt);
}
return out;
}
RefVertex Evaluate(double u, double v, bool posOnly = false) const {
const Weights4 wu = Weights(true, u);
const Weights4 wv = Weights(false, v);
RefVertex out;
for (int j = 0; j < 4; j++) {
for (int i = 0; i < 4; i++) {
const double w = wu.w[i] * wv.w[j];
const RefVertex &p = points[(wv.first + j) * pointsU + wu.first + i];
out.pos = out.pos + p.pos * w;
if (posOnly) {
continue;
}
for (int c = 0; c < 2; c++) {
out.uv[c] += p.uv[c] * w;
}
for (int c = 0; c < 4; c++) {
out.color[c] += p.color[c] * w;
}
}
}
return out;
}
// The surface normal, cross(dP/du, dP/dv), from central differences at a point moved a little
// towards the middle - at a pole the derivatives vanish, and this gives the limit.
D3 Normal(double u, double v) const {
const double inset = 1e-4, h = 1e-6;
u += (u < endU * 0.5) ? inset : -inset;
v += (v < endV * 0.5) ? inset : -inset;
const D3 du = (Evaluate(u + h, v, true).pos - Evaluate(u - h, v, true).pos) * (0.5 / h);
const D3 dv = (Evaluate(u, v + h, true).pos - Evaluate(u, v - h, true).pos) * (0.5 / h);
const D3 n = Cross(du, dv);
return n * (1.0 / n.Length());
}
};
struct TestCase {
const char *name;
bool bezier;
int pointsU, pointsV;
int tessU, tessV;
int typeU = 0, typeV = 0; // Spline only.
u32 vertType = GE_VTYPE_POS_FLOAT | GE_VTYPE_NRM_FLOAT | GE_VTYPE_TC_FLOAT | GE_VTYPE_COL_8888;
bool patchFacing = false;
int poleEdge = -1; // Collapse this edge to one point: 0 = first row (v = 0), 1 = first column (u = 0).
float poleNoise = 0.0f; // ...but only nearly, the way animated control points come out.
bool geExact = false; // The GE's own fixed-point evaluation, as the software renderer uses.
};
// A bumpy, uneven grid of control points with varying UVs and colors, so that a mixed-up weight or
// attribute shows.
std::vector<SimpleVertex> MakeControlPoints(const TestCase &tc) {
std::vector<SimpleVertex> points(tc.pointsU * tc.pointsV);
for (int j = 0; j < tc.pointsV; j++) {
for (int i = 0; i < tc.pointsU; i++) {
SimpleVertex &p = points[j * tc.pointsU + i];
p.pos = Vec3Packedf(i * 0.5f + 0.07f * j, 0.4f * sinf(i * 0.7f + j * 0.3f) + 0.1f * i, j * 0.45f - 0.05f * i);
p.nrm = Vec3Packedf(0.0f, 1.0f, 0.0f);
p.uv[0] = i * 0.25f + 0.02f * j;
p.uv[1] = j * 0.3f;
p.color[0] = (u8)(40 + 13 * i);
p.color[1] = (u8)(200 - 11 * j);
p.color[2] = (u8)(90 + 7 * i + 5 * j);
p.color[3] = (u8)(255 - 9 * i);
}
}
if (tc.poleEdge == 0) {
for (int i = 1; i < tc.pointsU; i++) {
points[i].pos = Vec3Packedf(points[0].pos.x + i * tc.poleNoise, points[0].pos.y - i * tc.poleNoise, points[0].pos.z);
}
} else if (tc.poleEdge == 1) {
for (int j = 1; j < tc.pointsV; j++) {
points[j * tc.pointsU].pos = Vec3Packedf(points[0].pos.x, points[0].pos.y + j * tc.poleNoise, points[0].pos.z - j * tc.poleNoise);
}
}
return points;
}
RefSurface MakeReference(const TestCase &tc, const std::vector<SimpleVertex> &points) {
RefSurface ref;
ref.bezier = tc.bezier;
ref.pointsU = tc.pointsU;
ref.pointsV = tc.pointsV;
for (const SimpleVertex &p : points) {
RefVertex r;
r.pos = { p.pos.x, p.pos.y, p.pos.z };
r.uv[0] = p.uv[0];
r.uv[1] = p.uv[1];
for (int c = 0; c < 4; c++) {
r.color[c] = p.color[c];
}
ref.points.push_back(r);
}
if (!tc.bezier) {
ref.knotsU = SplineKnots(tc.pointsU, tc.typeU);
ref.knotsV = SplineKnots(tc.pointsV, tc.typeV);
ref.endU = tc.pointsU - 3;
ref.endV = tc.pointsV - 3;
}
return ref;
}
struct Output {
std::vector<SimpleVertex> vertices;
std::vector<u16> indices;
int indexCount = 0;
int tessU = 0, tessV = 0; // As the surface ended up, after Init.
int patchesU = 0, patchesV = 0;
UVScale uvScale{ 1.0f, 1.0f, 0.0f, 0.0f }; // In: passed to the tessellator.
bool fullAlpha = false;
};
template <class Surface>
void Tessellate(Surface &surface, u32 vertType, const std::vector<SimpleVertex> &points, int numVerts, int numIndices, Output &out) {
const int numPoints = surface.num_points_u * surface.num_points_v;
std::vector<const SimpleVertex *> pointers(numPoints);
for (int i = 0; i < numPoints; i++) {
pointers[i] = &points[i];
}
std::vector<ControlPoint> controlPoints(numPoints);
ControlPoints cpoints;
cpoints.points = controlPoints.data();
cpoints.Convert(pointers.data(), numPoints);
out.vertices.assign(numVerts, SimpleVertex{});
out.indices.assign(numIndices, 0);
OutputBuffers buffers;
buffers.vertices = out.vertices.data();
buffers.indices = out.indices.data();
buffers.count = 0;
buffers.uvScale = out.uvScale;
SoftwareTessellation(buffers, surface, vertType, cpoints);
out.indexCount = buffers.count;
out.fullAlpha = buffers.fullAlpha;
out.tessU = surface.tess_u;
out.tessV = surface.tess_v;
out.patchesU = surface.num_patches_u;
out.patchesV = surface.num_patches_v;
}
void RunTessellator(const TestCase &tc, const std::vector<SimpleVertex> &points, Output &out) {
const int maxVertices = 65536;
if (tc.bezier) {
BezierSurface surface{};
surface.num_points_u = tc.pointsU;
surface.num_points_v = tc.pointsV;
surface.tess_u = tc.tessU;
surface.tess_v = tc.tessV;
surface.num_patches_u = (tc.pointsU - 1) / 3;
surface.num_patches_v = (tc.pointsV - 1) / 3;
surface.primType = GE_PATCHPRIM_TRIANGLES;
surface.patchFacing = tc.patchFacing;
surface.geExact = tc.geExact;
surface.Init(maxVertices);
const int patches = surface.num_patches_u * surface.num_patches_v;
Tessellate(surface, tc.vertType, points, (surface.tess_u + 1) * (surface.tess_v + 1) * patches, surface.tess_u * surface.tess_v * 6 * patches, out);
} else {
SplineSurface surface{};
surface.num_points_u = tc.pointsU;
surface.num_points_v = tc.pointsV;
surface.tess_u = tc.tessU;
surface.tess_v = tc.tessV;
surface.type_u = tc.typeU;
surface.type_v = tc.typeV;
surface.num_patches_u = tc.pointsU - 3;
surface.num_patches_v = tc.pointsV - 3;
surface.primType = GE_PATCHPRIM_TRIANGLES;
surface.patchFacing = tc.patchFacing;
surface.geExact = tc.geExact;
surface.Init(maxVertices);
const int divU = surface.num_patches_u * surface.tess_u;
const int divV = surface.num_patches_v * surface.tess_v;
Tessellate(surface, tc.vertType, points, (divU + 1) * (divV + 1), divU * divV * 6, out);
}
}
bool CheckVertex(const TestCase &tc, const RefSurface &ref, const SimpleVertex &got, const SimpleVertex &defaultPoint,
double u, double v, double genU, double genV, int index) {
const RefVertex want = ref.Evaluate(u, v);
const D3 gotPos = { got.pos.x, got.pos.y, got.pos.z };
const double posError = (gotPos - want.pos).Length();
if (posError > 1e-4) {
printf("%s: vertex %d at (%.3f, %.3f): pos %f %f %f, want %f %f %f\n", tc.name, index, u, v,
gotPos.x, gotPos.y, gotPos.z, want.pos.x, want.pos.y, want.pos.z);
return false;
}
const bool hasTex = (tc.vertType & GE_VTYPE_TC_MASK) != 0;
const double wantU = hasTex ? want.uv[0] : genU;
const double wantV = hasTex ? want.uv[1] : genV;
if (fabs(got.uv[0] - wantU) > 1e-4 || fabs(got.uv[1] - wantV) > 1e-4) {
printf("%s: vertex %d at (%.3f, %.3f): uv %f %f, want %f %f\n", tc.name, index, u, v, got.uv[0], got.uv[1], wantU, wantV);
return false;
}
const bool hasColor = (tc.vertType & GE_VTYPE_COL_MASK) != 0;
for (int c = 0; c < 4; c++) {
const double wantC = hasColor ? want.color[c] : defaultPoint.color[c];
if (fabs(got.color[c] - wantC) > 1.001) {
printf("%s: vertex %d at (%.3f, %.3f): color channel %d is %d, want %.2f\n", tc.name, index, u, v, c, got.color[c], wantC);
return false;
}
}
const bool hasNormal = (tc.vertType & GE_VTYPE_NRM_MASK) != 0;
D3 wantN = { 0.0, 0.0, 1.0 };
if (hasNormal) {
wantN = ref.Normal(u, v);
if (tc.patchFacing) {
wantN = wantN * -1.0;
}
}
const D3 gotN = { got.nrm.x, got.nrm.y, got.nrm.z };
// The length only to rsqrt precision: x86 normalizes with a bare _mm_rsqrt_ps (relative error up to 3.7e-4).
const double gotLen = gotN.Length();
const double dot = (gotN.x * wantN.x + gotN.y * wantN.y + gotN.z * wantN.z) / gotLen;
if (!(fabs(gotLen - 1.0) < 5e-4 && dot > 0.9999)) {
printf("%s: vertex %d at (%.3f, %.3f): normal %f %f %f, want %f %f %f\n", tc.name, index, u, v,
gotN.x, gotN.y, gotN.z, wantN.x, wantN.y, wantN.z);
return false;
}
return true;
}
bool CheckIndices(const TestCase &tc, const Output &out, int expectedCount) {
if (out.indexCount != expectedCount) {
printf("%s: %d indices, want %d\n", tc.name, out.indexCount, expectedCount);
return false;
}
for (int i = 0; i < out.indexCount; i++) {
if (out.indices[i] >= out.vertices.size()) {
printf("%s: index %d is %d, past the %d vertices\n", tc.name, i, out.indices[i], (int)out.vertices.size());
return false;
}
}
return true;
}
bool CheckCase(const TestCase &tc) {
const std::vector<SimpleVertex> points = MakeControlPoints(tc);
Output out;
RunTessellator(tc, points, out);
if (tc.bezier) {
const int verticesPerPatch = (out.tessU + 1) * (out.tessV + 1);
for (int pv = 0; pv < out.patchesV; pv++) {
for (int pu = 0; pu < out.patchesU; pu++) {
RefSurface ref = MakeReference(tc, points);
ref.firstU = pu * 3;
ref.firstV = pv * 3;
const int base = (pv * out.patchesU + pu) * verticesPerPatch;
for (int tv = 0; tv <= out.tessV; tv++) {
for (int tu = 0; tu <= out.tessU; tu++) {
const int index = base + tv * (out.tessU + 1) + tu;
const double u = (double)tu / out.tessU, v = (double)tv / out.tessV;
if (!CheckVertex(tc, ref, out.vertices[index], points[0], u, v, pu + u, pv + v, index)) {
return false;
}
}
}
}
}
return CheckIndices(tc, out, out.tessU * out.tessV * 6 * out.patchesU * out.patchesV);
}
const RefSurface ref = MakeReference(tc, points);
const int divU = out.patchesU * out.tessU, divV = out.patchesV * out.tessV;
for (int iv = 0; iv <= divV; iv++) {
for (int iu = 0; iu <= divU; iu++) {
const int index = iv * (divU + 1) + iu;
const double u = (double)iu / out.tessU, v = (double)iv / out.tessV;
if (!CheckVertex(tc, ref, out.vertices[index], points[0], u, v, u, v, index)) {
return false;
}
}
}
return CheckIndices(tc, out, divU * divV * 6);
}
// The tessellator writes vertices in the form the vertex decoder would decode them to, so that SubmitCurve can
// skip that decode. Check it against the decoders themselves.
bool CheckDecodedForm(const TestCase &tc, bool opaque) {
std::vector<SimpleVertex> points = MakeControlPoints(tc);
if (opaque) {
for (SimpleVertex &p : points) {
p.color[3] = 255;
}
}
Output plain;
RunTessellator(tc, points, plain);
Output decodedForm;
decodedForm.uvScale = { 1.5f, -0.75f, 0.25f, -2.0f };
RunTessellator(tc, points, decodedForm);
const u32 vtype = GE_VTYPE_TC_FLOAT | GE_VTYPE_COL_8888 | GE_VTYPE_NRM_FLOAT | GE_VTYPE_POS_FLOAT;
const int count = (int)plain.vertices.size();
VertexDecoderJitCache cache;
for (bool jit : { false, true }) {
VertexDecoder dec;
dec.SetVertexType(GetVertTypeID(vtype, GE_TEXMAP_TEXTURE_COORDS), VertexDecoderOptions{}, jit ? &cache : nullptr);
const DecVtxFormat &fmt = dec.GetDecVtxFmt();
if (fmt.stride != sizeof(SimpleVertex) || fmt.uvoff != offsetof(SimpleVertex, uv) || fmt.c0off != offsetof(SimpleVertex, color) ||
fmt.nrmoff != offsetof(SimpleVertex, nrm) || fmt.posoff != offsetof(SimpleVertex, pos)) {
printf("%s: the decoded format isn't laid out like SimpleVertex\n", tc.name);
return false;
}
// The decoder may write a vertex and 16 bytes past the end.
std::vector<SimpleVertex> decoded(count + 2);
gstate_c.vertexFullAlpha = true;
dec.DecodeVerts((u8 *)decoded.data(), (const u8 *)plain.vertices.data(), &decodedForm.uvScale, count);
if (gstate_c.vertexFullAlpha != decodedForm.fullAlpha) {
printf("%s%s: full alpha %d, the %s decoder says %d\n", tc.name, opaque ? ", opaque" : "", decodedForm.fullAlpha, jit ? "jit" : "interpreted", gstate_c.vertexFullAlpha);
return false;
}
for (int i = 0; i < count; i++) {
const SimpleVertex &want = decoded[i];
const SimpleVertex &got = decodedForm.vertices[i];
// The UV scale may or may not be fused, depending on the decoder and the platform.
const bool uvMatch = fabsf(got.uv[0] - want.uv[0]) <= 1e-6f * (1.0f + fabsf(want.uv[0])) && fabsf(got.uv[1] - want.uv[1]) <= 1e-6f * (1.0f + fabsf(want.uv[1]));
if (!uvMatch || got.color_32 != want.color_32 || memcmp(&got.nrm, &want.nrm, sizeof(got.nrm)) != 0 || memcmp(&got.pos, &want.pos, sizeof(got.pos)) != 0) {
printf("%s: vertex %d differs from the %s decoder: uv %f %f color %08x, want uv %f %f color %08x\n", tc.name, i, jit ? "jit" : "interpreted",
got.uv[0], got.uv[1], (u32)got.color_32, want.uv[0], want.uv[1], (u32)want.color_32);
return false;
}
}
}
return true;
}
// The vectorized GE-exact Bezier path against the scalar one, bit for bit. Random control points mix signs
// and exponents (the lerps work at the larger operand's), and include zeros and values small enough for the
// scalar fallback.
bool CheckGEExactSIMD(const TestCase &tc, uint32_t seed) {
std::vector<SimpleVertex> points = MakeControlPoints(tc);
if (seed != 0) {
uint32_t state = seed;
auto next = [&]() {
state = state * 1664525u + 1013904223u;
return state;
};
auto randomFloat = [&]() {
const uint32_t r = next();
if ((r & 15) == 0)
return 0.0f;
const int exponent = (r & 16) ? (int)(next() % 40) - 20 : (int)(next() % 240) - 125;
const float mantissa = 1.0f + (next() & 0xFFFFFF) / 16777216.0f;
return ldexpf((r & 32) ? -mantissa : mantissa, exponent);
};
for (SimpleVertex &p : points) {
p.pos = Vec3Packedf(randomFloat(), randomFloat(), randomFloat());
p.uv[0] = randomFloat();
p.uv[1] = randomFloat();
p.color_32 = next();
}
}
Output simd, scalar;
g_splineGEScalar = false;
RunTessellator(tc, points, simd);
g_splineGEScalar = true;
RunTessellator(tc, points, scalar);
g_splineGEScalar = false;
for (size_t i = 0; i < simd.vertices.size(); i++) {
if (memcmp(&simd.vertices[i], &scalar.vertices[i], sizeof(SimpleVertex)) != 0) {
const SimpleVertex &a = simd.vertices[i], &b = scalar.vertices[i];
printf("%s, seed %u: vertex %d differs: pos %a %a %a nrm %a %a %a uv %a %a color %08x, scalar pos %a %a %a nrm %a %a %a uv %a %a color %08x\n",
tc.name, seed, (int)i, a.pos.x, a.pos.y, a.pos.z, a.nrm.x, a.nrm.y, a.nrm.z, a.uv[0], a.uv[1], (u32)a.color_32,
b.pos.x, b.pos.y, b.pos.z, b.nrm.x, b.nrm.y, b.nrm.z, b.uv[0], b.uv[1], (u32)b.color_32);
return false;
}
}
return true;
}
} // namespace
bool TestSplineTessellation() {
const u32 posOnly = GE_VTYPE_POS_FLOAT;
const u32 posNrm = GE_VTYPE_POS_FLOAT | GE_VTYPE_NRM_FLOAT;
std::vector<TestCase> cases = {
{ "bezier, one patch", true, 4, 4, 8, 8 },
{ "bezier, 2x3 patches, uneven tessellation", true, 7, 10, 5, 3 },
{ "bezier, no attributes", true, 7, 4, 4, 6, 0, 0, posOnly },
{ "bezier, normals only", true, 4, 7, 6, 2, 0, 0, posNrm },
{ "bezier, patch facing", true, 4, 4, 5, 5, 0, 0, posNrm, true },
{ "bezier, pole at v = 0", true, 4, 4, 8, 8, 0, 0, posNrm, false, 0 },
{ "bezier, pole at u = 0", true, 4, 4, 8, 8, 0, 0, posNrm, false, 1 },
{ "spline, pole at v = 0, open", false, 5, 6, 4, 4, 3, 3, posNrm, false, 0 },
{ "bezier, pole at u = 0 with rounding noise", true, 4, 4, 8, 8, 0, 0, posNrm, false, 1, 1e-7f },
{ "spline, pole at v = 0 with rounding noise", false, 5, 6, 4, 4, 3, 3, posNrm, false, 0, 1e-7f },
};
// Each edge type on each axis, one axis with three patches and the other with two.
cases.push_back({ "spline, closed/open", false, 6, 5, 4, 6, 0, 3 });
cases.push_back({ "spline, open first/open last", false, 6, 5, 4, 6, 1, 2 });
cases.push_back({ "spline, open last/open first", false, 6, 5, 4, 6, 2, 1 });
cases.push_back({ "spline, open/closed", false, 6, 5, 4, 6, 3, 0 });
// A single patch, where both ends' knots meet in the same span.
cases.push_back({ "spline, one patch, closed", false, 4, 4, 3, 3, 0, 0 });
cases.push_back({ "spline, one patch, open first", false, 4, 4, 3, 3, 1, 1 });
cases.push_back({ "spline, one patch, open last", false, 4, 4, 3, 3, 2, 2 });
cases.push_back({ "spline, one patch, open", false, 4, 4, 3, 3, 3, 3 });
cases.push_back({ "spline, no attributes", false, 7, 5, 2, 5, 1, 2, posOnly });
cases.push_back({ "spline, patch facing", false, 5, 5, 4, 4, 0, 3, posNrm, true });
bool ok = true;
for (const TestCase &tc : cases) {
if (!CheckCase(tc)) {
ok = false;
}
}
// The vectorized GE-exact paths match the scalar ones. Spline tessellations of 3 and 5 put a vector's
// u steps in two segments.
const TestCase exactCases[] = {
{ "GE exact, bezier, one patch", true, 4, 4, 8, 8 },
{ "GE exact, bezier, 2x3 patches, uneven tessellation", true, 7, 10, 5, 3 },
{ "GE exact, bezier, normals only", true, 4, 7, 6, 2, 0, 0, posNrm },
{ "GE exact, bezier, patch facing", true, 4, 4, 5, 5, 0, 0, posNrm, true },
{ "GE exact, bezier, tessellation 1", true, 7, 4, 1, 1 },
{ "GE exact, bezier, tessellation 13", true, 4, 4, 13, 7 },
{ "GE exact, spline, open", false, 7, 6, 4, 4, 3, 3 },
{ "GE exact, spline, closed, uneven tessellation", false, 6, 7, 3, 5, 0, 0 },
{ "GE exact, spline, open first/open last", false, 8, 5, 5, 2, 1, 2 },
{ "GE exact, spline, one patch", false, 4, 4, 6, 6, 3, 3 },
{ "GE exact, spline, normals only, patch facing", false, 5, 6, 3, 3, 2, 1, posNrm, true },
{ "GE exact, spline, no attributes", false, 7, 5, 2, 5, 1, 2, posOnly },
{ "GE exact, spline, tessellation 1", false, 9, 4, 1, 1, 3, 0 },
};
for (TestCase tc : exactCases) {
tc.geExact = true;
for (uint32_t seed = 0; seed < 40; seed++) {
if (!CheckGEExactSIMD(tc, seed)) {
ok = false;
break;
}
}
}
// Uneven tessellation, so that rows end partway through a vector.
const TestCase decodedCases[] = {
{ "decoded form, bezier", true, 7, 4, 5, 3 },
{ "decoded form, spline", false, 6, 5, 3, 5, 1, 2 },
{ "decoded form, generated UVs, no color", false, 5, 5, 6, 2, 0, 3, posNrm },
};
for (const TestCase &tc : decodedCases) {
for (bool opaque : { false, true }) {
if (!CheckDecodedForm(tc, opaque)) {
ok = false;
}
}
}
// Speed, for whoever is optimizing this. A big spline and a batch of Bezier patches, the two
// shapes games send, with every attribute sampled.
{
TestCase spline = { "speed, spline", false, 10, 10, 8, 8, 3, 3 };
TestCase bezier = { "speed, bezier", true, 10, 10, 8, 8 };
TestCase splineExact = spline;
splineExact.name = "speed, spline, GE exact";
splineExact.geExact = true;
TestCase bezierExact = bezier;
bezierExact.name = "speed, bezier, GE exact";
bezierExact.geExact = true;
for (const TestCase *tc : { &spline, &bezier, &splineExact, &bezierExact }) {
const std::vector<SimpleVertex> points = MakeControlPoints(*tc);
Output out;
const double callsPerSecond = CallsPerSecond([&] { RunTessellator(*tc, points, out); }, 0.1, 1);
printf("%s: %dx%d control points, %d vertices: %.2f Mverts/s\n", tc->name, tc->pointsU, tc->pointsV,
(int)out.vertices.size(), callsPerSecond * out.vertices.size() / 1000000.0);
}
}
return ok;
}