mirror of
https://github.com/yuzu-emu/FasTC.git
synced 2024-12-01 00:04:21 +01:00
- Added a parameter to the PCA computation that returns the first and second
eigenvalues of the covariance matrix associated with the cluster. - Compared results of testing the ratio of eigenvalues as a measurement of 'linearity' for the different shapes, and output statistics. - Added a #define that controls whether or not we do shape estimation using quantized AABB error or eigenvalue ratios. The former seems to be better.
This commit is contained in:
parent
71fbbca1ee
commit
4c359f42a7
@ -37,10 +37,16 @@
|
||||
#define ALIGN_SSE __attribute__((aligned(16)))
|
||||
#endif
|
||||
|
||||
#define USE_PCA_FOR_SHAPE_ESTIMATION
|
||||
|
||||
enum EBlockStats {
|
||||
eBlockStat_Path,
|
||||
eBlockStat_Mode,
|
||||
|
||||
eBlockStat_SingleShapeEstimate,
|
||||
eBlockStat_TwoShapeEstimate,
|
||||
eBlockStat_ThreeShapeEstimate,
|
||||
|
||||
eBlockStat_ModeZeroEstimate,
|
||||
eBlockStat_ModeOneEstimate,
|
||||
eBlockStat_ModeTwoEstimate,
|
||||
@ -66,6 +72,10 @@ static const char *kBlockStatString[kNumBlockStats] = {
|
||||
"BlockStat_Path",
|
||||
"BlockStat_Mode",
|
||||
|
||||
"BlockStat_SingleShapeEstimate",
|
||||
"BlockStat_TwoShapeEstimate",
|
||||
"BlockStat_ThreeShapeEstimate",
|
||||
|
||||
"BlockStat_ModeZeroEstimate",
|
||||
"BlockStat_ModeOneEstimate",
|
||||
"BlockStat_ModeTwoEstimate",
|
||||
@ -972,7 +982,8 @@ double BC7CompressionMode::CompressCluster(const RGBACluster &cluster, RGBAVecto
|
||||
#if 1
|
||||
RGBAVector avg = cluster.GetTotal() / float(cluster.GetNumPoints());
|
||||
RGBADir axis;
|
||||
::GetPrincipalAxis(cluster.GetNumPoints(), cluster.GetPoints(), axis);
|
||||
double eigOne;
|
||||
::GetPrincipalAxis(cluster.GetNumPoints(), cluster.GetPoints(), axis, eigOne, NULL);
|
||||
|
||||
float mindp = FLT_MAX, maxdp = -FLT_MAX;
|
||||
for(int i = 0 ; i < cluster.GetNumPoints(); i++) {
|
||||
@ -1677,55 +1688,91 @@ namespace BC7C
|
||||
assert(!(clusters[0].GetPointBitString() & clusters[2].GetPointBitString()));
|
||||
}
|
||||
|
||||
static double EstimateTwoClusterError(RGBACluster &c) {
|
||||
static double EstimateTwoClusterError(RGBACluster &c, double (&estimates)[2]) {
|
||||
RGBAVector Min, Max, v;
|
||||
c.GetBoundingBox(Min, Max);
|
||||
v = Max - Min;
|
||||
if(v * v == 0) {
|
||||
gModeEstimate[1] = gModeEstimate[3] = 0.0;
|
||||
estimates[0] = estimates[1] = 0.0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const float *w = BC7C::GetErrorMetric();
|
||||
|
||||
const double err1 = 0.0001 + c.QuantizedError(Min, Max, 8, 0xFFFCFCFC, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
const double err1 = c.QuantizedError(Min, Max, 8, 0xFFFCFCFC, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
if(err1 >= 0.0)
|
||||
gModeEstimate[1] = err1;
|
||||
estimates[0] = err1;
|
||||
else
|
||||
gModeEstimate[1] = min(gModeEstimate[1], err1);
|
||||
estimates[0] = min(estimates[0], err1);
|
||||
|
||||
const double err3 = 0.0001 + c.QuantizedError(Min, Max, 8, 0xFFFEFEFE, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
const double err3 = c.QuantizedError(Min, Max, 8, 0xFFFEFEFE, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
if(err3 >= 0.0)
|
||||
gModeEstimate[3] = err3;
|
||||
estimates[1] = err3;
|
||||
else
|
||||
gModeEstimate[3] = min(gModeEstimate[3], err3);
|
||||
estimates[1] = min(estimates[1], err3);
|
||||
|
||||
return min(err1, err3);
|
||||
double error = 0.0001;
|
||||
#ifdef USE_PCA_FOR_SHAPE_ESTIMATION
|
||||
double eigOne = c.GetPrincipalEigenvalue();
|
||||
double eigTwo = c.GetSecondEigenvalue();
|
||||
if(eigOne != 0.0) {
|
||||
error += eigTwo / eigOne;
|
||||
}
|
||||
else {
|
||||
error += 1.0;
|
||||
}
|
||||
#else
|
||||
error += min(err1, err3);
|
||||
#endif
|
||||
return error;
|
||||
}
|
||||
|
||||
static double EstimateThreeClusterError(RGBACluster &c) {
|
||||
static double EstimateThreeClusterError(RGBACluster &c, double (&estimates)[2]) {
|
||||
RGBAVector Min, Max, v;
|
||||
c.GetBoundingBox(Min, Max);
|
||||
v = Max - Min;
|
||||
if(v * v == 0) {
|
||||
gModeEstimate[0] = gModeEstimate[2] = 0.0;
|
||||
estimates[0] = estimates[1] = 0.0;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
const float *w = BC7C::GetErrorMetric();
|
||||
const double err0 = 0.0001 + c.QuantizedError(Min, Max, 4, 0xFFF0F0F0, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
if(err0 >= 0.0)
|
||||
gModeEstimate[0] = err0;
|
||||
estimates[0] = err0;
|
||||
else
|
||||
gModeEstimate[0] = min(gModeEstimate[0], err0);
|
||||
estimates[0] = min(estimates[0], err0);
|
||||
|
||||
const double err2 = 0.0001 + c.QuantizedError(Min, Max, 4, 0xFFF8F8F8, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
if(err2 >= 0.0)
|
||||
gModeEstimate[2] = err2;
|
||||
estimates[1] = err2;
|
||||
else
|
||||
gModeEstimate[2] = min(gModeEstimate[2], err2);
|
||||
estimates[1] = min(estimates[1], err2);
|
||||
|
||||
return min(err0, err2);
|
||||
double error = 0.0001;
|
||||
#ifdef USE_PCA_FOR_SHAPE_ESTIMATION
|
||||
double eigOne = c.GetPrincipalEigenvalue();
|
||||
double eigTwo = c.GetSecondEigenvalue();
|
||||
|
||||
// printf("EigOne: %08.3f\tEigTwo: %08.3f\n", eigOne, eigTwo);
|
||||
if(eigOne != 0.0) {
|
||||
error += eigTwo / eigOne;
|
||||
}
|
||||
else {
|
||||
error += 1.0;
|
||||
}
|
||||
#else
|
||||
error += min(err0, err2);
|
||||
#endif
|
||||
return error;
|
||||
}
|
||||
|
||||
static void UpdateErrorEstimate(uint32 mode, double est) {
|
||||
assert(mode >= 0);
|
||||
assert(mode < BC7CompressionMode::kNumModes);
|
||||
if(gModeEstimate[mode] == -1.0 || est < gModeEstimate[mode]) {
|
||||
gModeEstimate[mode] = est;
|
||||
}
|
||||
}
|
||||
|
||||
// Compress a single block.
|
||||
@ -1825,12 +1872,31 @@ namespace BC7C
|
||||
}
|
||||
else {
|
||||
const float *w = GetErrorMetric();
|
||||
gModeEstimate[6] = 0.0001 + blockCluster.QuantizedError(Min, Max, 4, 0xFFF0F0F0, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
const double err = 0.0001 + blockCluster.QuantizedError(Min, Max, 4, 0xFEFEFEFE, RGBAVector(w[0], w[1], w[2], w[3]));
|
||||
UpdateErrorEstimate(6, err);
|
||||
|
||||
#ifdef USE_PCA_FOR_SHAPE_ESTIMATION
|
||||
if(statManager) {
|
||||
double eigOne = blockCluster.GetPrincipalEigenvalue();
|
||||
double eigTwo = blockCluster.GetSecondEigenvalue();
|
||||
double error;
|
||||
if(eigOne != 0.0) {
|
||||
error = eigTwo / eigOne;
|
||||
}
|
||||
else {
|
||||
error = 1.0;
|
||||
}
|
||||
|
||||
BlockStat s (kBlockStatString[eBlockStat_SingleShapeEstimate], error);
|
||||
statManager->AddStat(blockIdx, s);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// First we must figure out which shape to use. To do this, simply
|
||||
// see which shape has the smallest sum of minimum bounding spheres.
|
||||
double estimates[2] = { -1.0, -1.0 };
|
||||
double bestError[2] = { DBL_MAX, DBL_MAX };
|
||||
int bestShapeIdx[2] = { -1, -1 };
|
||||
RGBACluster bestClusters[2][3];
|
||||
@ -1841,8 +1907,38 @@ namespace BC7C
|
||||
PopulateTwoClustersForShape(blockCluster, i, clusters);
|
||||
|
||||
double err = 0.0;
|
||||
double errEstimate[2] = { -1.0, -1.0 };
|
||||
for(int ci = 0; ci < 2; ci++) {
|
||||
err += EstimateTwoClusterError(clusters[ci]);
|
||||
double shapeEstimate[2] = { -1.0, -1.0 };
|
||||
err += EstimateTwoClusterError(clusters[ci], shapeEstimate);
|
||||
|
||||
for(int ei = 0; ei < 2; ei++) {
|
||||
if(shapeEstimate[ei] >= 0.0) {
|
||||
if(errEstimate[ei] == -1.0) {
|
||||
errEstimate[ei] = shapeEstimate[ei];
|
||||
}
|
||||
else {
|
||||
errEstimate[ei] += shapeEstimate[ei];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_PCA_FOR_SHAPE_ESTIMATION
|
||||
err /= 2.0;
|
||||
#endif
|
||||
|
||||
if(errEstimate[0] != -1.0) {
|
||||
UpdateErrorEstimate(1, errEstimate[0]);
|
||||
}
|
||||
|
||||
if(errEstimate[1] != -1.0) {
|
||||
UpdateErrorEstimate(3, errEstimate[1]);
|
||||
}
|
||||
|
||||
if(statManager && err < bestError[0]) {
|
||||
BlockStat s = BlockStat(kBlockStatString[eBlockStat_TwoShapeEstimate], err);
|
||||
statManager->AddStat(blockIdx, s);
|
||||
}
|
||||
|
||||
// If it's small, we'll take it!
|
||||
@ -1874,8 +1970,38 @@ namespace BC7C
|
||||
PopulateThreeClustersForShape(blockCluster, i, clusters);
|
||||
|
||||
double err = 0.0;
|
||||
double errEstimate[2] = { -1.0, -1.0 };
|
||||
for(int ci = 0; ci < 3; ci++) {
|
||||
err += EstimateThreeClusterError(clusters[ci]);
|
||||
double shapeEstimate[2] = { -1.0, -1.0 };
|
||||
err += EstimateThreeClusterError(clusters[ci], shapeEstimate);
|
||||
|
||||
for(int ei = 0; ei < 2; ei++) {
|
||||
if(shapeEstimate[ei] >= 0.0) {
|
||||
if(errEstimate[ei] == -1.0) {
|
||||
errEstimate[ei] = shapeEstimate[ei];
|
||||
}
|
||||
else {
|
||||
errEstimate[ei] += shapeEstimate[ei];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_PCA_FOR_SHAPE_ESTIMATION
|
||||
err /= 3.0;
|
||||
#endif
|
||||
|
||||
if(errEstimate[0] != -1.0) {
|
||||
UpdateErrorEstimate(0, errEstimate[0]);
|
||||
}
|
||||
|
||||
if(errEstimate[1] != -1.0) {
|
||||
UpdateErrorEstimate(2, errEstimate[1]);
|
||||
}
|
||||
|
||||
if(statManager && err < bestError[1]) {
|
||||
BlockStat s = BlockStat(kBlockStatString[eBlockStat_ThreeShapeEstimate], err);
|
||||
statManager->AddStat(blockIdx, s);
|
||||
}
|
||||
|
||||
// If it's small, we'll take it!
|
||||
|
@ -307,12 +307,52 @@ void RGBACluster::GetPrincipalAxis(RGBADir &axis) {
|
||||
}
|
||||
|
||||
RGBAVector avg = m_Total / float(m_NumPoints);
|
||||
::GetPrincipalAxis(m_NumPoints, m_DataPoints, m_PrincipalAxis);
|
||||
m_PowerMethodIterations = ::GetPrincipalAxis(
|
||||
m_NumPoints,
|
||||
m_DataPoints,
|
||||
m_PrincipalAxis,
|
||||
m_PrincipalEigenvalue,
|
||||
&m_SecondEigenvalue
|
||||
);
|
||||
|
||||
m_PrincipalAxisCached = true;
|
||||
|
||||
GetPrincipalAxis(axis);
|
||||
}
|
||||
|
||||
double RGBACluster::GetPrincipalEigenvalue() {
|
||||
|
||||
if(!m_PrincipalAxisCached) {
|
||||
RGBADir dummy;
|
||||
GetPrincipalAxis(dummy);
|
||||
}
|
||||
|
||||
assert(m_PrincipalAxisCached);
|
||||
return m_PrincipalEigenvalue;
|
||||
}
|
||||
|
||||
double RGBACluster::GetSecondEigenvalue() {
|
||||
|
||||
if(!m_PrincipalAxisCached) {
|
||||
RGBADir dummy;
|
||||
GetPrincipalAxis(dummy);
|
||||
}
|
||||
|
||||
assert(m_PrincipalAxisCached);
|
||||
return m_SecondEigenvalue;
|
||||
}
|
||||
|
||||
uint32 RGBACluster::GetPowerMethodIterations() {
|
||||
|
||||
if(!m_PrincipalAxisCached) {
|
||||
RGBADir dummy;
|
||||
GetPrincipalAxis(dummy);
|
||||
}
|
||||
|
||||
assert(m_PrincipalAxisCached);
|
||||
return m_PowerMethodIterations;
|
||||
}
|
||||
|
||||
double RGBACluster::QuantizedError(const RGBAVector &p1, const RGBAVector &p2, uint8 nBuckets, uint32 bitMask, const RGBAVector &errorMetricVec, const int pbits[2], int *indices) const {
|
||||
|
||||
// nBuckets should be a power of two.
|
||||
@ -403,7 +443,58 @@ void ClampEndpoints(RGBAVector &p1, RGBAVector &p2) {
|
||||
clamp(p2.a, 0.0f, 255.0f);
|
||||
}
|
||||
|
||||
void GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis) {
|
||||
static uint32 PowerIteration(const RGBAMatrix &mat, RGBADir &eigVec, double &eigVal) {
|
||||
|
||||
int numIterations = 0;
|
||||
const int kMaxNumIterations = 200;
|
||||
|
||||
for(int nTries = 0; nTries < 3; nTries++) {
|
||||
// !SPEED! Find eigenvectors by using the power method. This is good because the
|
||||
// matrix is only 4x4, which allows us to use SIMD...
|
||||
RGBAVector b = RGBAVector(rand());
|
||||
assert(b.Length() > 0);
|
||||
b /= b.Length();
|
||||
|
||||
bool fixed = false;
|
||||
numIterations = 0;
|
||||
while(!fixed && ++numIterations < kMaxNumIterations) {
|
||||
|
||||
RGBAVector newB = mat * b;
|
||||
|
||||
// !HACK! If the principal eigenvector of the covariance matrix
|
||||
// converges to zero, that means that the points lie equally
|
||||
// spaced on a sphere in this space. In this (extremely rare)
|
||||
// situation, just choose a point and use it as the principal
|
||||
// direction.
|
||||
const float newBlen = newB.Length();
|
||||
if(newBlen < 1e-10) {
|
||||
eigVec = b;
|
||||
eigVal = 0.0;
|
||||
return numIterations;
|
||||
}
|
||||
|
||||
eigVal = newB.Length();
|
||||
newB /= eigVal;
|
||||
|
||||
if(fabs(1.0f - (b * newB)) < 1e-5)
|
||||
fixed = true;
|
||||
|
||||
b = newB;
|
||||
}
|
||||
|
||||
eigVec = b;
|
||||
if(numIterations < kMaxNumIterations) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(numIterations == kMaxNumIterations) {
|
||||
eigVal = 0.0;
|
||||
}
|
||||
return numIterations;
|
||||
}
|
||||
|
||||
uint32 GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis, double &eigOne, double *eigTwo) {
|
||||
|
||||
assert(nPts > 0);
|
||||
assert(nPts <= kMaxNumDataPoints);
|
||||
@ -445,7 +536,7 @@ void GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis) {
|
||||
|
||||
if(uptsIdx == 1) {
|
||||
axis.r = axis.g = axis.b = axis.a = 0.0f;
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
// Collinear?
|
||||
else {
|
||||
@ -462,7 +553,7 @@ void GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis) {
|
||||
|
||||
if(collinear) {
|
||||
axis = dir;
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,38 +573,36 @@ void GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis) {
|
||||
}
|
||||
}
|
||||
|
||||
// !SPEED! Find eigenvectors by using the power method. This is good because the
|
||||
// matrix is only 4x4, which allows us to use SIMD...
|
||||
RGBAVector b = toPtsMax;
|
||||
assert(b.Length() > 0);
|
||||
b /= b.Length();
|
||||
uint32 iters = PowerIteration(covMatrix, axis, eigOne);
|
||||
|
||||
bool fixed = false;
|
||||
int infLoopPrevention = 0;
|
||||
const int kMaxNumIterations = 200;
|
||||
while(!fixed && ++infLoopPrevention < kMaxNumIterations) {
|
||||
if(NULL != eigTwo) {
|
||||
if(eigOne != 0.0) {
|
||||
RGBAMatrix reduced = covMatrix - eigOne * RGBAMatrix(
|
||||
axis.c[0] * axis.c[0], axis.c[0] * axis.c[1], axis.c[0] * axis.c[2], axis.c[0] * axis.c[3],
|
||||
axis.c[1] * axis.c[0], axis.c[1] * axis.c[1], axis.c[1] * axis.c[2], axis.c[1] * axis.c[3],
|
||||
axis.c[2] * axis.c[0], axis.c[2] * axis.c[1], axis.c[2] * axis.c[2], axis.c[2] * axis.c[3],
|
||||
axis.c[3] * axis.c[0], axis.c[3] * axis.c[1], axis.c[3] * axis.c[2], axis.c[3] * axis.c[3]
|
||||
);
|
||||
|
||||
RGBAVector newB = covMatrix * b;
|
||||
|
||||
// !HACK! If the principal eigenvector of the covariance matrix
|
||||
// converges to zero, that means that the points lie equally
|
||||
// spaced on a sphere in this space. In this (extremely rare)
|
||||
// situation, just choose a point and use it as the principal
|
||||
// direction.
|
||||
const float newBlen = newB.Length();
|
||||
if(newBlen < 1e-10) {
|
||||
axis = toPts[0];
|
||||
return;
|
||||
bool allZero = true;
|
||||
for(int i = 0; i < 16; i++) {
|
||||
if(fabs(reduced[i]) > 0.0005) {
|
||||
allZero = false;
|
||||
}
|
||||
}
|
||||
|
||||
newB /= newB.Length();
|
||||
|
||||
if(fabs(1.0f - (b * newB)) < 1e-5)
|
||||
fixed = true;
|
||||
|
||||
b = newB;
|
||||
if(allZero) {
|
||||
*eigTwo = 0.0;
|
||||
}
|
||||
else {
|
||||
RGBADir dummyDir;
|
||||
iters += PowerIteration(reduced, dummyDir, *eigTwo);
|
||||
}
|
||||
}
|
||||
else {
|
||||
*eigTwo = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
assert(infLoopPrevention < kMaxNumIterations);
|
||||
axis = b;
|
||||
return iters;
|
||||
}
|
||||
|
@ -162,6 +162,18 @@ private:
|
||||
|
||||
public:
|
||||
|
||||
RGBAMatrix(
|
||||
float _m1, float _m2, float _m3, float _m4,
|
||||
float _m5, float _m6, float _m7, float _m8,
|
||||
float _m9, float _m10, float _m11, float _m12,
|
||||
float _m13, float _m14, float _m15, float _m16
|
||||
) :
|
||||
m1(_m1), m2(_m2), m3(_m3), m4(_m4),
|
||||
m5(_m5), m6(_m6), m7(_m7), m8(_m8),
|
||||
m9(_m9), m10(_m10), m11(_m11), m12(_m12),
|
||||
m13(_m13), m14(_m14), m15(_m15), m16(_m16)
|
||||
{ }
|
||||
|
||||
RGBAMatrix() :
|
||||
m1(1.0f), m2(0.0f), m3(0.0f), m4(0.0f),
|
||||
m5(0.0f), m6(1.0f), m7(0.0f), m8(0.0f),
|
||||
@ -294,7 +306,10 @@ public:
|
||||
m_PointBitString(c.m_PointBitString),
|
||||
m_Min(c.m_Min),
|
||||
m_Max(c.m_Max),
|
||||
m_PrincipalAxisCached(false)
|
||||
m_PrincipalAxisCached(c.m_PrincipalAxisCached),
|
||||
m_SecondEigenvalue(c.m_SecondEigenvalue),
|
||||
m_PowerMethodIterations(c.m_PowerMethodIterations),
|
||||
m_PrincipalAxis(c.m_PrincipalAxis)
|
||||
{
|
||||
memcpy(this->m_DataPoints, c.m_DataPoints, m_NumPoints * sizeof(RGBAVector));
|
||||
}
|
||||
@ -327,6 +342,9 @@ public:
|
||||
double QuantizedError(const RGBAVector &p1, const RGBAVector &p2, uint8 nBuckets, uint32 bitMask, const RGBAVector &errorMetricVec, const int pbits[2] = NULL, int *indices = NULL) const;
|
||||
|
||||
// Returns the principal axis for this point cluster.
|
||||
double GetPrincipalEigenvalue();
|
||||
double GetSecondEigenvalue();
|
||||
uint32 GetPowerMethodIterations();
|
||||
void GetPrincipalAxis(RGBADir &axis);
|
||||
|
||||
bool AllSamePoint() const { return m_Max == m_Min; }
|
||||
@ -345,11 +363,14 @@ private:
|
||||
RGBAVector m_Min, m_Max;
|
||||
int m_PointBitString;
|
||||
|
||||
double m_PrincipalEigenvalue;
|
||||
double m_SecondEigenvalue;
|
||||
uint32 m_PowerMethodIterations;
|
||||
RGBADir m_PrincipalAxis;
|
||||
bool m_PrincipalAxisCached;
|
||||
};
|
||||
|
||||
extern uint8 QuantizeChannel(const uint8 val, const uint8 mask, const int pBit = -1);
|
||||
extern void GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis);
|
||||
extern uint32 GetPrincipalAxis(int nPts, const RGBAVector *pts, RGBADir &axis, double &eigOne, double *eigTwo);
|
||||
|
||||
#endif //__RGBA_ENDPOINTS_H__
|
||||
|
Loading…
Reference in New Issue
Block a user