Files
ppsspp/unittest/TestVertexJit.cpp
Henrik RydgårdandClaude Opus 5.5 e8c39ed1a7 Unit tests: Share the benchmark timing loop
Four tests had their own copy of "call this until N seconds have passed,
then divide". CallsPerSecond in UnitTest.h does it; each keeps its old
duration and batch size.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-30 11:11:22 -06:00

1120 lines
33 KiB
C++

// Copyright (c) 2015- 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/.
#include <math.h>
#include <cmath>
#include <cstring>
#include <map>
#include <string>
#include "Common/CommonTypes.h"
#include "Common/MemoryUtil.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
#include "Core/HDRemaster.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/ge_constants.h"
#include "GPU/GPUState.h"
#include "unittest/TestVertexJit.h"
#include "unittest/UnitTest.h"
const UVScale g_uvScale{1.0f, 1.0f, 0.0f, 0.0f};
class VertexDecoderTestHarness {
static const int BUFFER_SIZE = 64 * 65536;
static const int ROUNDS = 200;
public:
VertexDecoderTestHarness()
: dec_(nullptr), needsReset_(true), dstPos_(0), assertFailed_(false) {
src_ = new u8[BUFFER_SIZE];
dst_ = new u8[BUFFER_SIZE];
cache_ = new VertexDecoderJitCache();
}
~VertexDecoderTestHarness() {
delete [] src_;
delete [] dst_;
delete cache_;
delete dec_;
}
void Reset() {
memset(src_, 0, BUFFER_SIZE);
memset(dst_, 0, BUFFER_SIZE);
options_ = {};
delete dec_;
dec_ = nullptr;
srcPos_ = 0;
dstPos_ = 0;
needsReset_ = false;
}
void SetOptions(const VertexDecoderOptions &opts) {
if (needsReset_) {
Reset();
}
options_ = opts;
}
void Execute(int vtype, int count, bool useJit) {
SetupExecute(vtype, useJit);
dec_->DecodeVerts(dst_, src_, &g_uvScale, count);
}
double ExecuteTimed(int vtype, int count, bool useJit) {
SetupExecute(vtype, useJit);
return CallsPerSecond([&] { dec_->DecodeVerts(dst_, src_, &g_uvScale, count); }, 0.5, ROUNDS);
}
void Add8(u8 x) {
if (needsReset_) {
Reset();
}
memcpy(src_ + srcPos_, &x, sizeof(x));
srcPos_ += sizeof(x);
}
void Add8(u8 x, u8 y) {
Add8(x);
Add8(y);
}
void Add8(u8 x, u8 y, u8 z) {
Add8(x);
Add8(y);
Add8(z);
}
void Add8(u8 x, u8 y, u8 z, u8 w) {
Add8(x);
Add8(y);
Add8(z);
Add8(w);
}
void Add16(u16_le x) {
if (needsReset_) {
Reset();
}
memcpy(src_ + srcPos_, &x, sizeof(x));
srcPos_ += sizeof(x);
}
void Add16(u16_le x, u16_le y) {
Add16(x);
Add16(y);
}
void Add16(u16_le x, u16_le y, u16_le z) {
Add16(x);
Add16(y);
Add16(z);
}
void AddFloat(float_le x) {
if (needsReset_) {
Reset();
}
memcpy(src_ + srcPos_, &x, sizeof(x));
srcPos_ += sizeof(x);
}
void AddFloat(float_le x, float_le y) {
AddFloat(x);
AddFloat(y);
}
void AddFloat(float_le x, float_le y, float_le z) {
AddFloat(x);
AddFloat(y);
AddFloat(z);
}
u8 Get8() {
return dst_[dstPos_++];
}
u16 Get16() {
u16_le result;
memcpy(&result, dst_ + dstPos_, sizeof(result));
dstPos_ += sizeof(result);
return result;
}
float GetFloat() {
float_le result;
memcpy(&result, dst_ + dstPos_, sizeof(result));
dstPos_ += sizeof(result);
return result;
}
void Assert8(const char *title, u8 x, u8 y) {
u8 resx = Get8();
u8 resy = Get8();
if (resx != x || resy != y) {
assertFailed_ = true;
printf("%s: Failed %d, %d != expected %d, %d\n", title, resx, resy, x, y);
}
}
void Assert8(const char *title, u8 x, u8 y, u8 z) {
u8 resx = Get8();
u8 resy = Get8();
u8 resz = Get8();
if (resx != x || resy != y || resz != z) {
assertFailed_ = true;
printf("%s: Failed %d, %d, %d != expected %d, %d, %d\n", title, resx, resy, resz, x, y, z);
}
}
void Assert8(const char *title, u8 x, u8 y, u8 z, u8 w) {
u8 resx = Get8();
u8 resy = Get8();
u8 resz = Get8();
u8 resw = Get8();
if (resx != x || resy != y || resz != z || resw != w) {
assertFailed_ = true;
printf("%s: Failed %d, %d, %d, %d != expected %d, %d, %d, %d\n", title, resx, resy, resz, resw, x, y, z, w);
}
}
void Assert16(const char *title, u16 x, u16 y) {
u16 resx = Get16();
u16 resy = Get16();
if (resx != x || resy != y) {
assertFailed_ = true;
printf("%s: Failed %d, %d != expected %d, %d\n", title, resx, resy, x, y);
}
}
void Assert16(const char *title, u16 x, u16 y, u16 z) {
u16 resx = Get16();
u16 resy = Get16();
u16 resz = Get16();
if (resx != x || resy != y || resz != z) {
assertFailed_ = true;
printf("%s: Failed %d, %d, %d != expected %d, %d, %d\n", title, resx, resy, resz, x, y, z);
}
}
bool CompareFloat(float a, float b) {
return a - fmodf(a, 0.0000001f) == b - fmodf(b, 0.0000001f);
}
void AssertFloat(const char *title, float x) {
float resx = GetFloat();
if (!CompareFloat(resx, x)) {
assertFailed_ = true;
printf("%s: Failed %f != expected %f\n", title, resx, x);
}
}
void AssertFloat(const char *title, float x, float y) {
float resx = GetFloat();
float resy = GetFloat();
if (!CompareFloat(resx, x) || !CompareFloat(resy, y)) {
assertFailed_ = true;
printf("%s: Failed %f, %f != expected %f, %f\n", title, resx, resy, x, y);
}
}
void AssertFloat(const char *title, float x, float y, float z) {
float resx = GetFloat();
float resy = GetFloat();
float resz = GetFloat();
if (!CompareFloat(resx, x) || !CompareFloat(resy, y) || !CompareFloat(resz, z)) {
assertFailed_ = true;
printf("%s: Failed %f, %f, %f != expected %f, %f, %f\n", title, resx, resy, resz, x, y, z);
}
}
void Skip(u32 c) {
dstPos_ += c;
}
void *GetData() {
return dst_;
}
int GetDstStride() {
if (dec_) {
return dec_->decFmt.stride;
}
return 0;
}
bool HasFailed() {
return assertFailed_;
}
private:
void SetupExecute(int vtype, bool useJit) {
if (dec_ != nullptr) {
delete dec_;
}
dec_ = new VertexDecoder();
dec_->SetVertexType(vtype, options_, useJit ? cache_ : nullptr);
dstPos_ = 0;
needsReset_ = true;
}
u8 *src_;
u8 *dst_;
VertexDecoderJitCache *cache_;
VertexDecoderOptions options_;
VertexDecoder *dec_;
int indexLowerBound_;
int indexUpperBound_;
bool needsReset_;
size_t srcPos_;
size_t dstPos_;
bool assertFailed_;
};
static bool TestVertex8() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_8BIT | GE_VTYPE_NRM_8BIT | GE_VTYPE_TC_8BIT;
dec.Add8(127, 128);
dec.Add8(127, 0, 128);
dec.Add8(127, 0, 128);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vtype, 1, jit == 1);
dec.AssertFloat("TestVertex8-TC", 127.0f / 128.0f, 1.0f);
dec.Assert8("TestVertex8-Nrm", 127, 0, 128);
dec.Skip(1);
dec.AssertFloat("TestVertex8-Pos", 127.0f / 128.0f, 0.0f, -1.0f);
}
return !dec.HasFailed();
}
static bool TestVertex16() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_16BIT | GE_VTYPE_NRM_16BIT | GE_VTYPE_TC_16BIT;
dec.Add16(32767, 32768);
dec.Add16(32767, 0, 32768);
dec.Add16(32767, 0, 32768);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vtype, 1, jit == 1);
dec.AssertFloat("TestVertex16-TC", 32767.0f / 32768.0f, 1.0f);
dec.Assert16("TestVertex16-Nrm", 32767, 0, 32768);
dec.Skip(2);
dec.AssertFloat("TestVertex16-Pos", 32767.0f / 32768.0f, 0.0f, -1.0f);
}
return !dec.HasFailed();
}
static bool TestVertexFloat() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_NRM_FLOAT | GE_VTYPE_TC_FLOAT;
dec.AddFloat(1.0f, -1.0f);
dec.AddFloat(1.0f, 0.5f, -1.0f);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vtype, 1, jit == 1);
dec.AssertFloat("TestVertexFloat-TC", 1.0f, -1.0f);
dec.AssertFloat("TestVertexFloat-Nrm", 1.0f, 0.5f, -1.0f);
dec.AssertFloat("TestVertexFloat-Pos", 1.0f, 0.5f, -1.0f);
}
return !dec.HasFailed();
}
static bool TestVertex8Through() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_8BIT | GE_VTYPE_NRM_8BIT | GE_VTYPE_TC_8BIT | GE_VTYPE_THROUGH;
dec.Add8(127, 128);
dec.Add8(127, 0, 128);
dec.Add8(127, 0, 128);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vtype, 1, jit == 1);
// Note: this is correct, even in through.
dec.AssertFloat("TestVertex8Through-TC", 127.0f / 128.0f, 1.0f);
dec.Assert8("TestVertex8Through-Nrm", 127, 0, 128);
// Ignoring Pos since s8 through isn't really an option.
}
return !dec.HasFailed();
}
static bool TestVertex16Through() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_16BIT | GE_VTYPE_NRM_16BIT | GE_VTYPE_TC_16BIT | GE_VTYPE_THROUGH;
dec.Add16(32767, 32768);
dec.Add16(32767, 0, 32768);
dec.Add16(32767, 0, 32768);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vtype, 1, jit == 1);
dec.AssertFloat("TestVertex16Through-TC", 32767.0f, 32768.0f);
dec.Assert16("TestVertex16Through-Nrm", 32767, 0, 32768);
dec.Skip(2);
dec.AssertFloat("TestVertex16Through-Pos", 32767.0f, 0.0f, 32768.0f);
}
return !dec.HasFailed();
}
static bool TestVertexFloatThrough() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_NRM_FLOAT | GE_VTYPE_TC_FLOAT | GE_VTYPE_THROUGH;
dec.AddFloat(1.0f, -1.0f);
dec.AddFloat(1.0f, 0.5f, -1.0f);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vtype, 1, jit == 1);
dec.AssertFloat("TestVertexFloatThrough-TC", 1.0f, -1.0f);
dec.AssertFloat("TestVertexFloatThrough-Nrm", 1.0f, 0.5f, -1.0f);
dec.AssertFloat("TestVertexFloatThrough-Pos", 1.0f, 0.5f, 0.0f);
}
return !dec.HasFailed();
}
static bool TestVertexColor8888() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_COL_8888;
bool failed = false;
dec.Add8(1, 2, 3, 4);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor8888-Col", 1, 2, 3, 4);
dec.AssertFloat("TestVertexColor8888-Pos", 1.0f, 0.5f, -1.0f);
if (gstate_c.vertexFullAlpha) {
printf("TestVertexColor8888: failed to clear vertexFullAlpha\n");
failed = true;
}
}
dec.Add8(255, 255, 255, 255);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor8888-Col", 255, 255, 255, 255);
dec.AssertFloat("TestVertexColor8888-Pos", 1.0f, 0.5f, -1.0f);
if (!gstate_c.vertexFullAlpha) {
printf("TestVertexColor8888: cleared vertexFullAlpha\n");
failed = true;
}
}
return !dec.HasFailed() && !failed;
}
static bool TestVertexColor4444() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_COL_4444;
bool failed = false;
dec.Add16(0x1234, 0);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor4444-Col", 0x44, 0x33, 0x22, 0x11);
dec.AssertFloat("TestVertexColor4444-Pos", 1.0f, 0.5f, -1.0f);
if (gstate_c.vertexFullAlpha) {
printf("TestVertexColor4444: failed to clear vertexFullAlpha\n");
failed = true;
}
}
dec.Add16(0xFFFF, 0);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor4444-Col", 255, 255, 255, 255);
dec.AssertFloat("TestVertexColor4444-Pos", 1.0f, 0.5f, -1.0f);
if (!gstate_c.vertexFullAlpha) {
printf("TestVertexColor4444: cleared vertexFullAlpha\n");
failed = true;
}
}
return !dec.HasFailed() && !failed;
}
static bool TestVertexColor5551() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_COL_5551;
bool failed = false;
dec.Add16((0 << 15) | (1 << 10) | (2 << 5) | 3, 0);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor5551-Col", 0x18, 0x10, 0x8, 0x0);
dec.AssertFloat("TestVertexColor5551-Pos", 1.0f, 0.5f, -1.0f);
if (gstate_c.vertexFullAlpha) {
printf("TestVertexColor5551: failed to clear vertexFullAlpha\n");
failed = true;
}
}
dec.Add16(0xFFFF, 0);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor5551-Col", 255, 255, 255, 255);
dec.AssertFloat("TestVertexColor5551-Pos", 1.0f, 0.5f, -1.0f);
if (!gstate_c.vertexFullAlpha) {
printf("TestVertexColor5551: cleared vertexFullAlpha\n");
failed = true;
}
}
return !dec.HasFailed() && !failed;
}
static bool TestVertexColor565() {
VertexDecoderTestHarness dec;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_COL_565;
bool failed = false;
dec.Add16((1 << 11) | (2 << 5) | 3, 0);
dec.AddFloat(1.0f, 0.5f, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
gstate_c.vertexFullAlpha = true;
dec.Execute(vtype, 1, jit == 1);
dec.Assert8("TestVertexColor565-Col", 0x18, 0x8, 0x8, 255);
dec.AssertFloat("TestVertexColor565-Pos", 1.0f, 0.5f, -1.0f);
if (!gstate_c.vertexFullAlpha) {
printf("TestVertexColor565: cleared vertexFullAlpha\n");
failed = true;
}
}
return !dec.HasFailed() && !failed;
}
static bool TestVertex8Skin() {
VertexDecoderTestHarness dec;
VertexDecoderOptions opts{};
dec.SetOptions(opts);
for (int i = 0; i < 8 * 12; ++i) {
gstate.boneMatrix[i] = 0.0f;
}
gstate.boneMatrix[0] = 2.0f;
gstate.boneMatrix[4] = 1.0f;
gstate.boneMatrix[8] = 5.0f;
gstate.boneMatrix[12] = 1.0f;
gstate.boneMatrix[16] = 2.0f;
gstate.boneMatrix[20] = 5.0f;
int vtype = GE_VTYPE_POS_8BIT | GE_VTYPE_NRM_8BIT | GE_VTYPE_WEIGHT_8BIT | (1 << GE_VTYPE_WEIGHTCOUNT_SHIFT);
u32 vertTypeID = GetVertTypeID(vtype, 0);
dec.Add8(128 + 64, 128 - 64);
dec.Add8(127, 0, 128);
dec.Add8(127, 0, 128);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vertTypeID, 1, jit == 1);
dec.AssertFloat("TestVertex8Skin-Nrm", (2.0f * 1.5f + 1.0f * 0.5f) * 127.0f / 128.0f, 0.0f, 2.0f * 5.0f * -1.0f);
dec.AssertFloat("TestVertex8Skin-Pos", (2.0f * 1.5f + 1.0f * 0.5f) * 127.0f / 128.0f, 0.0f, 2.0f * 5.0f * -1.0f);
}
return !dec.HasFailed();
}
static bool TestVertex16Skin() {
VertexDecoderTestHarness dec;
VertexDecoderOptions opts{};
dec.SetOptions(opts);
for (int i = 0; i < 8 * 12; ++i) {
gstate.boneMatrix[i] = 0.0f;
}
gstate.boneMatrix[0] = 2.0f;
gstate.boneMatrix[4] = 1.0f;
gstate.boneMatrix[8] = 5.0f;
gstate.boneMatrix[12] = 1.0f;
gstate.boneMatrix[16] = 2.0f;
gstate.boneMatrix[20] = 5.0f;
int vtype = GE_VTYPE_POS_16BIT | GE_VTYPE_NRM_16BIT | GE_VTYPE_WEIGHT_16BIT | (1 << GE_VTYPE_WEIGHTCOUNT_SHIFT);
u32 vertTypeID = GetVertTypeID(vtype, 0);
dec.Add16(32768 + 16384, 32768 - 16384);
dec.Add16(32767, 0, 32768);
dec.Add16(32767, 0, 32768);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vertTypeID, 1, jit == 1);
dec.AssertFloat("TestVertex16Skin-Nrm", (2.0f * 1.5f + 1.0f * 0.5f) * 32767.0f / 32768.0f, 0.0f, 2.0f * 5.0f * -1.0f);
dec.AssertFloat("TestVertex16Skin-Pos", (2.0f * 1.5f + 1.0f * 0.5f) * 32767.0f / 32768.0f, 0.0f, 2.0f * 5.0f * -1.0f);
}
return !dec.HasFailed();
}
static bool TestVertexFloatSkin() {
VertexDecoderTestHarness dec;
VertexDecoderOptions opts{};
dec.SetOptions(opts);
for (int i = 0; i < 8 * 12; ++i) {
gstate.boneMatrix[i] = 0.0f;
}
gstate.boneMatrix[0] = 2.0f;
gstate.boneMatrix[4] = 1.0f;
gstate.boneMatrix[8] = 5.0f;
gstate.boneMatrix[12] = 1.0f;
gstate.boneMatrix[16] = 2.0f;
gstate.boneMatrix[20] = 5.0f;
int vtype = GE_VTYPE_POS_FLOAT | GE_VTYPE_NRM_FLOAT | GE_VTYPE_WEIGHT_FLOAT | (1 << GE_VTYPE_WEIGHTCOUNT_SHIFT);
u32 vertTypeID = GetVertTypeID(vtype, 0);
dec.AddFloat(1.5f, 0.5f);
dec.AddFloat(1.0f, 0, -1.0f);
dec.AddFloat(1.0f, 0, -1.0f);
for (int jit = 0; jit <= 1; ++jit) {
dec.Execute(vertTypeID, 1, jit == 1);
dec.AssertFloat("TestVertexFloatSkin-Nrm", (2.0f * 1.5f + 1.0f * 0.5f) * 1.0f, 0.0f, 2.0f * 5.0f * -1.0f);
dec.AssertFloat("TestVertexFloatSkin-Pos", (2.0f * 1.5f + 1.0f * 0.5f) * 1.0f, 0.0f, 2.0f * 5.0f * -1.0f);
}
return !dec.HasFailed();
}
// TODO: Morph (col, pos, nrm), weights (no skin), morph + weights?
// Everything below checks the JIT (and the handwritten SIMD decoders) against the step functions,
// across the whole space of vertex formats. The two must agree bit for bit - not just closely -
// since tests and frame dumps are recorded with one and games run with the other.
namespace {
struct JitMatchRng {
uint32_t state = 0x12345678;
uint32_t Next() {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state;
}
float Float(float lo, float hi) {
return lo + (hi - lo) * (float)(Next() & 0xFFFFFF) / (float)0xFFFFFF;
}
// The GE loads morph weights, bone matrices and the UV scale as 24-bit floats.
float Float24(float lo, float hi) {
float f = Float(lo, hi);
uint32_t bits;
memcpy(&bits, &f, 4);
bits &= 0xFFFFFF00;
memcpy(&f, &bits, 4);
return f;
}
// Vertex data floats: mostly arbitrary, with some exact values mixed in.
float VertexFloat() {
static const float nice[] = { 0.0f, 1.0f, -1.0f, 0.5f, -0.5f, 2.0f, 127.0f / 128.0f, 256.0f };
if ((Next() & 3) == 0) {
return nice[Next() % ARRAY_SIZE(nice)];
}
return Float(-8.0f, 8.0f);
}
};
// Output size of the formats the decoder can produce.
// Accumulating steps (morph in particular) are compiled into fused multiply-adds on some
// architectures and not others, and the prescale paths differ in where the scale lands, so the
// last bit of a decoded value is not something every JIT can be expected to reproduce exactly.
// Allow a couple of ULPs there - every real bug this test has caught was orders of magnitude
// larger, so nothing interesting slips through.
static bool NearlyEqualFloat(float a, float b, int terms) {
if (a == b) {
return true;
}
if (std::isnan(a) || std::isnan(b)) {
return false;
}
// Relative to the larger magnitude, with a floor of 1 - a prescaled texcoord is a small
// difference of larger terms, so the error is best judged against what went into it.
// Relative to the larger magnitude, with a floor of 1 - a prescaled texcoord is a small
// difference of larger terms, so the error is best judged against what went into it. Allow
// one rounding per accumulated term, since morph sums several.
const float scale = std::max(1.0f, std::max(fabsf(a), fabsf(b)));
return fabsf(a - b) <= scale * 1e-6f * (float)std::max(1, terms);
}
int DecodedComponentSize(u8 fmt) {
switch (fmt) {
case DEC_FLOAT_2: return 8;
case DEC_FLOAT_3: return 12;
case DEC_S8_3: return 3;
case DEC_S16_3: return 6;
case DEC_U8_4: return 4;
default: return -1;
}
}
// With specialPos, some plain float positions get a NaN or infinity, and specialPos[v] says which.
void FillVertexData(JitMatchRng &rng, const VertexDecoder &dec, u8 *src, int count, bool *specialPos) {
static const int wtSize[] = { 0, 1, 2, 4 };
static const int tcSize[] = { 0, 1, 2, 4 };
static const int nrmPosSize[] = { 0, 1, 2, 4 };
auto fillScalars = [&](u8 *p, int n, int elemSize) {
for (int i = 0; i < n; i++) {
if (elemSize == 4) {
float_le f = rng.VertexFloat();
memcpy(p + i * 4, &f, 4);
} else if (elemSize == 2) {
u16_le v = (u16)rng.Next();
memcpy(p + i * 2, &v, 2);
} else {
p[i] = (u8)rng.Next();
}
}
};
for (int v = 0; v < count; v++) {
for (int m = 0; m < dec.morphcount; m++) {
u8 *p = src + v * dec.size + m * dec.onesize_;
if (dec.weighttype) {
int sz = wtSize[dec.weighttype];
for (int w = 0; w < dec.nweights; w++) {
// Weights are normally 0..1ish; keep floats there so skinning stays sane.
if (sz == 4) {
float_le f = rng.Float(0.0f, 1.5f);
memcpy(p + dec.weightoff + w * 4, &f, 4);
} else {
fillScalars(p + dec.weightoff + w * sz, 1, sz);
}
}
}
if (dec.tc) {
fillScalars(p + dec.tcoff, 2, tcSize[dec.tc]);
}
if (dec.col) {
fillScalars(p + dec.coloff, dec.col == (GE_VTYPE_COL_8888 >> GE_VTYPE_COL_SHIFT) ? 4 : 2, 1);
}
if (dec.nrm) {
fillScalars(p + dec.nrmoff, 3, nrmPosSize[dec.nrm]);
}
if (dec.pos) {
fillScalars(p + dec.posoff, 3, nrmPosSize[dec.pos]);
}
}
if (specialPos) {
specialPos[v] = (rng.Next() & 7) == 0;
if (specialPos[v]) {
static const float specials[] = { NAN, INFINITY, -INFINITY };
float_le f = specials[rng.Next() % 3];
memcpy(src + v * dec.size + dec.posoff + (rng.Next() % 3) * 4, &f, 4);
}
}
}
}
// Skinning is the one place the JITs may round differently: arm64 accumulates the bone matrices
// with fused multiply-adds, the steps multiply and add separately. The difference comes from
// rounding the intermediate terms, which can be far larger than the result when they cancel, so
// the allowed error scales with the sum of the terms' magnitudes rather than with the result.
void SkinTermMagnitudes(const VertexDecoder &dec, const u8 *vtx, bool pos, float out[3]) {
auto readScalar = [](const u8 *p, int type, int i) -> float {
switch (type) {
case 1: return fabsf((float)(s8)p[i] * (1.0f / 128.0f));
case 2: { s16_le s; memcpy(&s, p + i * 2, 2); return fabsf((float)(s16)s * (1.0f / 32768.0f)); }
default: { float_le f; memcpy(&f, p + i * 4, 4); return fabsf((float)f); }
}
};
auto readWeight = [](const u8 *p, int type, int i) -> float {
switch (type) {
case 1: return p[i] * (1.0f / 128.0f);
case 2: { u16_le w; memcpy(&w, p + i * 2, 2); return (u16)w * (1.0f / 32768.0f); }
default: { float_le f; memcpy(&f, p + i * 4, 4); return fabsf((float)f); }
}
};
// Weights come from the first morph frame only, same as the steps.
float absMatrix[12]{};
for (int j = 0; j < dec.nweights; j++) {
const float w = readWeight(vtx + dec.weightoff, dec.weighttype, j);
for (int k = 0; k < 12; k++) {
absMatrix[k] += w * fabsf(gstate.boneMatrix[j * 12 + k]);
}
}
const int type = pos ? dec.pos : dec.nrm;
const int off = pos ? dec.posoff : dec.nrmoff;
float absVec[3]{};
for (int m = 0; m < dec.morphcount; m++) {
const float mw = dec.morphcount > 1 ? fabsf(gstate_c.morphWeights[m]) : 1.0f;
for (int k = 0; k < 3; k++) {
absVec[k] += mw * readScalar(vtx + m * dec.onesize_ + off, type, k);
}
}
for (int i = 0; i < 3; i++) {
out[i] = absVec[0] * absMatrix[i] + absVec[1] * absMatrix[3 + i] + absVec[2] * absMatrix[6 + i] + (pos ? absMatrix[9 + i] : 0.0f);
}
}
struct JitMismatch {
int formats = 0;
int verts = 0;
std::string example;
};
} // namespace
[[maybe_unused]] static bool TestVertexJitMatchesSteps() {
// static, or MSVC treats these as captured references and won't use VERTS as an array bound.
static constexpr int VERTS = 32;
static constexpr int BUF_SIZE = 64 * 1024;
// Decode may overrun by a vertex plus 16 bytes, see DecodeVerts.
u8 *src = (u8 *)AllocateAlignedMemory(BUF_SIZE, 16);
u8 *refOut = (u8 *)AllocateAlignedMemory(BUF_SIZE, 16);
u8 *jitOut = (u8 *)AllocateAlignedMemory(BUF_SIZE, 16);
VertexDecoderJitCache *cache = new VertexDecoderJitCache();
JitMatchRng rng;
std::map<std::string, JitMismatch> mismatches;
int formatsTested = 0;
int formatsJitted = 0;
const bool savedDoubleTexCoords = g_DoubleTextureCoordinates;
auto testFormat = [&](u32 vtype, int uvGenMode, bool doubleTexCoords, bool expand8BitNormals) {
g_DoubleTextureCoordinates = doubleTexCoords;
VertexDecoderOptions opts{};
opts.expand8BitNormalsToFloat = expand8BitNormals;
const u32 vertTypeID = GetVertTypeID(vtype, uvGenMode);
VertexDecoder ref{};
ref.SetVertexType(vertTypeID, opts, nullptr);
// Without a cache, SetVertexType still installs the handwritten decoders for a couple of
// formats. The reference has to be the steps.
ref.jitted_ = nullptr;
if (cache->GetSpaceLeft() < 16384) {
cache->Clear();
}
VertexDecoder jit{};
jit.SetVertexType(vertTypeID, opts, cache);
formatsTested++;
if (!jit.jitted_) {
return;
}
formatsJitted++;
// Which step writes each component: weights (if any) first, then tc, col, nrm, pos.
int stepIndex = ref.weighttype ? 1 : 0;
StepFunction tcStep = ref.tc ? ref.steps_[stepIndex++] : nullptr;
StepFunction colStep = ref.col ? ref.steps_[stepIndex++] : nullptr;
StepFunction nrmStep = ref.nrm ? ref.steps_[stepIndex++] : nullptr;
StepFunction posStep = ref.steps_[stepIndex];
for (int i = 0; i < 8; i++) {
gstate_c.morphWeights[i] = rng.Float24(-0.5f, 1.5f);
}
for (int i = 0; i < 8 * 12; i++) {
gstate.boneMatrix[i] = rng.Float24(-2.0f, 2.0f);
}
const UVScale uvScale{ rng.Float24(-2.0f, 2.0f), rng.Float24(-2.0f, 2.0f), rng.Float24(-1.0f, 1.0f), rng.Float24(-1.0f, 1.0f) };
const int srcBytes = ref.VertexSize() * VERTS;
_assert_(srcBytes + 256 <= BUF_SIZE && ref.decFmt.stride * (VERTS + 1) + 16 <= BUF_SIZE);
// Only plain float positions are cleaned of NaN and infinity (see Step_PosFloat).
bool specialPos[VERTS]{};
FillVertexData(rng, ref, src, VERTS, posStep == &VertexDecoder::Step_PosFloat ? specialPos : nullptr);
const KnownVertexBounds initialBounds{ 0xFFFF, 0xFFFF, 0, 0 };
memset(refOut, 0, BUF_SIZE);
gstate_c.vertexFullAlpha = true;
gstate_c.vertBounds = initialBounds;
// Vary the count a little, so the decoders' leftover-vertex paths get used too.
const int numVerts = VERTS - (formatsTested & 3);
ref.DecodeVerts(refOut, src, &uvScale, numVerts);
const bool refFullAlpha = gstate_c.vertexFullAlpha;
const KnownVertexBounds refBounds = gstate_c.vertBounds;
memset(jitOut, 0, BUF_SIZE);
gstate_c.vertexFullAlpha = true;
gstate_c.vertBounds = initialBounds;
jit.DecodeVerts(jitOut, src, &uvScale, numVerts);
const bool jitFullAlpha = gstate_c.vertexFullAlpha;
const KnownVertexBounds jitBounds = gstate_c.vertBounds;
char fmtDesc[256]{};
ref.ToString(fmtDesc, sizeof(fmtDesc), true);
const char *kind = cache->IsInSpace((const u8 *)jit.jitted_) ? "jit" : "handwritten";
auto record = [&](const char *what, StepFunction step, int badVerts, const std::string &detail) {
std::string key = StringFromFormat("%-4s %-11s %s", what, kind, step ? GetStepFunctionName(step) : "-");
JitMismatch &m = mismatches[key];
if (m.formats == 0) {
m.example = StringFromFormat("%08x %s morph=%d uvgen=%d dbl=%d expand8=%d: %s", vertTypeID, fmtDesc, ref.morphcount, uvGenMode, (int)doubleTexCoords, (int)expand8BitNormals, detail.c_str());
}
m.formats++;
m.verts += badVerts;
};
auto compareComponent = [&](const char *what, StepFunction step, u8 fmt, int off, bool skinnedPos = false, bool skinnedNrm = false) {
if (fmt == DEC_NONE) {
return;
}
const bool skinned = skinnedPos || skinnedNrm;
const int sz = DecodedComponentSize(fmt);
if (sz < 0) {
record(what, step, 0, StringFromFormat("unexpected decoded format %d", fmt));
return;
}
int badVerts = 0;
std::string detail;
for (int v = 0; v < numVerts; v++) {
const u8 *r = refOut + v * ref.decFmt.stride + off;
const u8 *j = jitOut + v * ref.decFmt.stride + off;
if (specialPos[v] && step == posStep) {
// NaN and infinity only have to come out finite. How is up to the platform.
bool finite = true;
for (int c = 0; c < 3; c++) {
float fr, fj;
memcpy(&fr, r + c * 4, 4);
memcpy(&fj, j + c * 4, 4);
finite = finite && std::isfinite(fr) && std::isfinite(fj);
}
if (!finite && badVerts++ == 0) {
float fj[3];
memcpy(fj, j, 12);
detail = StringFromFormat("vert %d: NaN/inf input gave %g %g %g", v, fj[0], fj[1], fj[2]);
}
continue;
}
if (memcmp(r, j, sz) == 0) {
continue;
}
// Tolerate the last bit, see NearlyEqualFloat.
if (fmt == DEC_FLOAT_2 || fmt == DEC_FLOAT_3) {
bool close = true;
for (int c = 0; c < sz / 4; c++) {
float fr, fj;
memcpy(&fr, r + c * 4, 4);
memcpy(&fj, j + c * 4, 4);
close = close && NearlyEqualFloat(fr, fj, ref.morphcount);
}
if (close) {
continue;
}
} else if (fmt == DEC_U8_4) {
// A one-bit rounding difference upstream lands as +-1 on a channel.
bool close = true;
for (int c = 0; c < 4; c++) {
close = close && std::abs((int)r[c] - (int)j[c]) <= 1;
}
if (close) {
continue;
}
}
if (skinned && fmt == DEC_FLOAT_3) {
float terms[3];
SkinTermMagnitudes(ref, src + v * ref.VertexSize(), skinnedPos, terms);
bool close = true;
for (int c = 0; c < 3; c++) {
float fr, fj;
memcpy(&fr, r + c * 4, 4);
memcpy(&fj, j + c * 4, 4);
// About 32 ULPs of the largest term, to cover a rounding per accumulated bone.
if (!(fabsf(fr - fj) <= terms[c] * (1.0f / (1 << 19)))) {
close = false;
}
}
if (close) {
continue;
}
}
if (badVerts++ == 0) {
detail = StringFromFormat("vert %d: steps", v);
const bool isFloat = fmt == DEC_FLOAT_2 || fmt == DEC_FLOAT_3;
for (int pass = 0; pass < 2; pass++) {
const u8 *p = pass == 0 ? r : j;
if (pass == 1) {
detail += " vs jit";
}
for (int c = 0; c < (isFloat ? sz / 4 : sz); c++) {
if (isFloat) {
float f;
memcpy(&f, p + c * 4, 4);
detail += StringFromFormat(" %.9g", f);
} else if (fmt == DEC_S16_3) {
if (c < 3) {
s16 s;
memcpy(&s, p + c * 2, 2);
detail += StringFromFormat(" %d", s);
}
} else {
detail += StringFromFormat(" %d", fmt == DEC_S8_3 ? (int)(s8)p[c] : (int)p[c]);
}
}
}
}
}
if (badVerts) {
record(what, step, badVerts, detail);
}
};
compareComponent("uv", tcStep, ref.decFmt.uvfmt, ref.decFmt.uvoff);
compareComponent("col", colStep, ref.decFmt.c0fmt, ref.decFmt.c0off);
compareComponent("nrm", nrmStep, ref.decFmt.nrmfmt, ref.decFmt.nrmoff, false, ref.skinInDecode);
compareComponent("pos", posStep, DEC_FLOAT_3, ref.decFmt.posoff, ref.skinInDecode && !ref.throughmode, false);
if (refFullAlpha != jitFullAlpha) {
record("fullAlpha", colStep, 0, StringFromFormat("steps %d vs jit %d", (int)refFullAlpha, (int)jitFullAlpha));
}
// TODO: Only the steps track bounds for float UVs in through mode. Undecided which way to go.
if (tcStep != &VertexDecoder::Step_TcFloatThrough && memcmp(&refBounds, &jitBounds, sizeof(refBounds)) != 0) {
record("bounds", tcStep, 0, StringFromFormat("steps %d,%d-%d,%d vs jit %d,%d-%d,%d",
refBounds.minU, refBounds.minV, refBounds.maxU, refBounds.maxV, jitBounds.minU, jitBounds.minV, jitBounds.maxU, jitBounds.maxV));
}
};
static const int colFormats[] = { 0, 4, 5, 6, 7 };
for (int through = 0; through <= 1; through++) {
for (int tc = 0; tc < 4; tc++) {
for (int col : colFormats) {
for (int nrm = 0; nrm < 4; nrm++) {
for (int pos = 1; pos < 4; pos++) {
for (int wt = 0; wt < 4; wt++) {
for (int nweights = 1; nweights <= (wt ? 8 : 1); nweights++) {
for (int morph = 1; morph <= 8; morph++) {
u32 vtype = (tc << GE_VTYPE_TC_SHIFT) | (col << GE_VTYPE_COL_SHIFT) | (nrm << GE_VTYPE_NRM_SHIFT) |
(pos << GE_VTYPE_POS_SHIFT) | (wt << GE_VTYPE_WEIGHT_SHIFT) |
((nweights - 1) << GE_VTYPE_WEIGHTCOUNT_SHIFT) | ((morph - 1) << GE_VTYPE_MORPHCOUNT_SHIFT) |
(through ? GE_VTYPE_THROUGH : 0);
// The UV gen mode and double texcoords only change anything with texcoords, and
// only the TEXTURE_COORDS / TEXTURE_MATRIX split matters (the other two modes
// decode like these). The expand option only applies to plain 8-bit normals.
const int uvGenModes = (tc && !through) ? 2 : 1;
const int doubleModes = tc ? 2 : 1;
const int expandModes = (nrm == 1 && morph == 1 && !wt) ? 2 : 1;
for (int uvGen = 0; uvGen < uvGenModes; uvGen++) {
for (int dbl = 0; dbl < doubleModes; dbl++) {
for (int expand = 0; expand < expandModes; expand++) {
testFormat(vtype, uvGen == 0 ? GE_TEXMAP_TEXTURE_COORDS : GE_TEXMAP_TEXTURE_MATRIX, dbl != 0, expand != 0);
}
}
}
}
}
}
}
}
}
}
}
g_DoubleTextureCoordinates = savedDoubleTexCoords;
delete cache;
FreeAlignedMemory(src);
FreeAlignedMemory(refOut);
FreeAlignedMemory(jitOut);
printf("VertexJitMatchesSteps: %d formats, %d jitted, %d mismatch kinds\n", formatsTested, formatsJitted, (int)mismatches.size());
for (auto &iter : mismatches) {
printf(" %s: %d formats, %d verts\n e.g. %s\n", iter.first.c_str(), iter.second.formats, iter.second.verts, iter.second.example.c_str());
}
return mismatches.empty();
}
typedef bool (*VertexTestFunc)();
static VertexTestFunc vertdecTestFuncs[] = {
&TestVertex8,
&TestVertex16,
&TestVertexFloat,
&TestVertex8Through,
&TestVertex16Through,
&TestVertexFloatThrough,
&TestVertexColor8888,
&TestVertexColor4444,
&TestVertexColor5551,
&TestVertexColor565,
&TestVertex8Skin,
&TestVertex16Skin,
&TestVertexFloatSkin,
&TestVertexJitMatchesSteps,
};
bool TestVertexJit() {
VertexDecoderTestHarness dec;
/*for (int i = 0; i < 100; ++i) {
dec.AddFloat(0.5f, 1.0f, -1.0f);
}
int vtype = GE_VTYPE_POS_FLOAT;*/
/*for (int i = 0; i < 100; ++i) {
dec.Add16(32767, 0, 32768);
}
int vtype = GE_VTYPE_POS_16BIT;*/
for (int i = 0; i < 100; ++i) {
dec.Add8(127, 0, 128);
}
int vtype = GE_VTYPE_POS_8BIT;
double yesJit = dec.ExecuteTimed(vtype, 100, true);
double noJit = dec.ExecuteTimed(vtype, 100, false);
float x = dec.GetFloat();
float y = dec.GetFloat();
float z = dec.GetFloat();
printf("Result: %f, %f, %f\n", x, y, z);
printf("Jit was %fx faster than steps.\n\n", yesJit / noJit);
bool pass = true;
for (size_t i = 0; i < ARRAY_SIZE(vertdecTestFuncs); ++i) {
if (!vertdecTestFuncs[i]()) {
pass = false;
}
}
return pass;
}