mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-16 07:50:28 +02:00
Merge pull request #22414 from hrydgard/ge-bit-exact
SoftGPU: Make it mostly bit exact vs PSP hardware
This commit is contained in:
53 files changed
+3086
-640
No files matched your search
@@ -18,7 +18,7 @@ for it:
|
||||
| [docs/translations.md](docs/translations.md) | Translating UI strings with Tools/langtool |
|
||||
| [docs/pspautotests.md](docs/pspautotests.md) | Workflow for improving PPSSPP using pspautotests |
|
||||
| [docs/pspautotests-hardware.md](docs/pspautotests-hardware.md) | Writing a new pspautotest, and running it on a real PSP over PSPLink to record its `.expected` |
|
||||
| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests |
|
||||
| [docs/frametest.md](docs/frametest.md) | Framedump rendering tests, and replaying a dump on a real PSP |
|
||||
| [docs/sceGe.md](docs/sceGe.md) | How the firmware queues display lists, what SIGNAL and FINISH interrupts do and in which order, and how `ProcessDLQueue()` keeps that order |
|
||||
| [docs/sceAudio.md](docs/sceAudio.md) | How the audio output calls block, how deep they buffer, and what each error means |
|
||||
| [docs/WebSocketDebugger.md](docs/WebSocketDebugger.md) | WebSocket debugger protocol reference |
|
||||
|
||||
@@ -1374,6 +1374,7 @@ if(UNITTEST)
|
||||
unittest/TestLzrc.cpp
|
||||
unittest/TestMpegCsc.cpp
|
||||
unittest/TestSplineTessellation.cpp
|
||||
unittest/TestGEMath.cpp
|
||||
unittest/TestDemangle.cpp
|
||||
unittest/TestTextureReplacer.cpp
|
||||
unittest/TestRiscVEmitter.cpp
|
||||
|
||||
@@ -204,6 +204,9 @@ static const CommandLineParam g_autoParams[] = {
|
||||
{POFF(enableLogging), CmdParamType::Bool, "log", '\0', "Full log output, not just emulated printfs", CmdLineMode::Headless},
|
||||
{POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless},
|
||||
{POFF(screenshotFilenameSave), CmdParamType::String, "screenshot-save", '\0', "Save rendered screenshot to specified path (PNG if the path ends in .png, BMP otherwise)", CmdLineMode::Headless},
|
||||
{POFF(replayEnd), CmdParamType::Int, "replay-end", '\0', "When replaying a GE frame dump, only draw primitives up to this one (1-based)", CmdLineMode::Headless},
|
||||
{POFF(depthFilenameSave), CmdParamType::String, "depth-save", '\0', "Save the depth buffer to FILE along with the screenshot (u32 width, u32 height, u16 depths)", CmdLineMode::Headless},
|
||||
{POFF(screenshotRenderTarget), CmdParamType::Bool, "screenshot-render-target", '\0', "Take screenshots of the current render target instead of the displayed framebuffer", CmdLineMode::Headless},
|
||||
{POFF(screenshotFilenameDiff), CmdParamType::String, "screenshot-diff", '\0', "Save a visual comparison image to FILE when comparing screenshots", CmdLineMode::Headless},
|
||||
{POFF(screenshotSaveKeepAlpha), CmdParamType::Bool, "screenshot-keep-alpha", '\0', "Preserve the alpha channel when saving PNG screenshots (default: alpha is forced to 255)", CmdLineMode::Headless},
|
||||
{POFF(timeoutWall), CmdParamType::Double, "timeout-wall", '\0', "Stop the run after this many real seconds", CmdLineMode::Headless},
|
||||
|
||||
@@ -176,6 +176,9 @@ struct CommandLineOptions {
|
||||
|
||||
std::optional<std::string> screenshotFilename;
|
||||
std::optional<std::string> screenshotFilenameSave;
|
||||
std::optional<std::string> depthFilenameSave;
|
||||
std::optional<int> replayEnd;
|
||||
std::optional<bool> screenshotRenderTarget;
|
||||
std::optional<std::string> screenshotFilenameDiff;
|
||||
// Headless: preserve the alpha channel when saving PNG screenshots.
|
||||
std::optional<bool> screenshotSaveKeepAlpha;
|
||||
|
||||
@@ -173,6 +173,8 @@ set(GPU_SOURCES
|
||||
Software/DrawPixel.h
|
||||
Software/FuncId.cpp
|
||||
Software/FuncId.h
|
||||
Software/GEMath.cpp
|
||||
Software/GEMath.h
|
||||
Software/Lighting.cpp
|
||||
Software/Lighting.h
|
||||
Software/Rasterizer.cpp
|
||||
|
||||
+346
-23
@@ -16,6 +16,11 @@
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/Common.h"
|
||||
#include "Common/CPUDetect.h"
|
||||
#include "Common/Profiler/Profiler.h"
|
||||
@@ -24,6 +29,7 @@
|
||||
#include "GPU/Common/SoftwareTransformCommon.h"
|
||||
#include "GPU/ge_constants.h"
|
||||
#include "GPU/GPUState.h" // only needed for UVScale stuff
|
||||
#include "GPU/Software/GEMath.h"
|
||||
|
||||
class SimpleBufferManager {
|
||||
private:
|
||||
@@ -49,12 +55,14 @@ namespace Spline {
|
||||
|
||||
static void CopyQuadIndex(u16 *&indices, GEPatchPrimType type, const int idx0, const int idx1, const int idx2, const int idx3) {
|
||||
if (type == GE_PATCHPRIM_LINES) {
|
||||
// A zigzag per quad: left edge down, the diagonal up to the top right, right edge down (gpu/probe exp175,
|
||||
// exact: line colors and lit start pixels show the directions, and overlapping pixels the order).
|
||||
*(indices++) = idx0;
|
||||
*(indices++) = idx2;
|
||||
*(indices++) = idx2;
|
||||
*(indices++) = idx1;
|
||||
*(indices++) = idx1;
|
||||
*(indices++) = idx3;
|
||||
*(indices++) = idx1;
|
||||
*(indices++) = idx2;
|
||||
} else {
|
||||
*(indices++) = idx0;
|
||||
*(indices++) = idx2;
|
||||
@@ -77,6 +85,142 @@ void BuildIndex(u16 *indices, int &count, int num_u, int num_v, GEPatchPrimType
|
||||
}
|
||||
}
|
||||
|
||||
// Bezier patches as the GE evaluates them (gpu/probe exp48-51, exp139-140, bit exact). The parameter is k/256,
|
||||
// rounded toward the middle of the patch, and columns (v) are evaluated before rows (u), by de Casteljau.
|
||||
// Positions and texture coordinates: each lerp in 16-bit fixed point at the larger operand's exponent, both
|
||||
// operands truncated toward zero to that grid, then a + floor((b - a) k / 256). At k = 0 or 256 a lerp passes
|
||||
// its operand through unchanged. Colors: the same lerp on (c << 7) | 0x7F per channel, result >> 7.
|
||||
// Normals (exp142-143): the cross product (as the matrix unit sums it) of two tangents, each the difference of
|
||||
// the de Casteljau's last two points: along u, the row's; along v, those of the columns evaluated along u.
|
||||
static int BezierParam256(int i, int n) {
|
||||
return 2 * i <= n ? (256 * i) / n : 256 - (256 * (n - i)) / n;
|
||||
}
|
||||
|
||||
static float GEBezierLerp(float a, float b, int k) {
|
||||
if (k == 0) {
|
||||
return a;
|
||||
} else if (k == 256) {
|
||||
return b;
|
||||
}
|
||||
uint32_t ba, bb;
|
||||
memcpy(&ba, &a, 4);
|
||||
memcpy(&bb, &b, 4);
|
||||
const int ea = (ba >> 23) & 0xFF;
|
||||
const int eb = (bb >> 23) & 0xFF;
|
||||
const int e = std::max(ea, eb);
|
||||
if (e == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
// Mantissas (24 bits) shifted to 16 bits at exponent e, truncated toward zero.
|
||||
auto toFixed = [e](uint32_t bits, int ex) -> int {
|
||||
if (ex == 0 || e - ex >= 16) {
|
||||
return 0;
|
||||
}
|
||||
const int m = (int)(((bits & 0x7FFFFF) | 0x800000) >> (e - ex + 8));
|
||||
return (bits & 0x80000000) ? -m : m;
|
||||
};
|
||||
const int fa = toFixed(ba, ea);
|
||||
const int fb = toFixed(bb, eb);
|
||||
const int r = fa + (((fb - fa) * k) >> 8);
|
||||
// r * 2^(e - 127 - 15), exact.
|
||||
if (e <= 15) {
|
||||
return ldexpf((float)r, e - 127 - 15);
|
||||
}
|
||||
const uint32_t scaleBits = (uint32_t)(e - 15) << 23;
|
||||
float scale;
|
||||
memcpy(&scale, &scaleBits, 4);
|
||||
return (float)r * scale;
|
||||
}
|
||||
|
||||
// Also returns the de Casteljau's last two points, which the normal's tangents come from.
|
||||
static float GEBezierEval(const float p[4], int k, float *ab = nullptr, float *bc = nullptr) {
|
||||
const float a = GEBezierLerp(p[0], p[1], k);
|
||||
const float b = GEBezierLerp(p[1], p[2], k);
|
||||
const float c = GEBezierLerp(p[2], p[3], k);
|
||||
const float l = GEBezierLerp(a, b, k);
|
||||
const float r = GEBezierLerp(b, c, k);
|
||||
if (ab) {
|
||||
*ab = l;
|
||||
*bc = r;
|
||||
}
|
||||
return GEBezierLerp(l, r, k);
|
||||
}
|
||||
|
||||
static int GEBezierEvalColor(const int p[4], int k) {
|
||||
auto lerp = [k](int a, int b) { return a + (((b - a) * k) >> 8); };
|
||||
const int a = lerp(p[0], p[1]);
|
||||
const int b = lerp(p[1], p[2]);
|
||||
const int c = lerp(p[2], p[3]);
|
||||
return lerp(lerp(a, b), lerp(b, c));
|
||||
}
|
||||
|
||||
// Splines as the GE evaluates them (gpu/probe exp144-145, bit exact): de Boor at t = k/256 (k as for Bezier)
|
||||
// with the lerps above. Each blending factor (t - K[i]) / span comes from the nearer of its two knots, in 1/256:
|
||||
// floor(d / span) from the left one, 256 - floor(d / span) from the right. A segment's last point is the next
|
||||
// one's first (k = 0). An open end repeats its knot four times, a closed one keeps the unit spacing.
|
||||
struct GESplineParam {
|
||||
int seg;
|
||||
int alpha[6]; // level 1 for knots l-2, l-1, l, level 2 for l-1, l, level 3 for l (l = seg + 3)
|
||||
int k;
|
||||
};
|
||||
|
||||
static GESplineParam GESplineParamAt(int index, int tess, int numPatches, int type) {
|
||||
GESplineParam p;
|
||||
p.seg = index / tess;
|
||||
p.k = BezierParam256(index % tess, tess);
|
||||
if (p.seg == numPatches) {
|
||||
p.seg = numPatches - 1;
|
||||
p.k = 256;
|
||||
}
|
||||
auto knot = [&](int i) {
|
||||
if (i < 3 && (type & 1))
|
||||
return 0;
|
||||
if (i > numPatches + 3 && (type & 2))
|
||||
return numPatches;
|
||||
return i - 3;
|
||||
};
|
||||
const int l = p.seg + 3;
|
||||
const int is[6] = { l - 2, l - 1, l, l - 1, l, l };
|
||||
const int rs[6] = { 1, 1, 1, 2, 2, 3 };
|
||||
for (int n = 0; n < 6; ++n) {
|
||||
const int i = is[n];
|
||||
const int span = knot(i + 4 - rs[n]) - knot(i);
|
||||
const int dl = 256 * (p.seg - knot(i)) + p.k;
|
||||
const int dr = 256 * span - dl;
|
||||
p.alpha[n] = span == 0 ? 0 : (dl <= dr ? dl / span : 256 - dr / span);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
static float GESplineEval(const float d[4], const int a[6], float *ab = nullptr, float *bc = nullptr) {
|
||||
const float l0 = GEBezierLerp(d[0], d[1], a[0]);
|
||||
const float l1 = GEBezierLerp(d[1], d[2], a[1]);
|
||||
const float l2 = GEBezierLerp(d[2], d[3], a[2]);
|
||||
const float m0 = GEBezierLerp(l0, l1, a[3]);
|
||||
const float m1 = GEBezierLerp(l1, l2, a[4]);
|
||||
if (ab) {
|
||||
*ab = m0;
|
||||
*bc = m1;
|
||||
}
|
||||
return GEBezierLerp(m0, m1, a[5]);
|
||||
}
|
||||
|
||||
static int GESplineEvalColor(const int d[4], const int a[6]) {
|
||||
auto lerp = [](int x, int y, int k) { return x + (((y - x) * k) >> 8); };
|
||||
const int l0 = lerp(d[0], d[1], a[0]);
|
||||
const int l1 = lerp(d[1], d[2], a[1]);
|
||||
const int l2 = lerp(d[2], d[3], a[2]);
|
||||
return lerp(lerp(l0, l1, a[3]), lerp(l1, l2, a[4]), a[5]);
|
||||
}
|
||||
|
||||
// One control column of a patch evaluated at some v.
|
||||
struct GEBezierColumn {
|
||||
float pos[3];
|
||||
float ab[3], bc[3]; // for the normal
|
||||
float tex[2];
|
||||
int col[4]; // 15 bits
|
||||
};
|
||||
|
||||
class Bezier3DWeight {
|
||||
private:
|
||||
static void CalcWeights(float t, Weight &w) {
|
||||
@@ -334,13 +478,115 @@ void ControlPoints::Convert(const SimpleVertex *const *points, int size) {
|
||||
defcolor = points[0]->color_32;
|
||||
}
|
||||
|
||||
// The GE's spline: each control column at the vertex's v, then the row of those at its u (as for Bezier).
|
||||
template <bool sampleNrm, bool sampleCol, bool sampleTex, bool patchFacing>
|
||||
static void TessellateSplineGE(OutputBuffers &output, const SplineSurface &surface, const ControlPoints &points) {
|
||||
const int nu = surface.num_patches_u * surface.tess_u + 1;
|
||||
const int nv = surface.num_patches_v * surface.tess_v + 1;
|
||||
const int pointsU = surface.num_points_u;
|
||||
std::vector<GESplineParam> paramsU(nu);
|
||||
for (int i = 0; i < nu; ++i)
|
||||
paramsU[i] = GESplineParamAt(i, surface.tess_u, surface.num_patches_u, surface.type_u);
|
||||
std::vector<GEBezierColumn> columns(pointsU);
|
||||
|
||||
for (int iv = 0; iv < nv; ++iv) {
|
||||
const GESplineParam pv = GESplineParamAt(iv, surface.tess_v, surface.num_patches_v, surface.type_v);
|
||||
for (int c = 0; c < pointsU; ++c) {
|
||||
GEBezierColumn &column = columns[c];
|
||||
int idx[4];
|
||||
for (int r = 0; r < 4; ++r)
|
||||
idx[r] = (pv.seg + r) * pointsU + c;
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const float p[4] = { points.pos[idx[0]][j], points.pos[idx[1]][j], points.pos[idx[2]][j], points.pos[idx[3]][j] };
|
||||
column.pos[j] = GESplineEval(p, pv.alpha, &column.ab[j], &column.bc[j]);
|
||||
}
|
||||
if constexpr (sampleTex) {
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
const float p[4] = { points.tex[idx[0]][j], points.tex[idx[1]][j], points.tex[idx[2]][j], points.tex[idx[3]][j] };
|
||||
column.tex[j] = GESplineEval(p, pv.alpha);
|
||||
}
|
||||
}
|
||||
if constexpr (sampleCol) {
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
int p[4];
|
||||
for (int r = 0; r < 4; ++r)
|
||||
p[r] = ((int)(points.col[idx[r]][j] * 255.0f + 0.5f) << 7) | 0x7F;
|
||||
column.col[j] = GESplineEvalColor(p, pv.alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int iu = 0; iu < nu; ++iu) {
|
||||
const GESplineParam &pu = paramsU[iu];
|
||||
const GEBezierColumn *cols = &columns[pu.seg];
|
||||
SimpleVertex &vert = output.vertices[surface.GetIndex(iu, iv, 0, 0)];
|
||||
Vec3f tangentU, tangentV;
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const float row[4] = { cols[0].pos[j], cols[1].pos[j], cols[2].pos[j], cols[3].pos[j] };
|
||||
float ab, bc;
|
||||
vert.pos[j] = GESplineEval(row, pu.alpha, &ab, &bc);
|
||||
if constexpr (sampleNrm) {
|
||||
tangentU[j] = GEAdd(bc, -ab);
|
||||
const float abRow[4] = { cols[0].ab[j], cols[1].ab[j], cols[2].ab[j], cols[3].ab[j] };
|
||||
const float bcRow[4] = { cols[0].bc[j], cols[1].bc[j], cols[2].bc[j], cols[3].bc[j] };
|
||||
tangentV[j] = GEAdd(GESplineEval(bcRow, pu.alpha), -GESplineEval(abRow, pu.alpha));
|
||||
}
|
||||
}
|
||||
if constexpr (sampleCol) {
|
||||
u32 color = 0;
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const int row[4] = { cols[0].col[j], cols[1].col[j], cols[2].col[j], cols[3].col[j] };
|
||||
color |= (u32)(GESplineEvalColor(row, pu.alpha) >> 7) << (8 * j);
|
||||
}
|
||||
vert.color_32 = color;
|
||||
} else {
|
||||
vert.color_32 = points.defcolor;
|
||||
}
|
||||
if constexpr (sampleTex) {
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
const float row[4] = { cols[0].tex[j], cols[1].tex[j], cols[2].tex[j], cols[3].tex[j] };
|
||||
vert.uv[j] = GESplineEval(row, pu.alpha);
|
||||
}
|
||||
} else {
|
||||
vert.uv[0] = pu.seg + pu.k * (1.0f / 256.0f);
|
||||
vert.uv[1] = pv.seg + pv.k * (1.0f / 256.0f);
|
||||
}
|
||||
if constexpr (sampleNrm) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const int j1 = (j + 1) % 3, j2 = (j + 2) % 3;
|
||||
const GERowTerm terms[2] = { GEProduct(tangentU[j1], tangentV[j2]), GEProduct(-tangentU[j2], tangentV[j1]) };
|
||||
vert.nrm[j] = GERowSum(terms, 2);
|
||||
}
|
||||
if constexpr (patchFacing)
|
||||
vert.nrm *= -1.0f;
|
||||
} else {
|
||||
vert.nrm.SetZero();
|
||||
vert.nrm.z = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<class Surface>
|
||||
class SubdivisionSurface {
|
||||
public:
|
||||
template <bool sampleNrm, bool sampleCol, bool sampleTex, bool useSSE4, bool patchFacing>
|
||||
static void Tessellate(OutputBuffers &output, const Surface &surface, const ControlPoints &points, const Weight2D &weights) {
|
||||
if constexpr (std::is_same_v<Surface, SplineSurface>) {
|
||||
if (surface.geExact) {
|
||||
TessellateSplineGE<sampleNrm, sampleCol, sampleTex, patchFacing>(output, surface, points);
|
||||
surface.BuildIndex(output.indices, output.count);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const float inv_u = 1.0f / (float)surface.tess_u;
|
||||
const float inv_v = 1.0f / (float)surface.tess_v;
|
||||
bool exactBezier = false;
|
||||
if constexpr (std::is_same_v<Surface, BezierSurface>) {
|
||||
exactBezier = surface.geExact;
|
||||
}
|
||||
// Bezier: the GE's columns at each v step, for the current patch.
|
||||
std::vector<GEBezierColumn> columns(exactBezier ? (surface.tess_v + 1) * 4 : 0);
|
||||
|
||||
for (int patch_u = 0; patch_u < surface.num_patches_u; ++patch_u) {
|
||||
const int start_u = surface.GetTessStart(patch_u);
|
||||
@@ -355,18 +601,48 @@ public:
|
||||
Tessellator<Vec2f> tess_tex(points.tex, idx_v);
|
||||
Tessellator<Vec3f> tess_nrm(points.pos, idx_v);
|
||||
|
||||
if (exactBezier) {
|
||||
for (int tile_v = 0; tile_v <= surface.tess_v; ++tile_v) {
|
||||
const int kv = BezierParam256(tile_v, surface.tess_v);
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
GEBezierColumn &column = columns[tile_v * 4 + c];
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const float p[4] = { points.pos[idx_v[0] + c][j], points.pos[idx_v[1] + c][j], points.pos[idx_v[2] + c][j], points.pos[idx_v[3] + c][j] };
|
||||
column.pos[j] = GEBezierEval(p, kv, &column.ab[j], &column.bc[j]);
|
||||
}
|
||||
if constexpr (sampleTex) {
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
const float p[4] = { points.tex[idx_v[0] + c][j], points.tex[idx_v[1] + c][j], points.tex[idx_v[2] + c][j], points.tex[idx_v[3] + c][j] };
|
||||
column.tex[j] = GEBezierEval(p, kv);
|
||||
}
|
||||
}
|
||||
if constexpr (sampleCol) {
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
int p[4];
|
||||
for (int r = 0; r < 4; ++r) {
|
||||
p[r] = ((int)(points.col[idx_v[r] + c][j] * 255.0f + 0.5f) << 7) | 0x7F;
|
||||
}
|
||||
column.col[j] = GEBezierEvalColor(p, kv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int tile_u = start_u; tile_u <= surface.tess_u; ++tile_u) {
|
||||
const int index_u = surface.GetIndexU(patch_u, tile_u);
|
||||
const Weight &wu = weights.u[index_u];
|
||||
|
||||
// Pre-tessellate U lines
|
||||
tess_pos.SampleU(wu.basis);
|
||||
if constexpr (sampleCol)
|
||||
tess_col.SampleU(wu.basis);
|
||||
if constexpr (sampleTex)
|
||||
tess_tex.SampleU(wu.basis);
|
||||
if constexpr (sampleNrm)
|
||||
tess_nrm.SampleU(wu.deriv);
|
||||
// Pre-tessellate U lines (the exact path evaluates its own columns).
|
||||
if (!exactBezier) {
|
||||
tess_pos.SampleU(wu.basis);
|
||||
if constexpr (sampleCol)
|
||||
tess_col.SampleU(wu.basis);
|
||||
if constexpr (sampleTex)
|
||||
tess_tex.SampleU(wu.basis);
|
||||
if constexpr (sampleNrm)
|
||||
tess_nrm.SampleU(wu.deriv);
|
||||
}
|
||||
|
||||
for (int tile_v = start_v; tile_v <= surface.tess_v; ++tile_v) {
|
||||
const int index_v = surface.GetIndexV(patch_v, tile_v);
|
||||
@@ -375,20 +651,66 @@ public:
|
||||
SimpleVertex &vert = output.vertices[surface.GetIndex(index_u, index_v, patch_u, patch_v)];
|
||||
|
||||
// Tessellate
|
||||
vert.pos = tess_pos.SampleV(wv.basis);
|
||||
if constexpr (sampleCol) {
|
||||
vert.color_32 = tess_col.SampleV(wv.basis).ToRGBA();
|
||||
Vec3f tangentU, tangentV;
|
||||
if (exactBezier) {
|
||||
const int ku = BezierParam256(tile_u, surface.tess_u);
|
||||
const GEBezierColumn *cols = &columns[tile_v * 4];
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const float row[4] = { cols[0].pos[j], cols[1].pos[j], cols[2].pos[j], cols[3].pos[j] };
|
||||
float ab, bc;
|
||||
vert.pos[j] = GEBezierEval(row, ku, &ab, &bc);
|
||||
if constexpr (sampleNrm) {
|
||||
tangentU[j] = GEAdd(bc, -ab);
|
||||
const float abRow[4] = { cols[0].ab[j], cols[1].ab[j], cols[2].ab[j], cols[3].ab[j] };
|
||||
const float bcRow[4] = { cols[0].bc[j], cols[1].bc[j], cols[2].bc[j], cols[3].bc[j] };
|
||||
tangentV[j] = GEAdd(GEBezierEval(bcRow, ku), -GEBezierEval(abRow, ku));
|
||||
}
|
||||
}
|
||||
if constexpr (sampleCol) {
|
||||
u32 color = 0;
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const int row[4] = { cols[0].col[j], cols[1].col[j], cols[2].col[j], cols[3].col[j] };
|
||||
color |= (u32)(GEBezierEvalColor(row, ku) >> 7) << (8 * j);
|
||||
}
|
||||
vert.color_32 = color;
|
||||
} else {
|
||||
vert.color_32 = points.defcolor;
|
||||
}
|
||||
if constexpr (sampleTex) {
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
const float row[4] = { cols[0].tex[j], cols[1].tex[j], cols[2].tex[j], cols[3].tex[j] };
|
||||
vert.uv[j] = GEBezierEval(row, ku);
|
||||
}
|
||||
} else {
|
||||
// Generated: the parameter itself (exp140).
|
||||
vert.uv[0] = patch_u + ku * (1.0f / 256.0f);
|
||||
vert.uv[1] = patch_v + BezierParam256(tile_v, surface.tess_v) * (1.0f / 256.0f);
|
||||
}
|
||||
} else {
|
||||
vert.color_32 = points.defcolor;
|
||||
vert.pos = tess_pos.SampleV(wv.basis);
|
||||
if constexpr (sampleCol) {
|
||||
vert.color_32 = tess_col.SampleV(wv.basis).ToRGBA();
|
||||
} else {
|
||||
vert.color_32 = points.defcolor;
|
||||
}
|
||||
if constexpr (sampleTex) {
|
||||
tess_tex.SampleV(wv.basis).Write(vert.uv);
|
||||
} else {
|
||||
// Generate texcoord
|
||||
vert.uv[0] = patch_u + tile_u * inv_u;
|
||||
vert.uv[1] = patch_v + tile_v * inv_v;
|
||||
}
|
||||
}
|
||||
if constexpr (sampleTex) {
|
||||
tess_tex.SampleV(wv.basis).Write(vert.uv);
|
||||
} else {
|
||||
// Generate texcoord
|
||||
vert.uv[0] = patch_u + tile_u * inv_u;
|
||||
vert.uv[1] = patch_v + tile_v * inv_v;
|
||||
}
|
||||
if constexpr (sampleNrm) {
|
||||
if (sampleNrm && exactBezier) {
|
||||
// Unnormalized: lighting normalizes it. Vertex normals are unused (exp142).
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const int j1 = (j + 1) % 3, j2 = (j + 2) % 3;
|
||||
const GERowTerm terms[2] = { GEProduct(tangentU[j1], tangentV[j2]), GEProduct(-tangentU[j2], tangentV[j1]) };
|
||||
vert.nrm[j] = GERowSum(terms, 2);
|
||||
}
|
||||
if constexpr (patchFacing)
|
||||
vert.nrm *= -1.0f;
|
||||
} else if constexpr (sampleNrm) {
|
||||
const Vec3f derivU = tess_nrm.SampleV(wv.basis);
|
||||
const Vec3f derivV = tess_pos.SampleV(wv.deriv);
|
||||
|
||||
@@ -434,7 +756,8 @@ public:
|
||||
|
||||
static void Tessellate(OutputBuffers &output, const Surface &surface, const ControlPoints &points, const Weight2D &weights, u32 origVertType) {
|
||||
const bool params[] = {
|
||||
(origVertType & GE_VTYPE_NRM_MASK) != 0 || gstate.isLightingEnabled(),
|
||||
// Shade mapping uses the normal even with lighting off (gpu/probe exp143).
|
||||
(origVertType & GE_VTYPE_NRM_MASK) != 0 || gstate.isLightingEnabled() || gstate.getUVGenMode() == GE_TEXMAP_ENVIRONMENT_MAP,
|
||||
(origVertType & GE_VTYPE_COL_MASK) != 0,
|
||||
(origVertType & GE_VTYPE_TC_MASK) != 0,
|
||||
cpu_info.bSSE4_1,
|
||||
|
||||
@@ -83,6 +83,8 @@ struct BezierSurface : public SurfaceInfo {
|
||||
using WeightType = Bezier3DWeight;
|
||||
|
||||
int num_verts_per_patch;
|
||||
// Evaluate as the GE does, bit exact but slower (the software renderer).
|
||||
bool geExact = false;
|
||||
|
||||
void Init(int maxVertices) {
|
||||
SurfaceInfo::BaseInit();
|
||||
@@ -120,6 +122,8 @@ struct SplineSurface : public SurfaceInfo {
|
||||
using WeightType = Spline3DWeight;
|
||||
|
||||
int num_vertices_u;
|
||||
// Evaluate as the GE does, bit exact but slower (the software renderer).
|
||||
bool geExact = false;
|
||||
|
||||
void Init(int maxVertices) {
|
||||
SurfaceInfo::BaseInit();
|
||||
|
||||
@@ -160,3 +160,19 @@ struct SimpleVertex {
|
||||
Vec3Packedf nrm;
|
||||
Vec3Packedf pos;
|
||||
};
|
||||
|
||||
// The GE converts the fog factor to 8 bits per vertex, min(floor(256 * f), 255), and interpolates that
|
||||
// linearly in screen space. NaN and inf go by sign, like other values (measured with gpu/probe).
|
||||
inline int GEFogFactor(float f) {
|
||||
uint32_t bits;
|
||||
memcpy(&bits, &f, sizeof(bits));
|
||||
const uint32_t exp = bits >> 23;
|
||||
if ((bits & 0x80000000) != 0 || exp <= 126 - 8) {
|
||||
return 0;
|
||||
}
|
||||
if (exp > 126) {
|
||||
return 255;
|
||||
}
|
||||
const uint32_t mantissa = (bits & 0x007FFFFF) | 0x00800000;
|
||||
return mantissa >> (16 + 126 - exp);
|
||||
}
|
||||
+247
-4
@@ -16,9 +16,11 @@
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
#include <condition_variable>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
@@ -40,6 +42,7 @@
|
||||
#include "Core/HLE/sceDisplay.h"
|
||||
#include "Core/HLE/sceKernelMemory.h"
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/MemMapHelpers.h"
|
||||
#include "Core/MIPS/MIPS.h"
|
||||
#include "Core/MIPS/MIPSCodeUtils.h"
|
||||
#include "Core/System.h"
|
||||
@@ -100,6 +103,11 @@ static Operation g_opToExec;
|
||||
static u32 g_retVal;
|
||||
static bool g_opDone = true;
|
||||
static bool g_cancelled = false;
|
||||
static int g_drawLimit = 0;
|
||||
|
||||
void SetReplayDrawLimit(int lastPrim) {
|
||||
g_drawLimit = lastPrim;
|
||||
}
|
||||
|
||||
// Runs on operation thread
|
||||
u32 ExecuteOnMain(Operation opToExec) {
|
||||
@@ -389,7 +397,39 @@ private:
|
||||
std::vector<u32> execListQueue;
|
||||
u16 lastBufw_[8]{};
|
||||
u32 lastTex_[8]{};
|
||||
int prims_ = 0;
|
||||
u32 lastBase_ = 0;
|
||||
// The framebuffers drawn to so far by VRAM offset, with the areas the draws could reach, and the
|
||||
// current one's registers.
|
||||
struct DrawnRect {
|
||||
int x1, y1, x2, y2;
|
||||
bool Contains(int x, int y) const {
|
||||
return x >= x1 && x <= x2 && y >= y1 && y <= y2;
|
||||
}
|
||||
};
|
||||
struct DrawnTarget {
|
||||
u32 strideBytes;
|
||||
u32 bpp;
|
||||
std::vector<DrawnRect> rects;
|
||||
};
|
||||
std::map<u32, DrawnTarget> drawnTargets_;
|
||||
u32 fbPtr_ = 0;
|
||||
u32 fbWidth_ = 0;
|
||||
u32 fbFormat_ = 0;
|
||||
u32 region2_ = 0;
|
||||
u32 scissor2_ = 0;
|
||||
u32 vertType_ = 0;
|
||||
u32 zbPtr_ = 0;
|
||||
u32 zbWidth_ = 0;
|
||||
bool zTest_ = false;
|
||||
bool zWriteDisable_ = false;
|
||||
u32 clearMode_ = 0;
|
||||
// The vertices recorded for the next draw.
|
||||
u32 lastVertsPtr_ = 0;
|
||||
u32 lastVertsSize_ = 0;
|
||||
|
||||
void MarkDrawn(u32 prim);
|
||||
void CopyAroundDrawn(u32 addr, const u8 *data, u32 size);
|
||||
|
||||
const std::vector<u8> &pushbuf_;
|
||||
const std::vector<Command> &commands_;
|
||||
@@ -451,7 +491,14 @@ void DumpExecute::Registers(u32 ptr, u32 sz) {
|
||||
Memory::WriteUnchecked_U32((GE_CMD_JUMP << 24) | (execListBuf & 0x00FFFFFF), execListPos + 4);
|
||||
|
||||
execListPos = execListBuf;
|
||||
lastBase_ = execListBuf & 0xFF000000;
|
||||
// The queued vertex, index and texture addresses were made for the previous base, which the jump
|
||||
// just replaced (Tiger Woods ULUS10420 then read draw 21026's indices from 16 MB away).
|
||||
if (lastBase_ != 0xFFFFFFFF && lastBase_ != (execListBuf & 0xFF000000)) {
|
||||
Memory::WriteUnchecked_U32((GE_CMD_BASE << 24) | ((lastBase_ >> 8) & 0x00FF0000), execListPos);
|
||||
execListPos += 4;
|
||||
} else {
|
||||
lastBase_ = execListBuf & 0xFF000000;
|
||||
}
|
||||
|
||||
// Don't continue until we've stalled.
|
||||
// TODO: Is this really needed? It seems fine without it.
|
||||
@@ -487,6 +534,39 @@ void DumpExecute::Registers(u32 ptr, u32 sz) {
|
||||
lastBufw_[level] = bufw;
|
||||
}
|
||||
|
||||
if (cmd == GE_CMD_FRAMEBUFPTR)
|
||||
fbPtr_ = ops[i] & 0x00FFFFFF;
|
||||
if (cmd == GE_CMD_FRAMEBUFWIDTH)
|
||||
fbWidth_ = ops[i] & 0x00FFFFFF;
|
||||
if (cmd == GE_CMD_FRAMEBUFPIXFORMAT)
|
||||
fbFormat_ = ops[i] & 3;
|
||||
if (cmd == GE_CMD_REGION2)
|
||||
region2_ = ops[i] & 0x000FFFFF;
|
||||
if (cmd == GE_CMD_SCISSOR2)
|
||||
scissor2_ = ops[i] & 0x000FFFFF;
|
||||
if (cmd == GE_CMD_VERTEXTYPE)
|
||||
vertType_ = ops[i] & 0x00FFFFFF;
|
||||
if (cmd == GE_CMD_ZBUFPTR)
|
||||
zbPtr_ = ops[i] & 0x00FFFFFF;
|
||||
if (cmd == GE_CMD_ZBUFWIDTH)
|
||||
zbWidth_ = ops[i] & 0x00FFFFFF;
|
||||
if (cmd == GE_CMD_ZTESTENABLE)
|
||||
zTest_ = (ops[i] & 1) != 0;
|
||||
if (cmd == GE_CMD_ZWRITEDISABLE)
|
||||
zWriteDisable_ = (ops[i] & 1) != 0;
|
||||
if (cmd == GE_CMD_CLEARMODE)
|
||||
clearMode_ = ops[i] & 0xFFFF;
|
||||
if (cmd == GE_CMD_PRIM || cmd == GE_CMD_BEZIER || cmd == GE_CMD_SPLINE) {
|
||||
MarkDrawn(cmd == GE_CMD_PRIM ? ops[i] & 0x00FFFFFF : 0);
|
||||
prims_++;
|
||||
if (g_drawLimit > 0 && prims_ > g_drawLimit)
|
||||
ops[i] = GE_CMD_NOP << 24;
|
||||
}
|
||||
// Past the limit, keep the render target too, for --screenshot-render-target.
|
||||
if (g_drawLimit > 0 && prims_ >= g_drawLimit && (cmd == GE_CMD_FRAMEBUFPTR || cmd == GE_CMD_FRAMEBUFWIDTH || cmd == GE_CMD_FRAMEBUFPIXFORMAT)) {
|
||||
ops[i] = GE_CMD_NOP << 24;
|
||||
}
|
||||
|
||||
// Since we're here anyway, also NOP out texture addresses.
|
||||
// This makes Step Tex not hit phantom textures, but we rely on it for lastTex_[].
|
||||
if (cmd >= GE_CMD_TEXADDR0 && cmd <= GE_CMD_TEXADDR7) {
|
||||
@@ -521,6 +601,17 @@ void DumpExecute::SubmitListEnd() {
|
||||
void DumpExecute::Init(u32 ptr, u32 sz) {
|
||||
gstate.Restore((u32_le *)(pushbuf_.data() + ptr));
|
||||
ExecuteOnMain(Operation{ OpType::ReapplyGfxState });
|
||||
fbPtr_ = gstate.fbptr & 0x00FFFFFF;
|
||||
fbWidth_ = gstate.fbwidth & 0x00FFFFFF;
|
||||
fbFormat_ = gstate.framebufpixformat & 3;
|
||||
region2_ = gstate.region2 & 0x000FFFFF;
|
||||
scissor2_ = gstate.scissor2 & 0x000FFFFF;
|
||||
vertType_ = gstate.vertType & 0x00FFFFFF;
|
||||
zbPtr_ = gstate.zbptr & 0x00FFFFFF;
|
||||
zbWidth_ = gstate.zbwidth & 0x00FFFFFF;
|
||||
zTest_ = (gstate.zTestEnable & 1) != 0;
|
||||
zWriteDisable_ = (gstate.zmsk & 1) != 0;
|
||||
clearMode_ = gstate.clearmode & 0xFFFF;
|
||||
|
||||
// Hm, why are we doing this here? Should it be before?
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
@@ -530,7 +621,107 @@ void DumpExecute::Init(u32 ptr, u32 sz) {
|
||||
lastBase_ = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
// The drawing-space bounds of a through mode draw's vertices, or false if they aren't simple to read
|
||||
// (indexed, morphed, or not through mode).
|
||||
static bool ThroughModeBounds(u32 vtype, const u8 *data, u32 size, u32 count, int &x1, int &y1, int &x2, int &y2) {
|
||||
if ((vtype & GE_VTYPE_THROUGH) == 0 || (vtype & GE_VTYPE_IDX_MASK) != 0 || (vtype & GE_VTYPE_MORPHCOUNT_MASK) != 0 || count == 0)
|
||||
return false;
|
||||
|
||||
// Each component is aligned to its own size, and the vertex to the largest.
|
||||
static const u8 compSizes[4] = { 0, 1, 2, 4 };
|
||||
static const u8 colorSizes[8] = { 0, 0, 0, 0, 2, 2, 2, 4 };
|
||||
u32 offset = 0, align = 1;
|
||||
auto add = [&](u32 elemSize, u32 n) {
|
||||
if (elemSize == 0)
|
||||
return;
|
||||
offset = (offset + elemSize - 1) & ~(elemSize - 1);
|
||||
offset += elemSize * n;
|
||||
align = std::max(align, elemSize);
|
||||
};
|
||||
add(compSizes[(vtype & GE_VTYPE_WEIGHT_MASK) >> GE_VTYPE_WEIGHT_SHIFT], ((vtype & GE_VTYPE_WEIGHTCOUNT_MASK) >> GE_VTYPE_WEIGHTCOUNT_SHIFT) + 1);
|
||||
add(compSizes[(vtype & GE_VTYPE_TC_MASK) >> GE_VTYPE_TC_SHIFT], 2);
|
||||
add(colorSizes[(vtype & GE_VTYPE_COL_MASK) >> GE_VTYPE_COL_SHIFT], 1);
|
||||
add(compSizes[(vtype & GE_VTYPE_NRM_MASK) >> GE_VTYPE_NRM_SHIFT], 3);
|
||||
const u32 posSize = compSizes[(vtype & GE_VTYPE_POS_MASK) >> GE_VTYPE_POS_SHIFT];
|
||||
if (posSize == 0)
|
||||
return false;
|
||||
offset = (offset + posSize - 1) & ~(posSize - 1);
|
||||
const u32 posOffset = offset;
|
||||
add(posSize, 3);
|
||||
const u32 stride = (offset + align - 1) & ~(align - 1);
|
||||
if (stride * count > size)
|
||||
return false;
|
||||
|
||||
x1 = y1 = INT_MAX;
|
||||
x2 = y2 = INT_MIN;
|
||||
for (u32 i = 0; i < count; ++i) {
|
||||
const u8 *p = data + i * stride + posOffset;
|
||||
int x, y;
|
||||
if (posSize == 4) {
|
||||
float fx, fy;
|
||||
memcpy(&fx, p, 4);
|
||||
memcpy(&fy, p + 4, 4);
|
||||
if (!(fx >= -4096.0f && fx <= 4096.0f && fy >= -4096.0f && fy <= 4096.0f))
|
||||
return false;
|
||||
x = (int)fx;
|
||||
y = (int)fy;
|
||||
} else if (posSize == 2) {
|
||||
x = (s16)(p[0] | (p[1] << 8));
|
||||
y = (s16)(p[2] | (p[3] << 8));
|
||||
} else {
|
||||
x = (s8)p[0];
|
||||
y = (s8)p[1];
|
||||
}
|
||||
// Truncation can be a pixel short at either end; the bounds only need to be generous.
|
||||
x1 = std::min(x1, x - 1);
|
||||
y1 = std::min(y1, y - 1);
|
||||
x2 = std::max(x2, x + 1);
|
||||
y2 = std::max(y2, y + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void DumpExecute::MarkDrawn(u32 prim) {
|
||||
const u32 start = ((fbPtr_ & 0xFFFFF0) | ((fbWidth_ & 0xFF0000) << 8)) & 0x001FFFFF;
|
||||
const u32 bpp = fbFormat_ == GE_FORMAT_8888 ? 4 : 2;
|
||||
int x1 = 0, y1 = 0;
|
||||
int x2 = std::min(region2_ & 0x3FF, scissor2_ & 0x3FF);
|
||||
int y2 = std::min((region2_ >> 10) & 0x3FF, (scissor2_ >> 10) & 0x3FF);
|
||||
// A 2D draw (a sprite into a small buffer whose nominal area overlaps textures, Burnout) can say where
|
||||
// it is exactly.
|
||||
int vx1, vy1, vx2, vy2;
|
||||
if (prim != 0 && lastVertsSize_ != 0 && ThroughModeBounds(vertType_, pushbuf_.data() + lastVertsPtr_, lastVertsSize_, prim & 0xFFFF, vx1, vy1, vx2, vy2)) {
|
||||
x1 = std::max(x1, vx1);
|
||||
y1 = std::max(y1, vy1);
|
||||
x2 = std::min(x2, vx2);
|
||||
y2 = std::min(y2, vy2);
|
||||
if (x1 > x2 || y1 > y2)
|
||||
return;
|
||||
}
|
||||
|
||||
auto add = [&](u32 base, u32 strideBytes, u32 bytesPerPixel, const DrawnRect &rect) {
|
||||
DrawnTarget &t = drawnTargets_[base];
|
||||
t.strideBytes = strideBytes;
|
||||
t.bpp = bytesPerPixel;
|
||||
for (const DrawnRect &r : t.rects) {
|
||||
if (r.Contains(rect.x1, rect.y1) && r.Contains(rect.x2, rect.y2))
|
||||
return;
|
||||
}
|
||||
t.rects.push_back(rect);
|
||||
};
|
||||
add(start, (fbWidth_ & 0x07FC) * bpp, bpp, DrawnRect{ x1, y1, x2, y2 });
|
||||
const bool writesDepth = (clearMode_ & 1) ? (clearMode_ & 0x400) != 0 : (zTest_ && !zWriteDisable_);
|
||||
const int zbw = zbWidth_ & 0x07FC;
|
||||
if (writesDepth && zbw != 0) {
|
||||
// The PSP stores depth swizzled, so what lands in a drawn area is spread over whole rows in bands
|
||||
// (SOCOM's frame texture copy ran into its depth buffer's columns 480-511).
|
||||
add(zbPtr_ & 0x001FFFF0, zbw * 2, 2, DrawnRect{ 0, y1 & ~7, zbw - 1, y2 | 7 });
|
||||
}
|
||||
}
|
||||
|
||||
void DumpExecute::Vertices(u32 ptr, u32 sz) {
|
||||
lastVertsPtr_ = ptr;
|
||||
lastVertsSize_ = sz;
|
||||
u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this));
|
||||
if (psp == 0) {
|
||||
ERROR_LOG(Log::GeDebugger, "Unable to allocate for vertices");
|
||||
@@ -619,8 +810,12 @@ void DumpExecute::Memset(u32 ptr, u32 sz) {
|
||||
|
||||
if (Memory::IsVRAMAddress(data->dest)) {
|
||||
SyncStall();
|
||||
// Draws the software renderer still has queued come before the write.
|
||||
gpu->Flush();
|
||||
// TODO: should probably do this as an operation.
|
||||
gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz);
|
||||
// The software renderer leaves the memset to the caller, like sceKernelMemset does.
|
||||
if (!gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz))
|
||||
Memory::Memset(data->dest, (u8)data->value, data->sz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +827,7 @@ void DumpExecute::Memcpy(u32 ptr, u32 sz) {
|
||||
PROFILE_THIS_SCOPE("ReplayMemcpy");
|
||||
if (Memory::IsVRAMAddress(execMemcpyDest)) {
|
||||
SyncStall();
|
||||
gpu->Flush();
|
||||
Memory::MemcpyUnchecked(execMemcpyDest, pushbuf_.data() + ptr, sz);
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, execMemcpyDest, sz, "ReplayMemcpy");
|
||||
gpu->PerformWriteColorFromMemory(execMemcpyDest, sz);
|
||||
@@ -682,12 +878,59 @@ void DumpExecute::Framebuf(int level, u32 ptr, u32 sz) {
|
||||
// TODO: Could use drawnVRAM flag, but it can be wrong.
|
||||
// Could potentially always skip if !isTarget, but playing it safe for offset texture behavior.
|
||||
if (Memory::IsValidRange(framebuf->addr, pspSize) && !unchangedVRAM && (!isTarget || !g_Config.bSoftwareRendering)) {
|
||||
// After the draws before it, which the software renderer may still have queued.
|
||||
SyncStall();
|
||||
gpu->Flush();
|
||||
// Intentionally don't trigger an upload here.
|
||||
Memory::MemcpyUnchecked(framebuf->addr, pushbuf_.data() + ptr + headerSize, pspSize);
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, framebuf->addr, pspSize, "ReplayTex");
|
||||
CopyAroundDrawn(framebuf->addr, pushbuf_.data() + ptr + headerSize, pspSize);
|
||||
}
|
||||
}
|
||||
|
||||
// The software renderer has the real contents of what this replay drew, while the dump's copy can be stale
|
||||
// (a hardware backend that didn't read it back, GTA LCS). So a copy leaves out the areas the replay's draws
|
||||
// could reach: a texture inside one (Princess Maker 5 textures from the right half of its 1024 wide
|
||||
// framebuffer), or one whose recorded size runs over a buffer drawn next to it (Rainbow Six copies its
|
||||
// 512x512 frame texture before each strip it draws into the display buffer inside that range). A texture
|
||||
// beside the drawn area still comes through (Burnout keeps one in columns 480-511).
|
||||
void DumpExecute::CopyAroundDrawn(u32 addr, const u8 *data, u32 size) {
|
||||
// A texture recorded at its full size can run past the end of VRAM, into the depth swizzle mirror
|
||||
// (Princess Maker 5's 1024x1024 one at 0x04000400). That isn't the game's data.
|
||||
if (Memory::IsVRAMAddress(addr))
|
||||
size = std::min(size, 0x00200000 - (addr & 0x001FFFFF));
|
||||
std::vector<std::pair<int64_t, int64_t>> skip;
|
||||
const int64_t start = addr & 0x001FFFFF, end = start + size;
|
||||
if (g_Config.bSoftwareRendering) {
|
||||
for (const auto &it : drawnTargets_) {
|
||||
const DrawnTarget &t = it.second;
|
||||
if (t.strideBytes == 0)
|
||||
continue;
|
||||
for (const DrawnRect &r : t.rects) {
|
||||
for (int y = std::max(r.y1, 0); y <= r.y2; ++y) {
|
||||
const int64_t row = (int64_t)it.first + (int64_t)y * t.strideBytes;
|
||||
const int64_t a = row + (int64_t)std::max(r.x1, 0) * t.bpp;
|
||||
const int64_t b = row + (int64_t)(r.x2 + 1) * t.bpp;
|
||||
if (b > start && a < end)
|
||||
skip.emplace_back(std::max(a, start), std::min(b, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(skip.begin(), skip.end());
|
||||
}
|
||||
|
||||
int64_t pos = start;
|
||||
auto copyTo = [&](int64_t until) {
|
||||
if (until > pos) {
|
||||
Memory::MemcpyUnchecked(addr + (u32)(pos - start), data + (pos - start), (u32)(until - pos));
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, addr + (u32)(pos - start), (u32)(until - pos), "ReplayTex");
|
||||
}
|
||||
};
|
||||
for (const auto &s : skip) {
|
||||
copyTo(s.first);
|
||||
pos = std::max(pos, s.second);
|
||||
}
|
||||
copyTo(end);
|
||||
}
|
||||
|
||||
void DumpExecute::Display(u32 ptr, u32 sz, bool allowFlip) {
|
||||
struct DisplayBufData {
|
||||
PSPPointer<u8> topaddr;
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
@@ -29,6 +31,9 @@ enum class ReplayResult {
|
||||
};
|
||||
|
||||
void WriteRunDumpCode(u32 addr);
|
||||
// Only draws primitives up to this one (1-based, counting PRIM, BEZIER and SPLINE), like the PSP side's
|
||||
// pspautotests/utils/ppdmp-playback --end. 0 draws everything.
|
||||
void SetReplayDrawLimit(int lastPrim);
|
||||
ReplayResult RunMountedReplay(const std::string &filename);
|
||||
|
||||
// Will also cancel a currently running replay.
|
||||
|
||||
+20
-26
@@ -48,13 +48,21 @@
|
||||
|
||||
namespace GPURecord {
|
||||
|
||||
// Appends data (or zeroes, for a null p) at a 4-byte aligned offset, zero padded: players read register blocks and headers as
|
||||
// words, which a PSP can't do unaligned.
|
||||
static u32 AppendToPushbuf(std::vector<u8> &pushbuf, const void *p, u32 sz) {
|
||||
const u32 ptr = ((u32)pushbuf.size() + 3) & ~3;
|
||||
pushbuf.resize(ptr + sz, 0);
|
||||
if (p && sz)
|
||||
memcpy(pushbuf.data() + ptr, p, sz);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void Recorder::FlushRegisters() {
|
||||
if (!lastRegisters.empty()) {
|
||||
Command last{ CommandType::REGISTERS };
|
||||
last.ptr = (u32)pushbuf.size();
|
||||
last.sz = (u32)(lastRegisters.size() * sizeof(u32));
|
||||
pushbuf.resize(pushbuf.size() + last.sz);
|
||||
memcpy(pushbuf.data() + last.ptr, lastRegisters.data(), last.sz);
|
||||
last.ptr = AppendToPushbuf(pushbuf, lastRegisters.data(), last.sz);
|
||||
lastRegisters.clear();
|
||||
|
||||
commands.push_back(last);
|
||||
@@ -141,9 +149,8 @@ bool Recorder::BeginRecording() {
|
||||
flipLastAction = gpuStats.totals.numFlips;
|
||||
flipFinishAt = -1;
|
||||
|
||||
u32 ptr = (u32)pushbuf.size();
|
||||
u32 sz = 512 * 4;
|
||||
pushbuf.resize(pushbuf.size() + sz);
|
||||
u32 ptr = AppendToPushbuf(pushbuf, nullptr, sz);
|
||||
gstate.Save((u32_le *)(pushbuf.data() + ptr));
|
||||
commands.push_back({ CommandType::INIT, sz, ptr });
|
||||
lastVRAM.resize(2 * 1024 * 1024);
|
||||
@@ -153,9 +160,7 @@ bool Recorder::BeginRecording() {
|
||||
if (gpu->GetCurrentClut(clut)) {
|
||||
sz = clut.GetStride() * clut.PixelSize();
|
||||
_assert_msg_(sz == 1024, "CLUT should be 1024 bytes");
|
||||
ptr = (u32)pushbuf.size();
|
||||
pushbuf.resize(pushbuf.size() + sz);
|
||||
memcpy(pushbuf.data() + ptr, clut.GetData(), sz);
|
||||
ptr = AppendToPushbuf(pushbuf, clut.GetData(), sz);
|
||||
commands.push_back({ CommandType::CLUT, sz, ptr });
|
||||
}
|
||||
|
||||
@@ -283,6 +288,7 @@ Command Recorder::EmitCommandWithRAM(CommandType t, const void *p, u32 sz, u32 a
|
||||
FlushRegisters();
|
||||
|
||||
Command cmd{ t, sz, 0 };
|
||||
align = std::max(align, 4U);
|
||||
|
||||
if (sz) {
|
||||
// If at all possible, try to find it already in the buffer.
|
||||
@@ -550,9 +556,7 @@ void Recorder::EmitClut(u32 op) {
|
||||
ClutAddrData data{ addr, flags };
|
||||
|
||||
FlushRegisters();
|
||||
Command cmd{ CommandType::CLUTADDR, sizeof(data), (u32)pushbuf.size() };
|
||||
pushbuf.resize(pushbuf.size() + sizeof(data));
|
||||
memcpy(pushbuf.data() + cmd.ptr, &data, sizeof(data));
|
||||
Command cmd{ CommandType::CLUTADDR, sizeof(data), AppendToPushbuf(pushbuf, &data, sizeof(data)) };
|
||||
commands.push_back(cmd);
|
||||
|
||||
if ((flags & 2) == 0)
|
||||
@@ -635,9 +639,7 @@ void Recorder::CheckEdramTrans() {
|
||||
lastEdramTrans = value;
|
||||
|
||||
FlushRegisters();
|
||||
Command cmd{ CommandType::EDRAMTRANS, sizeof(value), (u32)pushbuf.size() };
|
||||
pushbuf.resize(pushbuf.size() + sizeof(value));
|
||||
memcpy(pushbuf.data() + cmd.ptr, &value, sizeof(value));
|
||||
Command cmd{ CommandType::EDRAMTRANS, sizeof(value), AppendToPushbuf(pushbuf, &value, sizeof(value)) };
|
||||
commands.push_back(cmd);
|
||||
}
|
||||
|
||||
@@ -708,9 +710,7 @@ void Recorder::NotifyMemcpy(u32 dest, u32 src, u32 sz) {
|
||||
CheckEdramTrans();
|
||||
if (Memory::IsVRAMAddress(dest)) {
|
||||
FlushRegisters();
|
||||
Command cmd{ CommandType::MEMCPYDEST, sizeof(dest), (u32)pushbuf.size() };
|
||||
pushbuf.resize(pushbuf.size() + sizeof(dest));
|
||||
memcpy(pushbuf.data() + cmd.ptr, &dest, sizeof(dest));
|
||||
Command cmd{ CommandType::MEMCPYDEST, sizeof(dest), AppendToPushbuf(pushbuf, &dest, sizeof(dest)) };
|
||||
commands.push_back(cmd);
|
||||
|
||||
sz = Memory::ClampValidSizeAt(dest, sz);
|
||||
@@ -739,9 +739,7 @@ void Recorder::NotifyMemset(u32 dest, int v, u32 sz) {
|
||||
MemsetCommand data{ dest, v, sz };
|
||||
|
||||
FlushRegisters();
|
||||
Command cmd{ CommandType::MEMSET, sizeof(data), (u32)pushbuf.size() };
|
||||
pushbuf.resize(pushbuf.size() + sizeof(data));
|
||||
memcpy(pushbuf.data() + cmd.ptr, &data, sizeof(data));
|
||||
Command cmd{ CommandType::MEMSET, sizeof(data), AppendToPushbuf(pushbuf, &data, sizeof(data)) };
|
||||
commands.push_back(cmd);
|
||||
ClearLastVRAM(dest, v, sz);
|
||||
DirtyVRAM(dest, sz, DirtyVRAMFlag::CLEAN);
|
||||
@@ -794,10 +792,8 @@ void Recorder::NotifyDisplay(u32 framebuf, int stride, int fmt) {
|
||||
DisplayBufData disp{ { framebuf }, stride, fmt };
|
||||
|
||||
FlushRegisters();
|
||||
u32 ptr = (u32)pushbuf.size();
|
||||
u32 sz = (u32)sizeof(disp);
|
||||
pushbuf.resize(pushbuf.size() + sz);
|
||||
memcpy(pushbuf.data() + ptr, &disp, sz);
|
||||
u32 ptr = AppendToPushbuf(pushbuf, &disp, sz);
|
||||
|
||||
commands.push_back({ CommandType::DISPLAY, sz, ptr });
|
||||
|
||||
@@ -824,10 +820,8 @@ void Recorder::NotifyBeginFrame() {
|
||||
__DisplayGetFramebuf(&disp.topaddr, &disp.linesize, &disp.pixelFormat, 0);
|
||||
|
||||
FlushRegisters();
|
||||
u32 ptr = (u32)pushbuf.size();
|
||||
u32 sz = (u32)sizeof(disp);
|
||||
pushbuf.resize(pushbuf.size() + sz);
|
||||
memcpy(pushbuf.data() + ptr, &disp, sz);
|
||||
u32 ptr = AppendToPushbuf(pushbuf, &disp, sz);
|
||||
|
||||
commands.push_back({ CommandType::DISPLAY, sz, ptr });
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
<ClInclude Include="Software\Sampler.h" />
|
||||
<ClInclude Include="Software\SoftGpu.h" />
|
||||
<ClInclude Include="Software\TransformUnit.h" />
|
||||
<ClInclude Include="Software\GEMath.h" />
|
||||
<ClInclude Include="Common\TextureDecoder.h" />
|
||||
<ClInclude Include="Vulkan\DebugVisVulkan.h" />
|
||||
<ClInclude Include="Vulkan\DrawEngineVulkan.h" />
|
||||
@@ -473,6 +474,7 @@
|
||||
<ClCompile Include="Software\SamplerX86.cpp" />
|
||||
<ClCompile Include="Software\SoftGpu.cpp" />
|
||||
<ClCompile Include="Software\TransformUnit.cpp" />
|
||||
<ClCompile Include="Software\GEMath.cpp" />
|
||||
<ClCompile Include="Common\TextureDecoder.cpp" />
|
||||
<ClCompile Include="Vulkan\DebugVisVulkan.cpp" />
|
||||
<ClCompile Include="Vulkan\DrawEngineVulkan.cpp" />
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
<ClInclude Include="Software\TransformUnit.h">
|
||||
<Filter>Software</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Software\GEMath.h">
|
||||
<Filter>Software</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Common\VertexDecoderCommon.h">
|
||||
<Filter>Common</Filter>
|
||||
</ClInclude>
|
||||
@@ -296,6 +299,9 @@
|
||||
<ClCompile Include="Software\TransformUnit.cpp">
|
||||
<Filter>Software</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Software\GEMath.cpp">
|
||||
<Filter>Software</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Common\VertexDecoderCommon.cpp">
|
||||
<Filter>Common</Filter>
|
||||
</ClCompile>
|
||||
|
||||
+11
-6
@@ -1377,15 +1377,20 @@ __forceinline unsigned int Vec4<float>::ToRGBA() const
|
||||
__m128i c = _mm_cvtps_epi32(_mm_mul_ps(SAFE_M128(vec), _mm_set_ps1(255.0f)));
|
||||
__m128i c16 = _mm_packs_epi32(c, c);
|
||||
return _mm_cvtsi128_si32(_mm_packus_epi16(c16, c16));
|
||||
#elif PPSSPP_ARCH(ARM64_NEON)
|
||||
// Round to nearest, like SSE.
|
||||
uint16x4_t c16 = vqmovun_s32(vcvtnq_s32_f32(vmulq_f32(vec, vdupq_n_f32(255.0f))));
|
||||
uint8x8_t c8 = vqmovn_u16(vcombine_u16(c16, c16));
|
||||
return vget_lane_u32(vreinterpret_u32_u8(c8), 0);
|
||||
#elif PPSSPP_ARCH(ARM_NEON)
|
||||
uint16x4_t c16 = vqmovun_s32(vcvtq_s32_f32(vmulq_f32(vec, vdupq_n_f32(255.0f))));
|
||||
uint16x4_t c16 = vqmovun_s32(vcvtq_s32_f32(vaddq_f32(vmulq_f32(vec, vdupq_n_f32(255.0f)), vdupq_n_f32(0.5f))));
|
||||
uint8x8_t c8 = vqmovn_u16(vcombine_u16(c16, c16));
|
||||
return vget_lane_u32(vreinterpret_u32_u8(c8), 0);
|
||||
#else
|
||||
return (clamp_u8((int)(r() * 255.f)) << 0) |
|
||||
(clamp_u8((int)(g() * 255.f)) << 8) |
|
||||
(clamp_u8((int)(b() * 255.f)) << 16) |
|
||||
(clamp_u8((int)(a() * 255.f)) << 24);
|
||||
return (clamp_u8((int)lrintf(r() * 255.f)) << 0) |
|
||||
(clamp_u8((int)lrintf(g() * 255.f)) << 8) |
|
||||
(clamp_u8((int)lrintf(b() * 255.f)) << 16) |
|
||||
(clamp_u8((int)lrintf(a() * 255.f)) << 24);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1492,7 +1497,7 @@ inline Vec4<int> Vec4<int>::operator << (const int amount) const {
|
||||
|
||||
template<>
|
||||
inline Vec4<int> Vec4<int>::operator >> (const int amount) const {
|
||||
return Vec4<int>(_mm_srli_epi32(SAFE_M128I(ivec), amount));
|
||||
return Vec4<int>(_mm_srai_epi32(SAFE_M128I(ivec), amount));
|
||||
}
|
||||
|
||||
// Vec4<float> operation
|
||||
|
||||
@@ -220,6 +220,14 @@ void BinManager::UpdateState() {
|
||||
scissor_.x2 = screenScissorBR.x + SCREEN_SCALE_FACTOR - 1;
|
||||
scissor_.y2 = screenScissorBR.y + SCREEN_SCALE_FACTOR - 1;
|
||||
|
||||
// Pixels past the stride land in the next row, which another task may be drawing (Tokimeki
|
||||
// Memorial's 128 wide blur buffer under a full screen scissor).
|
||||
const bool pastStride = scissorBR.x >= gstate.FrameBufStride();
|
||||
if (pastStride != pastStride_) {
|
||||
pastStride_ = pastStride;
|
||||
dirty_ |= SoftDirty::BINNER_OVERLAP;
|
||||
}
|
||||
|
||||
// If we're about to texture from something still pending (i.e. depth), flush.
|
||||
if (HasTextureWrite(state))
|
||||
Flush("tex");
|
||||
@@ -243,7 +251,7 @@ void BinManager::UpdateState() {
|
||||
|
||||
// Disallow threads when rendering to the target, even offset.
|
||||
bool selfRender = HasTextureWrite(state);
|
||||
int newMaxTasks = selfRender || FORCE_SINGLE_THREAD ? 1 : g_threadManager.GetNumLooperThreads();
|
||||
int newMaxTasks = selfRender || pastStride_ || FORCE_SINGLE_THREAD ? 1 : g_threadManager.GetNumLooperThreads();
|
||||
if (newMaxTasks > MAX_POSSIBLE_TASKS)
|
||||
newMaxTasks = MAX_POSSIBLE_TASKS;
|
||||
// We don't want to overlap wrong, so flush any pending.
|
||||
@@ -273,8 +281,74 @@ void BinManager::UpdateState() {
|
||||
ClearDirty(SoftDirty::BINNER_RANGE);
|
||||
}
|
||||
pendingOverlap_ = pendingOverlap_ || selfRender;
|
||||
selfRender_ = selfRender;
|
||||
ClearDirty(SoftDirty::BINNER_OVERLAP);
|
||||
}
|
||||
states_[stateIndex_].selfTexture = selfRender_;
|
||||
states_[stateIndex_].texFlushGen = texFlushGen_;
|
||||
}
|
||||
|
||||
// The GE samples through its texture cache, so a primitive that textures from the buffer it draws to
|
||||
// mostly sees that buffer as it was before the primitive (exp81, exp159, exp160). The cache actually
|
||||
// fills 8-row blocks as the primitive first reads them, so rows drawn before then show through; this
|
||||
// doesn't model that. A texture that fits in the 8 KB cache stays there until TEXFLUSH, so later
|
||||
// primitives and draws see it as it was when first read (FF Type-0's 16x16 4444 blur, three passes over
|
||||
// one buffer without a flush).
|
||||
const RasterizerState &BinManager::SelfTextureSnapshot(const BinItem &item, const RasterizerState &state) {
|
||||
constexpr uint32_t mirrorMask = 0x041FFFFF;
|
||||
const uint32_t bits = textureBitsPerPixel[state.samplerID.texfmt];
|
||||
const uint32_t fbBpp = state.pixelID.FBFormat() == GE_FORMAT_8888 ? 4 : 2;
|
||||
const uint32_t fbStrideBytes = state.pixelID.cached.framebufStride * fbBpp;
|
||||
|
||||
selfTexState_ = state;
|
||||
uint32_t totalBytes = 0;
|
||||
bool sameTexture = true;
|
||||
for (int i = 0; i <= state.maxTexLevel; ++i) {
|
||||
const uint32_t bytes = state.samplerID.cached.sizes[i].w * bits / 8 * state.samplerID.cached.sizes[i].h;
|
||||
totalBytes += bytes;
|
||||
sameTexture = sameTexture && selfTexAddr_[i] == state.texaddr[i] && selfTexBuf_[i].size() == state.texbufw[i] * bits / 8 * state.samplerID.cached.sizes[i].h;
|
||||
}
|
||||
const bool cacheSized = totalBytes <= 8192;
|
||||
if (cacheSized && selfTexCached_ && sameTexture && selfTexFlushGen_ == state.texFlushGen) {
|
||||
for (int i = 0; i <= state.maxTexLevel; ++i) {
|
||||
if (!selfTexBuf_[i].empty())
|
||||
selfTexState_.texptr[i] = selfTexBuf_[i].data();
|
||||
}
|
||||
return selfTexState_;
|
||||
}
|
||||
if (cacheSized && !(selfTexCached_ && sameTexture))
|
||||
selfTexValid_ = false;
|
||||
selfTexCached_ = cacheSized;
|
||||
selfTexFlushGen_ = state.texFlushGen;
|
||||
|
||||
for (int i = 0; i <= state.maxTexLevel; ++i) {
|
||||
const u8 *src = state.texptr[i];
|
||||
const uint32_t bytes = state.texbufw[i] * bits / 8 * state.samplerID.cached.sizes[i].h;
|
||||
if (!src || !Memory::IsValidRange(state.texaddr[i], bytes))
|
||||
continue;
|
||||
std::vector<u8> &buf = selfTexBuf_[i];
|
||||
if (!selfTexValid_ || selfTexAddr_[i] != state.texaddr[i] || buf.size() != bytes) {
|
||||
buf.assign(src, src + bytes);
|
||||
selfTexAddr_[i] = state.texaddr[i];
|
||||
} else {
|
||||
// Only the rows the previous primitive drew have changed.
|
||||
const DrawingCoords tl = TransformUnit::ScreenToDrawing(selfTexLastRange_.x1, selfTexLastRange_.y1);
|
||||
const DrawingCoords br = TransformUnit::ScreenToDrawing(selfTexLastRange_.x2, selfTexLastRange_.y2);
|
||||
const int64_t rowsStart = (int64_t)(drawTargetAddr_ & mirrorMask) + tl.y * fbStrideBytes;
|
||||
const int64_t rowsEnd = (int64_t)(drawTargetAddr_ & mirrorMask) + (br.y + 1) * fbStrideBytes;
|
||||
const int64_t texStart = state.texaddr[i] & mirrorMask;
|
||||
const int64_t start = std::max(rowsStart, texStart) - texStart;
|
||||
const int64_t end = std::min(rowsEnd, texStart + bytes) - texStart;
|
||||
if (start < end)
|
||||
memcpy(buf.data() + start, src + start, (size_t)(end - start));
|
||||
}
|
||||
selfTexState_.texptr[i] = buf.data();
|
||||
}
|
||||
|
||||
// A depth write could change the texture outside the color rows, so then copy all of it each time.
|
||||
selfTexValid_ = !state.pixelID.depthWrite;
|
||||
selfTexLastRange_ = item.range;
|
||||
return selfTexState_;
|
||||
}
|
||||
|
||||
bool BinManager::HasTextureWrite(const RasterizerState &state) {
|
||||
@@ -411,12 +485,11 @@ void BinManager::AddTriangle(const VertexData &v0, const VertexData &v1, const V
|
||||
Vec2<int> d02((int)v0.screenpos.x - (int)v2.screenpos.x, (int)v0.screenpos.y - (int)v2.screenpos.y);
|
||||
Vec2<int> d12((int)v1.screenpos.x - (int)v2.screenpos.x, (int)v1.screenpos.y - (int)v2.screenpos.y);
|
||||
|
||||
// Drop primitives which are not in CCW order by checking the cross product.
|
||||
// Drop primitives which are not in CCW order by checking the cross product, and ones with zero
|
||||
// area, which light no pixels even on an edge through pixel centers (gpu/probe exp118).
|
||||
static_assert(SCREEN_SCALE_FACTOR <= 16, "Fails if scale factor is too high");
|
||||
if (d01.x * d02.y - d01.y * d02.x < 0)
|
||||
return;
|
||||
// If all points have identical coords, we'll have 0 weights and not skip properly, so skip here.
|
||||
if ((d01.x == 0 && d02.x == 0) || (d01.y == 0 && d02.y == 0))
|
||||
// In 64 bits: with vertices far apart in the 4096 pixel space the products overflow 32 (gpu/probe exp132).
|
||||
if ((int64_t)d01.x * d02.y - (int64_t)d01.y * d02.x <= 0)
|
||||
return;
|
||||
|
||||
// Was it fully outside the scissor?
|
||||
@@ -537,9 +610,17 @@ void BinManager::Drain(bool flushing) {
|
||||
|
||||
if (taskRanges_.size() <= 1) {
|
||||
PROFILE_THIS_SCOPE("bin_drain_single");
|
||||
// Anything (transfers, the CPU) may have written memory since the last drain.
|
||||
selfTexValid_ = false;
|
||||
while (!queue_.Empty()) {
|
||||
const BinItem &item = queue_.PeekNext();
|
||||
DrawBinItem(item, states_[item.stateIndex]);
|
||||
const RasterizerState &state = states_[item.stateIndex];
|
||||
if (state.selfTexture) {
|
||||
DrawBinItem(item, SelfTextureSnapshot(item, state));
|
||||
} else {
|
||||
selfTexValid_ = false;
|
||||
DrawBinItem(item, state);
|
||||
}
|
||||
queue_.SkipNext();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -194,6 +194,11 @@ public:
|
||||
|
||||
void UpdateState();
|
||||
void UpdateClut(const void *src);
|
||||
// TEXFLUSH empties the GE's texture cache, which self-texturing can see.
|
||||
void NotifyTexFlush() {
|
||||
texFlushGen_++;
|
||||
dirty_ |= SoftDirty::SAMPLER_TEXLIST;
|
||||
}
|
||||
|
||||
const Rasterizer::RasterizerState &State() {
|
||||
return states_[stateIndex_];
|
||||
@@ -268,6 +273,20 @@ private:
|
||||
std::unordered_map<uint32_t, BinDirtyRange> pendingReads_;
|
||||
|
||||
bool pendingOverlap_ = false;
|
||||
// Whether the current state textures from what it draws to, and the texture as it was before the
|
||||
// primitive being drawn for one that does.
|
||||
bool selfRender_ = false;
|
||||
// The scissor reaches past the framebuffer's stride.
|
||||
bool pastStride_ = false;
|
||||
Rasterizer::RasterizerState selfTexState_;
|
||||
std::vector<u8> selfTexBuf_[8];
|
||||
uint32_t selfTexAddr_[8]{};
|
||||
bool selfTexValid_ = false;
|
||||
uint32_t texFlushGen_ = 0;
|
||||
uint32_t selfTexFlushGen_ = 0;
|
||||
// The snapshot is of a texture small enough to stay in the GE's 8 KB texture cache.
|
||||
bool selfTexCached_ = false;
|
||||
BinCoords selfTexLastRange_{};
|
||||
bool creatingState_ = false;
|
||||
// JIT clear generations when the current state was computed.
|
||||
int jitGen_ = -1;
|
||||
@@ -288,6 +307,7 @@ private:
|
||||
void MarkPendingWrites(const Rasterizer::RasterizerState &state);
|
||||
bool HasTextureWrite(const Rasterizer::RasterizerState &state);
|
||||
bool IsExactSelfRender(const Rasterizer::RasterizerState &state, const BinItem &item) const;
|
||||
const Rasterizer::RasterizerState &SelfTextureSnapshot(const BinItem &item, const Rasterizer::RasterizerState &state);
|
||||
void OptimizePendingStates(uint16_t first, uint16_t last);
|
||||
BinCoords Scissor(BinCoords range);
|
||||
BinCoords Range(const VertexData &v0, const VertexData &v1, const VertexData &v2);
|
||||
|
||||
+112
-143
@@ -28,7 +28,6 @@
|
||||
namespace Clipper {
|
||||
|
||||
enum {
|
||||
SKIP_FLAG = -1,
|
||||
CLIP_NEG_Z_BIT = 0x20,
|
||||
};
|
||||
|
||||
@@ -39,6 +38,12 @@ static inline int CalcClipMask(const ClipCoords &v) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// With depth clip on, a vertex beyond the near plane is clipped away before the viewport, so its screen
|
||||
// position being out of range doesn't matter (the new vertices get checked after clipping).
|
||||
static inline bool OutsideRangeBeforeClip(const ClipVertexData &v, bool depthClip) {
|
||||
return v.OutsideRange() && !(depthClip && CalcClipMask(v.clippos) != 0);
|
||||
}
|
||||
|
||||
inline bool different_signs(float x, float y) {
|
||||
return ((x <= 0 && y > 0) || (x > 0 && y <= 0));
|
||||
}
|
||||
@@ -59,51 +64,6 @@ inline void clip_interpolate(ClipVertexData &dest, float t, const ClipVertexData
|
||||
dest.v.screenpos.x = 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
#define CLIP_POLY( PLANE_BIT, A, B, C, D ) \
|
||||
{ \
|
||||
if (mask & PLANE_BIT) { \
|
||||
int idxPrev = inlist[0]; \
|
||||
float dpPrev = clip_dotprod(*Vertices[idxPrev], A, B, C, D );\
|
||||
int outcount = 0; \
|
||||
\
|
||||
inlist[n] = inlist[0]; \
|
||||
for (int j = 1; j <= n; j++) { \
|
||||
int idx = inlist[j]; \
|
||||
float dp = clip_dotprod(*Vertices[idx], A, B, C, D ); \
|
||||
if (dpPrev >= 0) { \
|
||||
outlist[outcount++] = idxPrev; \
|
||||
} \
|
||||
\
|
||||
/* Skipping w sign mismatches avoids inversions, but is incorrect. See #16131. */ \
|
||||
/* For now, it's better to avoid inversions as they usually are undesired. */ \
|
||||
if (different_signs(dp, dpPrev)) { \
|
||||
auto &vert = Vertices[numVertices++]; \
|
||||
if (dp < 0) { \
|
||||
float t = dp / (dp - dpPrev); \
|
||||
clip_interpolate(*vert, t, *Vertices[idx], *Vertices[idxPrev]); \
|
||||
} else { \
|
||||
float t = dpPrev / (dpPrev - dp); \
|
||||
clip_interpolate(*vert, t, *Vertices[idxPrev], *Vertices[idx]); \
|
||||
} \
|
||||
outlist[outcount++] = numVertices - 1; \
|
||||
} \
|
||||
\
|
||||
idxPrev = idx; \
|
||||
dpPrev = dp; \
|
||||
} \
|
||||
\
|
||||
if (outcount < 3) \
|
||||
continue; \
|
||||
\
|
||||
{ \
|
||||
int *tmp = inlist; \
|
||||
inlist = outlist; \
|
||||
outlist = tmp; \
|
||||
n = outcount; \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
#define CLIP_LINE(PLANE_BIT, A, B, C, D) \
|
||||
{ \
|
||||
if (mask & PLANE_BIT) { \
|
||||
@@ -127,18 +87,27 @@ inline void clip_interpolate(ClipVertexData &dest, float t, const ClipVertexData
|
||||
} \
|
||||
}
|
||||
|
||||
static inline bool CheckOutsideZ(ClipCoords p, int &pos, int &neg) {
|
||||
constexpr float outsideValue = 1.000030517578125f;
|
||||
float z = p.z / p.w;
|
||||
if (z >= outsideValue) {
|
||||
pos++;
|
||||
return true;
|
||||
// The GE compares the (float24) clip coordinates directly, no division: outside a plane when |c| > w. A
|
||||
// primitive is culled when all its vertices are outside the same plane, x, y or z, for every primitive type
|
||||
// and with clipping and depth clamp on or off (gpu/probe exp176 for x and y: a vertex exactly on the plane is
|
||||
// inside, one a float24 step past it outside).
|
||||
static bool cullXY = true;
|
||||
|
||||
void SetCullXY(bool cull) {
|
||||
cullXY = cull;
|
||||
}
|
||||
|
||||
static inline int OutsideMask(const ClipCoords &p) {
|
||||
int mask = 0;
|
||||
if (cullXY) {
|
||||
if (p.x > p.w) mask |= 1;
|
||||
if (-p.x > p.w) mask |= 2;
|
||||
if (p.y > p.w) mask |= 4;
|
||||
if (-p.y > p.w) mask |= 8;
|
||||
}
|
||||
if (-z >= outsideValue) {
|
||||
neg++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (p.z > p.w) mask |= 16;
|
||||
if (-p.z > p.w) mask |= 32;
|
||||
return mask;
|
||||
}
|
||||
|
||||
static void RotateUV(const VertexData &tl, const VertexData &br, VertexData &tr, VertexData &bl) {
|
||||
@@ -201,16 +170,10 @@ void ProcessRect(const ClipVertexData &v0, const ClipVertexData &v1, BinManager
|
||||
if (v0.OutsideRange() || v1.OutsideRange())
|
||||
return;
|
||||
|
||||
// We may discard the entire rect based on depth values.
|
||||
int outsidePos = 0, outsideNeg = 0;
|
||||
CheckOutsideZ(v0.clippos, outsidePos, outsideNeg);
|
||||
CheckOutsideZ(v1.clippos, outsidePos, outsideNeg);
|
||||
|
||||
// With depth clamp off, we discard the rectangle if even one vert is outside.
|
||||
if (outsidePos + outsideNeg > 0 && !gstate.isDepthClipEnabled())
|
||||
if (OutsideMask(v0.clippos) & OutsideMask(v1.clippos))
|
||||
return;
|
||||
// With it on, both must be outside in the same direction.
|
||||
else if (outsidePos >= 2 || outsideNeg >= 2)
|
||||
// Rects aren't clipped: one with a vertex behind the camera is culled, with depth clip on or off.
|
||||
if (!(v0.clippos.w > 0.0f && v1.clippos.w > 0.0f))
|
||||
return;
|
||||
|
||||
bool splitFog = v0.v.fogdepth != v1.v.fogdepth;
|
||||
@@ -266,6 +229,8 @@ void ProcessPoint(const ClipVertexData &v0, BinManager &binner) {
|
||||
if (!binner.State().throughMode) {
|
||||
if (v0.OutsideRange())
|
||||
return;
|
||||
if (OutsideMask(v0.clippos))
|
||||
return;
|
||||
}
|
||||
|
||||
// Points need no clipping. Will be bounds checked in the rasterizer (which seems backwards?)
|
||||
@@ -279,24 +244,23 @@ void ProcessLine(const ClipVertexData &v0, const ClipVertexData &v1, BinManager
|
||||
return;
|
||||
}
|
||||
|
||||
const bool depthClip = gstate.isDepthClipEnabled();
|
||||
// If any verts were outside range, throw the entire prim away.
|
||||
if (v0.OutsideRange() || v1.OutsideRange())
|
||||
if (OutsideRangeBeforeClip(v0, depthClip) || OutsideRangeBeforeClip(v1, depthClip))
|
||||
return;
|
||||
|
||||
int outsidePos = 0, outsideNeg = 0;
|
||||
CheckOutsideZ(v0.clippos, outsidePos, outsideNeg);
|
||||
CheckOutsideZ(v1.clippos, outsidePos, outsideNeg);
|
||||
|
||||
// With depth clamp off, we discard the line if even one vert is outside.
|
||||
if (outsidePos + outsideNeg > 0 && !gstate.isDepthClipEnabled())
|
||||
return;
|
||||
// With it on, both must be outside in the same direction.
|
||||
else if (outsidePos >= 2 || outsideNeg >= 2)
|
||||
if (OutsideMask(v0.clippos) & OutsideMask(v1.clippos))
|
||||
return;
|
||||
|
||||
int mask0 = CalcClipMask(v0.clippos);
|
||||
int mask1 = CalcClipMask(v1.clippos);
|
||||
int mask = mask0 | mask1;
|
||||
// See ProcessTriangle.
|
||||
if (!depthClip) {
|
||||
if (!(v0.clippos.w > 0.0f && v1.clippos.w > 0.0f))
|
||||
return;
|
||||
mask = 0;
|
||||
}
|
||||
if ((mask & CLIP_NEG_Z_BIT) == 0) {
|
||||
binner.AddLine(v0.v, v1.v);
|
||||
return;
|
||||
@@ -311,11 +275,12 @@ void ProcessLine(const ClipVertexData &v0, const ClipVertexData &v1, BinManager
|
||||
binner.AddLine(data[0].v, data[1].v);
|
||||
}
|
||||
|
||||
void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const ClipVertexData &v2, const ClipVertexData &provoking, BinManager &binner) {
|
||||
void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const ClipVertexData &v2, const ClipVertexData &provoking, BinManager &binner, bool reversed) {
|
||||
int mask = 0;
|
||||
if (!binner.State().throughMode) {
|
||||
const bool depthClip = gstate.isDepthClipEnabled();
|
||||
// If any verts were outside range, throw the entire prim away.
|
||||
if (v0.OutsideRange() || v1.OutsideRange() || v2.OutsideRange())
|
||||
if (OutsideRangeBeforeClip(v0, depthClip) || OutsideRangeBeforeClip(v1, depthClip) || OutsideRangeBeforeClip(v2, depthClip))
|
||||
return;
|
||||
// If all verts have negative W, we also cull.
|
||||
if (v0.clippos.w < 0.0f && v1.clippos.w < 0.0f && v2.clippos.w < 0.0f)
|
||||
@@ -325,18 +290,16 @@ void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const C
|
||||
mask |= CalcClipMask(v1.clippos);
|
||||
mask |= CalcClipMask(v2.clippos);
|
||||
|
||||
// We may discard the entire triangle based on depth values. First check what's outside.
|
||||
int outsidePos = 0, outsideNeg = 0;
|
||||
CheckOutsideZ(v0.clippos, outsidePos, outsideNeg);
|
||||
CheckOutsideZ(v1.clippos, outsidePos, outsideNeg);
|
||||
CheckOutsideZ(v2.clippos, outsidePos, outsideNeg);
|
||||
if (OutsideMask(v0.clippos) & OutsideMask(v1.clippos) & OutsideMask(v2.clippos))
|
||||
return;
|
||||
|
||||
// With depth clamp off, we discard the triangle if even one vert is outside.
|
||||
if (outsidePos + outsideNeg > 0 && !gstate.isDepthClipEnabled())
|
||||
return;
|
||||
// With it on, all three must be outside in the same direction.
|
||||
else if (outsidePos >= 3 || outsideNeg >= 3)
|
||||
return;
|
||||
// With depth clip off, the GE doesn't clip at the near plane: the part beyond it is drawn with
|
||||
// extrapolated depth. A vertex behind the camera (w <= 0) culls the whole triangle instead.
|
||||
if (!depthClip) {
|
||||
if (!(v0.clippos.w > 0.0f && v1.clippos.w > 0.0f && v2.clippos.w > 0.0f))
|
||||
return;
|
||||
mask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// No clipping is common, let's skip processing if we can.
|
||||
@@ -353,65 +316,71 @@ void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const C
|
||||
return;
|
||||
}
|
||||
|
||||
enum { NUM_CLIPPED_VERTICES = 3, NUM_INDICES = NUM_CLIPPED_VERTICES + 3 };
|
||||
|
||||
ClipVertexData* Vertices[NUM_INDICES];
|
||||
ClipVertexData ClippedVertices[NUM_INDICES];
|
||||
for (int i = 0; i < NUM_INDICES; ++i)
|
||||
Vertices[i] = &ClippedVertices[i];
|
||||
|
||||
// TODO: Change logic when it's a backface (why? In what way?)
|
||||
ClippedVertices[0] = v0;
|
||||
ClippedVertices[1] = v1;
|
||||
ClippedVertices[2] = v2;
|
||||
|
||||
int indices[NUM_INDICES] = { 0, 1, 2, SKIP_FLAG, SKIP_FLAG, SKIP_FLAG };
|
||||
int numIndices = 3;
|
||||
|
||||
for (int i = 0; i < 3; i += 3) {
|
||||
int vlist[2][2*6+1];
|
||||
int *inlist = vlist[0], *outlist = vlist[1];
|
||||
int n = 3;
|
||||
int numVertices = 3;
|
||||
|
||||
inlist[0] = 0;
|
||||
inlist[1] = 1;
|
||||
inlist[2] = 2;
|
||||
|
||||
// mark this triangle as unused in case it should be completely clipped
|
||||
indices[0] = SKIP_FLAG;
|
||||
indices[1] = SKIP_FLAG;
|
||||
indices[2] = SKIP_FLAG;
|
||||
|
||||
// The PSP only clips on negative Z (importantly, regardless of viewport.)
|
||||
CLIP_POLY(CLIP_NEG_Z_BIT, 0, 0, 1, 1);
|
||||
|
||||
// transform the poly in inlist into triangles
|
||||
indices[0] = inlist[0];
|
||||
indices[1] = inlist[1];
|
||||
indices[2] = inlist[2];
|
||||
for (int j = 3; j < n; ++j) {
|
||||
indices[numIndices++] = inlist[0];
|
||||
indices[numIndices++] = inlist[j - 1];
|
||||
indices[numIndices++] = inlist[j];
|
||||
}
|
||||
// Clip at the near plane like the GE (gpu/probe exp43, exp44): new vertices are interpolated from
|
||||
// the inside vertex with the GE's math, and with one vertex outside (o), the quad is split from the
|
||||
// vertex before it (p) in the submitted order: (p, a, b) and (p, b, n), a and b being the new
|
||||
// vertices on p-o and o-n. reversed means we got the vertices in the opposite order.
|
||||
const ClipVertexData *src[3] = { &v0, &v1, &v2 };
|
||||
bool outside[3];
|
||||
int numOutside = 0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
outside[i] = src[i]->clippos.z < -src[i]->clippos.w;
|
||||
numOutside += outside[i] ? 1 : 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i + 3 <= numIndices; i += 3) {
|
||||
if (indices[i] != SKIP_FLAG) {
|
||||
ClipVertexData &subv0 = *Vertices[indices[i + 0]];
|
||||
ClipVertexData &subv1 = *Vertices[indices[i + 1]];
|
||||
ClipVertexData &subv2 = *Vertices[indices[i + 2]];
|
||||
auto nearPoint = [](ClipVertexData &dest, const ClipVertexData &in, const ClipVertexData &out) {
|
||||
const float t = TransformUnit::NearPlaneT(in.clippos, out.clippos);
|
||||
dest.Lerp(t, in, out);
|
||||
dest.clippos = TransformUnit::NearPlanePoint(in.clippos, out.clippos, t);
|
||||
// Texture coordinates go through the same arithmetic as the position (gpu/probe exp136).
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
const float delta = TruncateToFloat24(GEAdd(out.v.texturecoords[c], -in.v.texturecoords[c]));
|
||||
dest.v.texturecoords[c] = TruncateToFloat24(GEAdd(ProductToFloat24((double)t * delta), in.v.texturecoords[c]));
|
||||
}
|
||||
bool outsideRange = false;
|
||||
dest.v.screenpos = TransformUnit::ClipToScreen(dest.clippos, &outsideRange);
|
||||
dest.v.clipw = dest.clippos.w;
|
||||
if (outsideRange)
|
||||
dest.v.screenpos.x = 0x7FFFFFFF;
|
||||
};
|
||||
|
||||
if (subv0.OutsideRange() || subv1.OutsideRange() || subv2.OutsideRange())
|
||||
continue;
|
||||
ClipVertexData made[2];
|
||||
const ClipVertexData *tris[2][3];
|
||||
int numTris = 0;
|
||||
if (numOutside == 1) {
|
||||
const int o = outside[0] ? 0 : (outside[1] ? 1 : 2);
|
||||
const int p = reversed ? (o + 1) % 3 : (o + 2) % 3;
|
||||
const int n = reversed ? (o + 2) % 3 : (o + 1) % 3;
|
||||
nearPoint(made[0], *src[p], *src[o]);
|
||||
nearPoint(made[1], *src[n], *src[o]);
|
||||
// Keep the winding we were given.
|
||||
const int first = reversed ? 2 : 1, second = reversed ? 1 : 2;
|
||||
tris[0][0] = src[p]; tris[0][first] = &made[0]; tris[0][second] = &made[1];
|
||||
tris[1][0] = src[p]; tris[1][first] = &made[1]; tris[1][second] = src[n];
|
||||
numTris = 2;
|
||||
} else if (numOutside == 2) {
|
||||
const int i = !outside[0] ? 0 : (!outside[1] ? 1 : 2);
|
||||
const int n = (i + 1) % 3, p = (i + 2) % 3;
|
||||
nearPoint(made[0], *src[i], *src[n]);
|
||||
nearPoint(made[1], *src[i], *src[p]);
|
||||
tris[0][0] = src[i]; tris[0][1] = &made[0]; tris[0][2] = &made[1];
|
||||
numTris = 1;
|
||||
}
|
||||
|
||||
if (gstate.getShadeMode() == GE_SHADE_FLAT) {
|
||||
// So that the order of clipping doesn't matter...
|
||||
subv2.v.color0 = provoking.v.color0;
|
||||
subv2.v.color1 = provoking.v.color1;
|
||||
}
|
||||
for (int t = 0; t < numTris; ++t) {
|
||||
const ClipVertexData &subv0 = *tris[t][0];
|
||||
const ClipVertexData &subv1 = *tris[t][1];
|
||||
const ClipVertexData &subv2 = *tris[t][2];
|
||||
if (subv0.OutsideRange() || subv1.OutsideRange() || subv2.OutsideRange())
|
||||
continue;
|
||||
|
||||
if (gstate.getShadeMode() == GE_SHADE_FLAT) {
|
||||
// So that the order of clipping doesn't matter...
|
||||
VertexData corrected2 = subv2.v;
|
||||
corrected2.color0 = provoking.v.color0;
|
||||
corrected2.color1 = provoking.v.color1;
|
||||
binner.AddTriangle(subv0.v, subv1.v, corrected2);
|
||||
} else {
|
||||
binner.AddTriangle(subv0.v, subv1.v, subv2.v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ namespace Clipper {
|
||||
|
||||
void ProcessPoint(const ClipVertexData &v0, BinManager &binner);
|
||||
void ProcessLine(const ClipVertexData &v0, const ClipVertexData &v1, BinManager &binner);
|
||||
void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const ClipVertexData &v2, const ClipVertexData &provoking, BinManager &binner);
|
||||
void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const ClipVertexData &v2, const ClipVertexData &provoking, BinManager &binner, bool reversed = false);
|
||||
void ProcessRect(const ClipVertexData &v0, const ClipVertexData &v1, BinManager &binner);
|
||||
|
||||
// Immediate-mode vertices carry screen x and y in their clip position, so only z culls them.
|
||||
void SetCullXY(bool cull);
|
||||
|
||||
} // namespace
|
||||
@@ -445,13 +445,14 @@ static inline Vec3<int> GetSourceFactor(PixelBlendFactor factor, const Vec4<int>
|
||||
return Vec3<int>::AssignToAll(2 * source.a());
|
||||
|
||||
case PixelBlendFactor::DOUBLEINVSRCALPHA:
|
||||
return Vec3<int>::AssignToAll(255 - std::min(2 * source.a(), 255));
|
||||
// Can be negative, see SignedBlendingResult().
|
||||
return Vec3<int>::AssignToAll(255 - 2 * source.a());
|
||||
|
||||
case PixelBlendFactor::DOUBLEDSTALPHA:
|
||||
return Vec3<int>::AssignToAll(2 * dst.a());
|
||||
|
||||
case PixelBlendFactor::DOUBLEINVDSTALPHA:
|
||||
return Vec3<int>::AssignToAll(255 - std::min(2 * dst.a(), 255));
|
||||
return Vec3<int>::AssignToAll(255 - 2 * dst.a());
|
||||
|
||||
case PixelBlendFactor::FIX:
|
||||
default:
|
||||
@@ -502,13 +503,14 @@ static inline Vec3<int> GetDestFactor(PixelBlendFactor factor, const Vec4<int> &
|
||||
return Vec3<int>::AssignToAll(2 * source.a());
|
||||
|
||||
case PixelBlendFactor::DOUBLEINVSRCALPHA:
|
||||
return Vec3<int>::AssignToAll(255 - std::min(2 * source.a(), 255));
|
||||
// Can be negative, see SignedBlendingResult().
|
||||
return Vec3<int>::AssignToAll(255 - 2 * source.a());
|
||||
|
||||
case PixelBlendFactor::DOUBLEDSTALPHA:
|
||||
return Vec3<int>::AssignToAll(2 * dst.a());
|
||||
|
||||
case PixelBlendFactor::DOUBLEINVDSTALPHA:
|
||||
return Vec3<int>::AssignToAll(255 - std::min(2 * dst.a(), 255));
|
||||
return Vec3<int>::AssignToAll(255 - 2 * dst.a());
|
||||
|
||||
case PixelBlendFactor::FIX:
|
||||
default:
|
||||
@@ -524,10 +526,39 @@ static inline Vec3<int> GetDestFactor(PixelBlendFactor factor, const Vec4<int> &
|
||||
}
|
||||
|
||||
// Removed inline here - it was never chosen to be inlined by the compiler anyway, too complex.
|
||||
static bool IsSignedBlendFactor(PixelBlendFactor factor) {
|
||||
return factor == PixelBlendFactor::DOUBLEINVSRCALPHA || factor == PixelBlendFactor::DOUBLEINVDSTALPHA;
|
||||
}
|
||||
|
||||
// 255 - 2a goes negative for a >= 128, and then the term subtracts: -(((2c + 1) * (2|f| + 1)) >> 10)
|
||||
// (gpu/probe exp116, all factors and equations exact).
|
||||
static Vec3<int> SignedBlendingResult(const PixelFuncID &pixelID, const Vec3<int> &srcfactor, const Vec3<int> &dstfactor, const Vec4<int> &source, const Vec4<int> &dst) {
|
||||
auto term = [](int c, int f) {
|
||||
const int t = ((2 * c + 1) * (2 * std::abs(f) + 1)) >> 10;
|
||||
return f < 0 ? -t : t;
|
||||
};
|
||||
Vec3<int> result;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int s = term(source[i], srcfactor[i]);
|
||||
const int d = term(dst[i], dstfactor[i]);
|
||||
switch (pixelID.AlphaBlendEq()) {
|
||||
case GE_BLENDMODE_MUL_AND_SUBTRACT: result[i] = s - d; break;
|
||||
case GE_BLENDMODE_MUL_AND_SUBTRACT_REVERSE: result[i] = d - s; break;
|
||||
default: result[i] = s + d; break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Vec3<int> AlphaBlendingResult(const PixelFuncID &pixelID, const Vec4<int> &source, const Vec4<int> &dst) {
|
||||
// Note: These factors cannot go below 0, but they can go above 255 when doubling.
|
||||
// Note: These factors can go above 255 when doubling, and below 0 for the doubled inverses.
|
||||
Vec3<int> srcfactor = GetSourceFactor(pixelID.AlphaBlendSrc(), source, dst, pixelID.cached.alphaBlendSrc);
|
||||
Vec3<int> dstfactor = GetDestFactor(pixelID.AlphaBlendDst(), source, dst, pixelID.cached.alphaBlendDst);
|
||||
const GEBlendMode eq = pixelID.AlphaBlendEq();
|
||||
if ((IsSignedBlendFactor(pixelID.AlphaBlendSrc()) || IsSignedBlendFactor(pixelID.AlphaBlendDst())) &&
|
||||
(eq == GE_BLENDMODE_MUL_AND_ADD || eq == GE_BLENDMODE_MUL_AND_SUBTRACT || eq == GE_BLENDMODE_MUL_AND_SUBTRACT_REVERSE)) {
|
||||
return SignedBlendingResult(pixelID, srcfactor, dstfactor, source, dst);
|
||||
}
|
||||
|
||||
switch (pixelID.AlphaBlendEq()) {
|
||||
case GE_BLENDMODE_MUL_AND_ADD:
|
||||
@@ -769,6 +800,9 @@ void SOFTRAST_CALL DrawSinglePixel(int x, int y, int z, int fog, Vec4IntArg colo
|
||||
}
|
||||
|
||||
SingleFunc GetSingleFunc(const PixelFuncID &id, BinManager *binner) {
|
||||
// The jit clamps blend factors at 0.
|
||||
if (id.alphaBlend && (IsSignedBlendFactor(id.AlphaBlendSrc()) || IsSignedBlendFactor(id.AlphaBlendDst())))
|
||||
return jitCache->GenericSingle(id);
|
||||
SingleFunc jitted = jitCache->GetSingle(id, binner);
|
||||
if (jitted) {
|
||||
return jitted;
|
||||
|
||||
@@ -1998,7 +1998,7 @@ bool PixelJitCache::Jit_ApplyLogicOp(const PixelFuncID &id, RegCache::Reg colorR
|
||||
skipStandardWrites_.push_back(J(true));
|
||||
|
||||
tableValues[GE_LOGIC_NAND] = GetCodePointer();
|
||||
AND(bits, R(temp1Reg), MatR(colorOff));
|
||||
AND(bits, R(colorReg), MatR(colorOff));
|
||||
NOT(32, R(colorReg));
|
||||
if (stencilReg != INVALID_REG) {
|
||||
AND(bits, R(colorReg), notStencilMask);
|
||||
|
||||
+18
-3
@@ -41,6 +41,19 @@ static inline GEComparison OptimizeRefByteCompare(GEComparison func, u8 ref) {
|
||||
return func;
|
||||
}
|
||||
|
||||
// The stencil test compares the other way around, ref against the buffer's value.
|
||||
static inline GEComparison OptimizeStencilRefCompare(GEComparison func, u8 ref) {
|
||||
if (ref == 0 && func == GE_COMP_GREATER)
|
||||
return GE_COMP_NEVER;
|
||||
if (ref == 0xFF && func == GE_COMP_LESS)
|
||||
return GE_COMP_NEVER;
|
||||
if (ref == 0 && func == GE_COMP_LEQUAL)
|
||||
return GE_COMP_ALWAYS;
|
||||
if (ref == 0xFF && func == GE_COMP_GEQUAL)
|
||||
return GE_COMP_ALWAYS;
|
||||
return func;
|
||||
}
|
||||
|
||||
static inline PixelBlendFactor OptimizeAlphaFactor(uint32_t color) {
|
||||
if (color == 0x00000000)
|
||||
return PixelBlendFactor::ZERO;
|
||||
@@ -84,7 +97,7 @@ void ComputePixelFuncID(PixelFuncID *id) {
|
||||
|
||||
if (id->stencilTest) {
|
||||
id->stencilTestRef = gstate.getStencilTestRef() & gstate.getStencilTestMask();
|
||||
id->stencilTestFunc = OptimizeRefByteCompare(gstate.getStencilTestFunction(), id->stencilTestRef);
|
||||
id->stencilTestFunc = OptimizeStencilRefCompare(gstate.getStencilTestFunction(), id->stencilTestRef);
|
||||
id->hasStencilTestMask = gstate.getStencilTestMask() != 0xFF && gstate.FrameBufFormat() != GE_FORMAT_565;
|
||||
|
||||
// Stencil can't be written on 565, and any invalid op acts like KEEP, which is 0.
|
||||
@@ -476,8 +489,10 @@ void ComputeSamplerID(SamplerID *id_out) {
|
||||
|
||||
id.texfmt = fmt;
|
||||
id.swizzle = gstate.isTextureSwizzled();
|
||||
// Only CLUT4 can use separate CLUTs per mimap.
|
||||
id.useSharedClut = fmt != GE_TFMT_CLUT4 || maxLevel == 0 || !gstate.isMipmapEnabled() || gstate.isClutSharedForMipmaps();
|
||||
// With separate CLUTs, level n puts n above the index bits, wrapped to the CLUT: CLUT4 adds n * 16,
|
||||
// CLUT8 with a 16-bit palette (512 entries) adds (n & 1) * 256, and the rest wrap back to 0 (gpu/probe exp90).
|
||||
const bool clutPerLevel = fmt == GE_TFMT_CLUT4 || (fmt == GE_TFMT_CLUT8 && gstate.getClutPaletteFormat() != GE_CMODE_32BIT_ABGR8888);
|
||||
id.useSharedClut = !clutPerLevel || maxLevel == 0 || !gstate.isMipmapEnabled() || gstate.isClutSharedForMipmaps();
|
||||
if (gstate.isTextureFormatIndexed()) {
|
||||
id.clutfmt = gstate.getClutPaletteFormat();
|
||||
id.hasClutMask = gstate.getClutIndexMask() != 0xFF;
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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/.
|
||||
|
||||
#include "Common/BitScan.h"
|
||||
#include "GPU/Software/GEMath.h"
|
||||
|
||||
// The GE's reciprocal: the top 7 bits of w's 15-bit float24 mantissa pick a segment, which the low
|
||||
// 8 bits interpolate linearly. b is the segment's start in units of 2^-17, m its slope in units of
|
||||
// 2^-23 per step, and 63 a rounding bias below half. Measured for every mantissa on hardware.
|
||||
struct GERecipSegment {
|
||||
int32_t b;
|
||||
int32_t m;
|
||||
};
|
||||
|
||||
static const GERecipSegment geRecipSegments[128] = {
|
||||
{ 131072, -254 }, { 130055, -250 }, { 129054, -246 }, { 128071, -243 }, { 127099, -239 }, { 126143, -235 }, { 125203, -232 }, { 124274, -228 },
|
||||
{ 123361, -225 }, { 122461, -222 }, { 121574, -219 }, { 120700, -216 }, { 119836, -212 }, { 118986, -209 }, { 118150, -207 }, { 117323, -204 },
|
||||
{ 116508, -201 }, { 115704, -198 }, { 114913, -196 }, { 114130, -193 }, { 113358, -190 }, { 112599, -188 }, { 111847, -185 }, { 111107, -183 },
|
||||
{ 110375, -180 }, { 109654, -178 }, { 108943, -176 }, { 108239, -173 }, { 107545, -171 }, { 106861, -169 }, { 106184, -167 }, { 105517, -165 },
|
||||
{ 104858, -163 }, { 104206, -161 }, { 103563, -159 }, { 102927, -157 }, { 102300, -155 }, { 101680, -153 }, { 101067, -151 }, { 100463, -150 },
|
||||
{ 99865, -148 }, { 99273, -146 }, { 98689, -144 }, { 98113, -143 }, { 97542, -141 }, { 96977, -139 }, { 96421, -138 }, { 95869, -136 },
|
||||
{ 95326, -135 }, { 94786, -133 }, { 94255, -132 }, { 93727, -130 }, { 93207, -129 }, { 92691, -127 }, { 92182, -126 }, { 91680, -125 },
|
||||
{ 91180, -123 }, { 90688, -122 }, { 90201, -121 }, { 89717, -119 }, { 89240, -118 }, { 88769, -117 }, { 88302, -116 }, { 87838, -114 },
|
||||
{ 87381, -113 }, { 86928, -112 }, { 86481, -111 }, { 86037, -110 }, { 85599, -109 }, { 85164, -108 }, { 84732, -106 }, { 84307, -105 },
|
||||
{ 83885, -104 }, { 83468, -103 }, { 83055, -102 }, { 82646, -101 }, { 82241, -100 }, { 81839, -99 }, { 81442, -98 }, { 81049, -97 },
|
||||
{ 80659, -96 }, { 80275, -96 }, { 79892, -95 }, { 79513, -94 }, { 79138, -93 }, { 78766, -92 }, { 78398, -91 }, { 78033, -90 },
|
||||
{ 77673, -90 }, { 77315, -89 }, { 76960, -88 }, { 76608, -87 }, { 76260, -86 }, { 75914, -85 }, { 75574, -85 }, { 75234, -84 },
|
||||
{ 74898, -83 }, { 74564, -82 }, { 74236, -82 }, { 73908, -81 }, { 73584, -80 }, { 73264, -80 }, { 72944, -79 }, { 72628, -78 },
|
||||
{ 72316, -78 }, { 72005, -77 }, { 71697, -76 }, { 71393, -76 }, { 71090, -75 }, { 70789, -74 }, { 70493, -74 }, { 70197, -73 },
|
||||
{ 69906, -73 }, { 69615, -72 }, { 69327, -71 }, { 69042, -71 }, { 68759, -70 }, { 68479, -70 }, { 68200, -69 }, { 67925, -69 },
|
||||
{ 67650, -68 }, { 67378, -67 }, { 67109, -67 }, { 66841, -66 }, { 66577, -66 }, { 66313, -65 }, { 66052, -65 }, { 65793, -64 },
|
||||
};
|
||||
|
||||
// w must be a normal float24. Returns a float24 (q has 16 significant bits, or is 2^16).
|
||||
float GERecip(float w) {
|
||||
uint32_t bits;
|
||||
memcpy(&bits, &w, sizeof(bits));
|
||||
const uint32_t i = (bits >> 8) & 0x7FFF;
|
||||
const int e = (int)((bits >> 23) & 0xFF) - 127; // |w| = 1.i * 2^e
|
||||
const GERecipSegment &seg = geRecipSegments[i >> 8];
|
||||
const int32_t q = (64 * seg.b + 63 + seg.m * (int32_t)(i & 255)) >> 7; // 1 / 1.i in units of 2^-16
|
||||
return copysign(ldexpf((float)q, -16 - e), w);
|
||||
}
|
||||
|
||||
// The GE's reciprocal square root (gpu/probe exp69, bit exact): for d = 1.i * 2^E, segment i >> 8 of the
|
||||
// table for E's parity, linearly interpolated like GERecip by the low 8 bits. Even E stores 1 / sqrt(1.i),
|
||||
// odd E 1 / sqrt(2 * 1.i), both in units of 2^-16.
|
||||
static const GERecipSegment geRsqrtSegments[2][128] = {
|
||||
{
|
||||
{ 131072, -127 }, { 130562, -125 }, { 130059, -124 }, { 129562, -122 }, { 129070, -121 }, { 128584, -120 }, { 128103, -118 }, { 127628, -117 },
|
||||
{ 127158, -116 }, { 126693, -114 }, { 126233, -113 }, { 125778, -112 }, { 125328, -111 }, { 124883, -110 }, { 124443, -108 }, { 124007, -107 },
|
||||
{ 123575, -106 }, { 123149, -105 }, { 122726, -104 }, { 122308, -103 }, { 121894, -102 }, { 121484, -101 }, { 121079, -100 }, { 120677, -99 },
|
||||
{ 120279, -98 }, { 119886, -97 }, { 119496, -96 }, { 119110, -95 }, { 118727, -94 }, { 118349, -93 }, { 117974, -92 }, { 117602, -92 },
|
||||
{ 117234, -91 }, { 116869, -90 }, { 116508, -89 }, { 116150, -88 }, { 115795, -87 }, { 115444, -87 }, { 115096, -86 }, { 114751, -85 },
|
||||
{ 114409, -84 }, { 114070, -83 }, { 113734, -83 }, { 113400, -82 }, { 113070, -81 }, { 112743, -81 }, { 112419, -80 }, { 112097, -79 },
|
||||
{ 111778, -79 }, { 111462, -78 }, { 111148, -77 }, { 110837, -77 }, { 110529, -76 }, { 110223, -75 }, { 109920, -75 }, { 109619, -74 },
|
||||
{ 109321, -73 }, { 109025, -73 }, { 108732, -72 }, { 108441, -72 }, { 108152, -71 }, { 107865, -71 }, { 107581, -70 }, { 107299, -69 },
|
||||
{ 107019, -69 }, { 106742, -68 }, { 106466, -68 }, { 106193, -67 }, { 105922, -67 }, { 105652, -66 }, { 105385, -66 }, { 105120, -65 },
|
||||
{ 104857, -65 }, { 104596, -64 }, { 104337, -64 }, { 104079, -63 }, { 103824, -63 }, { 103570, -62 }, { 103319, -62 }, { 103069, -62 },
|
||||
{ 102821, -61 }, { 102575, -61 }, { 102330, -60 }, { 102087, -60 }, { 101846, -59 }, { 101607, -59 }, { 101369, -59 }, { 101133, -58 },
|
||||
{ 100899, -58 }, { 100666, -57 }, { 100435, -57 }, { 100205, -56 }, { 99977, -56 }, { 99751, -56 }, { 99526, -55 }, { 99303, -55 },
|
||||
{ 99081, -55 }, { 98860, -54 }, { 98641, -54 }, { 98424, -54 }, { 98208, -53 }, { 97993, -53 }, { 97780, -52 }, { 97568, -52 },
|
||||
{ 97357, -52 }, { 97148, -51 }, { 96940, -51 }, { 96734, -51 }, { 96529, -50 }, { 96325, -50 }, { 96122, -50 }, { 95921, -50 },
|
||||
{ 95721, -49 }, { 95522, -49 }, { 95325, -49 }, { 95128, -48 }, { 94933, -48 }, { 94739, -48 }, { 94546, -47 }, { 94355, -47 },
|
||||
{ 94164, -47 }, { 93975, -47 }, { 93787, -46 }, { 93600, -46 }, { 93414, -46 }, { 93229, -45 }, { 93046, -45 }, { 92863, -45 }
|
||||
},
|
||||
{
|
||||
{ 92681, -89 }, { 92321, -88 }, { 91966, -87 }, { 91614, -86 }, { 91266, -85 }, { 90923, -84 }, { 90583, -84 }, { 90247, -83 },
|
||||
{ 89914, -82 }, { 89585, -81 }, { 89260, -80 }, { 88939, -79 }, { 88620, -78 }, { 88306, -77 }, { 87994, -77 }, { 87686, -76 },
|
||||
{ 87381, -75 }, { 87079, -74 }, { 86780, -73 }, { 86485, -73 }, { 86192, -72 }, { 85902, -71 }, { 85615, -70 }, { 85331, -70 },
|
||||
{ 85050, -69 }, { 84772, -68 }, { 84496, -68 }, { 84223, -67 }, { 83953, -66 }, { 83685, -66 }, { 83420, -65 }, { 83157, -65 },
|
||||
{ 82897, -64 }, { 82639, -63 }, { 82383, -63 }, { 82130, -62 }, { 81880, -62 }, { 81631, -61 }, { 81385, -61 }, { 81141, -60 },
|
||||
{ 80899, -59 }, { 80659, -59 }, { 80422, -58 }, { 80186, -58 }, { 79953, -57 }, { 79721, -57 }, { 79492, -56 }, { 79264, -56 },
|
||||
{ 79039, -55 }, { 78815, -55 }, { 78594, -54 }, { 78374, -54 }, { 78156, -54 }, { 77940, -53 }, { 77725, -53 }, { 77512, -52 },
|
||||
{ 77302, -52 }, { 77092, -51 }, { 76885, -51 }, { 76679, -51 }, { 76475, -50 }, { 76272, -50 }, { 76071, -49 }, { 75872, -49 },
|
||||
{ 75674, -49 }, { 75478, -48 }, { 75283, -48 }, { 75090, -47 }, { 74898, -47 }, { 74707, -47 }, { 74519, -46 }, { 74331, -46 },
|
||||
{ 74145, -46 }, { 73960, -45 }, { 73777, -45 }, { 73595, -45 }, { 73415, -44 }, { 73235, -44 }, { 73057, -44 }, { 72881, -43 },
|
||||
{ 72705, -43 }, { 72531, -43 }, { 72358, -42 }, { 72186, -42 }, { 72016, -42 }, { 71847, -42 }, { 71679, -41 }, { 71512, -41 },
|
||||
{ 71346, -41 }, { 71181, -40 }, { 71018, -40 }, { 70856, -40 }, { 70694, -40 }, { 70534, -39 }, { 70375, -39 }, { 70217, -39 },
|
||||
{ 70060, -38 }, { 69905, -38 }, { 69750, -38 }, { 69596, -38 }, { 69443, -37 }, { 69291, -37 }, { 69141, -37 }, { 68991, -37 },
|
||||
{ 68842, -36 }, { 68694, -36 }, { 68547, -36 }, { 68401, -36 }, { 68256, -36 }, { 68112, -35 }, { 67969, -35 }, { 67826, -35 },
|
||||
{ 67685, -35 }, { 67544, -34 }, { 67405, -34 }, { 67266, -34 }, { 67128, -34 }, { 66991, -34 }, { 66854, -33 }, { 66719, -33 },
|
||||
{ 66584, -33 }, { 66450, -33 }, { 66317, -33 }, { 66185, -32 }, { 66054, -32 }, { 65923, -32 }, { 65793, -32 }, { 65664, -32 }
|
||||
},
|
||||
};
|
||||
|
||||
// d must be a positive normal float24.
|
||||
float GERsqrt(float d) {
|
||||
uint32_t bits;
|
||||
memcpy(&bits, &d, sizeof(bits));
|
||||
const uint32_t i = (bits >> 8) & 0x7FFF;
|
||||
const int e = (int)((bits >> 23) & 0xFF) - 127;
|
||||
const GERecipSegment &seg = geRsqrtSegments[e & 1][i >> 8];
|
||||
const int32_t q = (64 * seg.b + 63 + seg.m * (int32_t)(i & 255)) >> 7;
|
||||
return ldexpf((float)q, -16 - (e >> 1));
|
||||
}
|
||||
|
||||
float GEAddFloat24(float a, float b) {
|
||||
return TruncateToFloat24(GEAdd(a, b));
|
||||
}
|
||||
|
||||
// A dot product as the GE's dot product unit sums it (like a matrix row without translation).
|
||||
float GEDot(const Vec3f &a, const Vec3f &b) {
|
||||
GERowTerm terms[3] = {
|
||||
GEProduct(TruncateToFloat24(a.x), TruncateToFloat24(b.x)),
|
||||
GEProduct(TruncateToFloat24(a.y), TruncateToFloat24(b.y)),
|
||||
GEProduct(TruncateToFloat24(a.z), TruncateToFloat24(b.z)),
|
||||
};
|
||||
return GERowSum(terms, 3);
|
||||
}
|
||||
|
||||
// Normalizes like the GE (gpu/probe exp69, exp61): the squared length summed like a dot product, then
|
||||
// each component times GERsqrt of it, as a float24. Returns the length, d2 * GERsqrt(d2) (0 leaves v alone).
|
||||
float GENormalize(Vec3f &v) {
|
||||
const float d2 = GEDot(v, v);
|
||||
if (!(d2 > 0.0f) || !std::isfinite(d2))
|
||||
return 0.0f;
|
||||
const float r = GERsqrt(d2);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
v[i] = ProductToFloat24((double)TruncateToFloat24(v[i]) * r);
|
||||
return ProductToFloat24((double)d2 * r);
|
||||
}
|
||||
|
||||
// The GE's reciprocal for triangle setup (gpu/probe exp38-41): a 16-bit significand index, 256 segments,
|
||||
// each linear over its low 8 bits, q = (2K + M * x) >> 8. Not the same table as the one for z/w.
|
||||
struct GESetupRecipSegment {
|
||||
int32_t k;
|
||||
int32_t m;
|
||||
};
|
||||
|
||||
static const GESetupRecipSegment geSetupRecip[256] = {
|
||||
{ 8388735, -257 }, { 8355952, -254 }, { 8323568, -252 }, { 8291440, -250 },
|
||||
{ 8259552, -248 }, { 8227904, -246 }, { 8196496, -244 }, { 8165328, -242 },
|
||||
{ 8134400, -240 }, { 8103696, -238 }, { 8073232, -237 }, { 8043008, -235 },
|
||||
{ 8012992, -233 }, { 7983200, -231 }, { 7953632, -230 }, { 7924288, -228 },
|
||||
{ 7895152, -226 }, { 7866240, -225 }, { 7837520, -223 }, { 7809024, -221 },
|
||||
{ 7780736, -220 }, { 7752640, -218 }, { 7724752, -217 }, { 7697072, -215 },
|
||||
{ 7669584, -213 }, { 7642288, -212 }, { 7615184, -210 }, { 7588272, -209 },
|
||||
{ 7561552, -208 }, { 7535024, -206 }, { 7508672, -205 }, { 7482512, -203 },
|
||||
{ 7456528, -202 }, { 7430736, -200 }, { 7405104, -199 }, { 7379664, -198 },
|
||||
{ 7354384, -196 }, { 7329280, -195 }, { 7304352, -194 }, { 7279600, -192 },
|
||||
{ 7255008, -191 }, { 7230576, -190 }, { 7206320, -188 }, { 7182208, -187 },
|
||||
{ 7158272, -186 }, { 7134496, -185 }, { 7110864, -183 }, { 7087392, -182 },
|
||||
{ 7064080, -181 }, { 7040928, -180 }, { 7017920, -179 }, { 6995056, -178 },
|
||||
{ 6972336, -176 }, { 6949776, -175 }, { 6927360, -174 }, { 6905088, -173 },
|
||||
{ 6882960, -172 }, { 6860960, -171 }, { 6839104, -170 }, { 6817408, -169 },
|
||||
{ 6795824, -168 }, { 6774384, -166 }, { 6753088, -165 }, { 6731920, -164 },
|
||||
{ 6710880, -163 }, { 6689968, -162 }, { 6669200, -161 }, { 6648544, -160 },
|
||||
{ 6628032, -159 }, { 6607632, -158 }, { 6587360, -157 }, { 6567216, -156 },
|
||||
{ 6547200, -155 }, { 6527296, -154 }, { 6507520, -154 }, { 6487856, -153 },
|
||||
{ 6468320, -152 }, { 6448896, -151 }, { 6429584, -150 }, { 6410384, -149 },
|
||||
{ 6391312, -148 }, { 6372352, -147 }, { 6353488, -146 }, { 6334752, -145 },
|
||||
{ 6316128, -145 }, { 6297600, -144 }, { 6279184, -143 }, { 6260880, -142 },
|
||||
{ 6242672, -141 }, { 6224576, -140 }, { 6206592, -140 }, { 6188704, -139 },
|
||||
{ 6170928, -138 }, { 6153248, -137 }, { 6135664, -136 }, { 6118176, -136 },
|
||||
{ 6100800, -135 }, { 6083520, -134 }, { 6066336, -133 }, { 6049248, -133 },
|
||||
{ 6032256, -132 }, { 6015360, -131 }, { 5998544, -130 }, { 5981840, -130 },
|
||||
{ 5965232, -129 }, { 5948704, -128 }, { 5932272, -128 }, { 5915920, -127 },
|
||||
{ 5899680, -126 }, { 5883504, -125 }, { 5867440, -125 }, { 5851440, -124 },
|
||||
{ 5835552, -123 }, { 5819728, -123 }, { 5804000, -122 }, { 5788352, -121 },
|
||||
{ 5772800, -121 }, { 5757328, -120 }, { 5741920, -119 }, { 5726608, -119 },
|
||||
{ 5711392, -118 }, { 5696240, -118 }, { 5681168, -117 }, { 5666176, -116 },
|
||||
{ 5651264, -116 }, { 5636432, -115 }, { 5621680, -114 }, { 5606992, -114 },
|
||||
{ 5592400, -113 }, { 5577872, -113 }, { 5563424, -112 }, { 5549040, -112 },
|
||||
{ 5534736, -111 }, { 5520512, -110 }, { 5506368, -110 }, { 5492272, -109 },
|
||||
{ 5478272, -109 }, { 5464320, -108 }, { 5450464, -108 }, { 5436656, -107 },
|
||||
{ 5422928, -106 }, { 5409264, -106 }, { 5395680, -105 }, { 5382160, -105 },
|
||||
{ 5368704, -104 }, { 5355312, -104 }, { 5341984, -103 }, { 5328736, -103 },
|
||||
{ 5315552, -102 }, { 5302416, -102 }, { 5289360, -101 }, { 5276368, -101 },
|
||||
{ 5263440, -100 }, { 5250560, -100 }, { 5237760, -99 }, { 5225008, -99 },
|
||||
{ 5212336, -98 }, { 5199712, -98 }, { 5187152, -97 }, { 5174656, -97 },
|
||||
{ 5162208, -96 }, { 5149840, -96 }, { 5137520, -96 }, { 5125248, -95 },
|
||||
{ 5113056, -95 }, { 5100896, -94 }, { 5088816, -94 }, { 5076784, -93 },
|
||||
{ 5064816, -93 }, { 5052896, -92 }, { 5041040, -92 }, { 5029232, -92 },
|
||||
{ 5017472, -91 }, { 5005776, -91 }, { 4994144, -90 }, { 4982560, -90 },
|
||||
{ 4971024, -89 }, { 4959536, -89 }, { 4948112, -89 }, { 4936736, -88 },
|
||||
{ 4925408, -88 }, { 4914144, -87 }, { 4902928, -87 }, { 4891760, -87 },
|
||||
{ 4880640, -86 }, { 4869568, -86 }, { 4858560, -85 }, { 4847584, -85 },
|
||||
{ 4836672, -85 }, { 4825792, -84 }, { 4814976, -84 }, { 4804208, -83 },
|
||||
{ 4793488, -83 }, { 4782800, -83 }, { 4772176, -82 }, { 4761600, -82 },
|
||||
{ 4751056, -82 }, { 4740576, -81 }, { 4730128, -81 }, { 4719744, -81 },
|
||||
{ 4709392, -80 }, { 4699088, -80 }, { 4688816, -79 }, { 4678608, -79 },
|
||||
{ 4668432, -79 }, { 4658304, -78 }, { 4648224, -78 }, { 4638192, -78 },
|
||||
{ 4628192, -77 }, { 4618240, -77 }, { 4608320, -77 }, { 4598464, -76 },
|
||||
{ 4588640, -76 }, { 4578848, -76 }, { 4569104, -75 }, { 4559408, -75 },
|
||||
{ 4549744, -75 }, { 4540128, -74 }, { 4530544, -74 }, { 4521008, -74 },
|
||||
{ 4511520, -74 }, { 4502048, -73 }, { 4492640, -73 }, { 4483264, -73 },
|
||||
{ 4473920, -72 }, { 4464608, -72 }, { 4455360, -72 }, { 4446128, -71 },
|
||||
{ 4436944, -71 }, { 4427792, -71 }, { 4418688, -71 }, { 4409616, -70 },
|
||||
{ 4400576, -70 }, { 4391568, -70 }, { 4382608, -69 }, { 4373680, -69 },
|
||||
{ 4364800, -69 }, { 4355936, -69 }, { 4347120, -68 }, { 4338336, -68 },
|
||||
{ 4329600, -68 }, { 4320880, -67 }, { 4312208, -67 }, { 4303568, -67 },
|
||||
{ 4294960, -67 }, { 4286384, -66 }, { 4277840, -66 }, { 4269344, -66 },
|
||||
{ 4260880, -66 }, { 4252432, -65 }, { 4244032, -65 }, { 4235664, -65 },
|
||||
{ 4227328, -65 }, { 4219024, -64 }, { 4210752, -64 }, { 4202512, -64 },
|
||||
};
|
||||
|
||||
// Every K but segment 0's is floor(2^39 / (65536 + 256 i) / 16) * 16, the start of segment i in 1/8ths. The table
|
||||
// was fit to probe data; segments it left loose (39, 161, 162, 253, 255) use that formula, which games confirmed
|
||||
// for 39 and 161, and large-w UV planes (gpu/probe exp136, exp138) for 105 and 106.
|
||||
int64_t GESetupRecip(uint64_t absDet, int *e) {
|
||||
const uint32_t hi = (uint32_t)(absDet >> 32);
|
||||
const int ex = hi ? 63 - (int)clz32_nonzero(hi) : 31 - (int)clz32_nonzero((uint32_t)absDet);
|
||||
*e = ex;
|
||||
const int index = (int)((absDet << 16) >> ex) - 65536;
|
||||
const GESetupRecipSegment &seg = geSetupRecip[index >> 8];
|
||||
return (2 * (int64_t)seg.k + seg.m * (index & 255)) >> 8;
|
||||
}
|
||||
|
||||
float GELightPow(float v, float e) {
|
||||
if (e <= 0.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
if (v > 0.0f) {
|
||||
// PSPLightPow, but the product is exact, and keeps 19 fraction bits of the log (units of 16 in
|
||||
// the float's bits), truncated toward zero (gpu/probe exp102: e = 1.1 and 0.3, exp4: e = 0.05).
|
||||
int32_t ix;
|
||||
memcpy(&ix, &v, sizeof(ix));
|
||||
double t = std::trunc((double)e * (double)(ix - 0x3F800000) * (1.0 / 16.0)) * 16.0 + 1065353216.0;
|
||||
t = t >= 0.0 ? (t < 2139095039.0 ? t : 2139095039.0) : 0.0;
|
||||
int32_t iy = (int32_t)t;
|
||||
float y;
|
||||
memcpy(&y, &iy, sizeof(y));
|
||||
return y;
|
||||
}
|
||||
// Negative stays negative, so let's just return the original.
|
||||
return v;
|
||||
}
|
||||
|
||||
int GELineCoverageAlpha(int64_t x0, int64_t y0, int64_t x1, int64_t y1, int px, int py) {
|
||||
int64_t PX = (int64_t)px * 16 + 16 / 2;
|
||||
int64_t PY = (int64_t)py * 16 + 16 / 2;
|
||||
// A diagonal line is y-major (gpu/probe exp137).
|
||||
if (std::abs(x1 - x0) <= std::abs(y1 - y0)) {
|
||||
std::swap(x0, y0);
|
||||
std::swap(x1, y1);
|
||||
std::swap(PX, PY);
|
||||
}
|
||||
const int64_t den = x1 - x0;
|
||||
if (den == 0)
|
||||
return 128;
|
||||
// The minor offset at the pixel walks from v0 along the major axis with a slope like a triangle plane's
|
||||
// gradient: the setup reciprocal of the major length, 14 fraction bits per subpixel, floored
|
||||
// (gpu/probe exp112, exact).
|
||||
int e;
|
||||
const int64_t q = GESetupRecip((uint64_t)std::abs(den), &e);
|
||||
const int64_t walk = den > 0 ? PX - x0 : x0 - PX;
|
||||
const int64_t slope = (-16 * (y1 - y0) * q) >> (e + 2);
|
||||
const int64_t o = ((16 * (PY - y0)) * (1 << 14) + slope * walk) >> 14;
|
||||
return (int)std::clamp<int64_t>(128 - (o < 0 ? -o : o), 0, 255);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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/.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The GE's own arithmetic, as measured on a PSP (gpu/probe experiments; see unittest/TestGEMath.cpp).
|
||||
// float24s are floats with the low 8 mantissa bits clear.
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "GPU/Math3D.h"
|
||||
|
||||
using namespace Math3D;
|
||||
|
||||
inline float TruncateToFloat24(float f) {
|
||||
uint32_t bits;
|
||||
memcpy(&bits, &f, sizeof(bits));
|
||||
bits &= 0xFFFFFF00;
|
||||
memcpy(&f, &bits, sizeof(f));
|
||||
return f;
|
||||
}
|
||||
|
||||
// A product of two float24s has up to 32 significant bits: form it in a double, and truncate it
|
||||
// there, since rounding it to a float first could round up across the truncation.
|
||||
inline float ProductToFloat24(double d) {
|
||||
uint64_t bits;
|
||||
memcpy(&bits, &d, sizeof(bits));
|
||||
if (((bits >> 52) & 0x7FF) != 0x7FF) {
|
||||
bits &= ~((1ULL << (52 - 15)) - 1);
|
||||
}
|
||||
memcpy(&d, &bits, sizeof(d));
|
||||
return (float)d;
|
||||
}
|
||||
|
||||
// The GE's reciprocal and reciprocal square root (TransformUnit, depth, UVs, lighting).
|
||||
float GERecip(float w);
|
||||
float GERsqrt(float d);
|
||||
|
||||
// How the GE adds two float24s, such as a product and a matrix translation or the viewport center:
|
||||
// the adder has no guard bits, so the smaller term is truncated to the precision of the larger one.
|
||||
// Clearing the low 8 + (exponent difference) bits of each does that, and the float sum is then exact.
|
||||
inline float GEAdd(float a, float b) {
|
||||
uint32_t ba, bb;
|
||||
memcpy(&ba, &a, sizeof(ba));
|
||||
memcpy(&bb, &b, sizeof(bb));
|
||||
const int ea = (ba >> 23) & 0xFF;
|
||||
const int eb = (bb >> 23) & 0xFF;
|
||||
if (ea == 0 || eb == 0) {
|
||||
// Zero (or denormal, which the GE treats as zero) leaves the other term alone.
|
||||
return ea == 0 ? (eb == 0 ? 0.0f : TruncateToFloat24(b)) : TruncateToFloat24(a);
|
||||
}
|
||||
const int emax = std::max(ea, eb);
|
||||
const int na = 8 + emax - ea;
|
||||
const int nb = 8 + emax - eb;
|
||||
ba = na >= 24 ? (ba & 0x80000000) : (ba & ~((1U << na) - 1));
|
||||
bb = nb >= 24 ? (bb & 0x80000000) : (bb & ~((1U << nb) - 1));
|
||||
memcpy(&a, &ba, sizeof(a));
|
||||
memcpy(&b, &bb, sizeof(b));
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// One term of a matrix row as the GE keeps it: the exact product, truncated at a fixed bit weight,
|
||||
// 2^-15 below the inputs' exponents combined. A significand product of 2 or more keeps a 17th bit.
|
||||
struct GERowTerm {
|
||||
int32_t mantissa; // The term is mantissa * 2^lsbExp.
|
||||
int lsbExp;
|
||||
float Value() const { return ldexpf((float)mantissa, lsbExp); }
|
||||
};
|
||||
|
||||
// Zero and denormals (which the GE treats as zero) give a zero term.
|
||||
inline GERowTerm GEProduct(float a, float b) {
|
||||
uint32_t ba, bb;
|
||||
memcpy(&ba, &a, sizeof(ba));
|
||||
memcpy(&bb, &b, sizeof(bb));
|
||||
const int ea = (ba >> 23) & 0xFF;
|
||||
const int eb = (bb >> 23) & 0xFF;
|
||||
if (ea == 0 || eb == 0) {
|
||||
return { 0, INT_MIN };
|
||||
}
|
||||
// The 24-bit significands make a 48-bit product with 46 fraction bits, of which we keep 15.
|
||||
const uint64_t ma = (ba & 0x007FFFFF) | 0x00800000;
|
||||
const uint64_t mb = (bb & 0x007FFFFF) | 0x00800000;
|
||||
const int32_t m = (int32_t)((ma * mb) >> 31);
|
||||
return { ((ba ^ bb) & 0x80000000) ? -m : m, (ea - 127) + (eb - 127) - 15 };
|
||||
}
|
||||
|
||||
// The GE sums a matrix row in one go, with no order: every term is truncated to the bit weight of the
|
||||
// term with the largest one, then they're added exactly, and the result becomes a float24.
|
||||
inline float GERowSum(const GERowTerm *terms, int count) {
|
||||
int lsbExp = INT_MIN;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
lsbExp = std::max(lsbExp, terms[i].lsbExp);
|
||||
}
|
||||
int32_t sum = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const int32_t m = terms[i].mantissa;
|
||||
const int shift = lsbExp - terms[i].lsbExp;
|
||||
if (m != 0 && shift < 32) {
|
||||
sum += m < 0 ? -(-m >> shift) : (m >> shift);
|
||||
}
|
||||
}
|
||||
if (sum == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
return TruncateToFloat24(ldexpf((float)sum, lsbExp));
|
||||
}
|
||||
|
||||
float GEAddFloat24(float a, float b);
|
||||
float GEDot(const Vec3f &a, const Vec3f &b);
|
||||
float GENormalize(Vec3f &v);
|
||||
|
||||
// The reciprocal triangle setup uses for its planes: q ~ 2^(e + 16) / absDet, e = floor(log2(absDet)).
|
||||
int64_t GESetupRecip(uint64_t absDet, int *e);
|
||||
|
||||
// The float-bits log2 the mip level selection uses, in 1/16 (a signed 1.27.4 value): the exponent and
|
||||
// the top 4 mantissa bits, a piecewise linear log2 floored to 1/16 (gpu/probe exp58-60).
|
||||
inline int GELog16(float delta) {
|
||||
union FloatBits {
|
||||
float f;
|
||||
u32 u;
|
||||
};
|
||||
FloatBits f;
|
||||
f.f = delta;
|
||||
int useful = (f.u >> 19) & 0x0FFF;
|
||||
return useful - 127 * 16;
|
||||
}
|
||||
|
||||
// 15 significant bits, truncated: what a UV plane keeps of a lone value (gpu/probe exp83).
|
||||
inline float GETruncateTexCoord(float f) {
|
||||
u32 bits;
|
||||
memcpy(&bits, &f, 4);
|
||||
bits &= 0xFFFFFE00;
|
||||
memcpy(&f, &bits, 4);
|
||||
return f;
|
||||
}
|
||||
|
||||
// u = s * R(q) keeps 24 significant bits, truncated: a float32 product without rounding, not a float24
|
||||
// (gpu/probe exp82: 23 or 24 bits match, 22 and 25 don't).
|
||||
inline float GEUVProduct(double d) {
|
||||
uint64_t bits;
|
||||
memcpy(&bits, &d, sizeof(bits));
|
||||
if (((bits >> 52) & 0x7FF) != 0x7FF) {
|
||||
bits &= ~((1ULL << (52 - 23)) - 1);
|
||||
}
|
||||
memcpy(&d, &bits, sizeof(d));
|
||||
return (float)d;
|
||||
}
|
||||
|
||||
// The lighting pow (gpu/probe exp98, exp102): Mitchell's approximation on the float's bits, with the
|
||||
// product exact and truncated toward zero to units of 16 in the bits. e <= 0 gives 1, and a non-positive v is returned as is.
|
||||
float GELightPow(float v, float e);
|
||||
|
||||
// How the GE scales light by a factor (gpu/probe exp61-63): the light and material colors make an
|
||||
// 8-bit product x = ((2l + 1) * (2m + 1)) >> 10, and each factor (N.L or the specular power, then the
|
||||
// attenuation and spot) becomes an 8-bit s = floor(256 * f), 0 for a negative f, that is expanded like a color:
|
||||
// ((2x + 1) * (2s + 1)) >> 10. A factor of 1 (s = 256) leaves x as it is.
|
||||
inline Vec4<int> GELightColorProduct(const Vec4<int> &lightFactor, const Vec4<int> &materialFactor) {
|
||||
return (lightFactor * materialFactor) >> 10;
|
||||
}
|
||||
|
||||
inline Vec4<int> GELightColorScale(const Vec4<int> &x, float f) {
|
||||
const int s = std::clamp((int)(256.0f * f), 0, 256);
|
||||
return ((x * 2 + Vec4<int>::AssignToAll(1)) * (2 * s + 1)) >> 10;
|
||||
}
|
||||
|
||||
// The alpha of pixel (px, py) of an antialiased line between two points in screen subpixels (gpu/probe
|
||||
// exp112, 1735 of 1744 pixels). Antialiased lines light the same pixels as plain ones, and this replaces the
|
||||
// vertex alpha: 128 - |v|, v the floor of 16 times the pixel center's offset from the line along the minor
|
||||
// axis in subpixels, with the line's minor position from the triangle setup's reciprocal of its length.
|
||||
int GELineCoverageAlpha(int64_t x0, int64_t y0, int64_t x1, int64_t y1, int px, int py);
|
||||
+98
-98
@@ -22,7 +22,9 @@
|
||||
#include "Common/Math/SIMDHeaders.h"
|
||||
#include "GPU/GPUState.h"
|
||||
#include "GPU/Common/TransformCommon.h"
|
||||
#include "GPU/Software/GEMath.h"
|
||||
#include "GPU/Software/Lighting.h"
|
||||
#include "GPU/Software/TransformUnit.h"
|
||||
|
||||
#if PPSSPP_ARCH(SSE2)
|
||||
// For the SSE4 stuff.
|
||||
@@ -45,16 +47,6 @@ static inline Vec3f GetLightVec(const u32 lparams[12], int light) {
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline float pspLightPow(float v, float e) {
|
||||
if (e <= 0.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
if (v > 0.0f) {
|
||||
return PSPLightPow(v, e);
|
||||
}
|
||||
// Negative stays negative, so let's just return the original.
|
||||
return v;
|
||||
}
|
||||
|
||||
static inline Vec4<int> LightColorFactor(const Vec4<int> &expanded, const Vec4<int> &ones) {
|
||||
#if defined(_M_SSE) && !PPSSPP_ARCH(X86)
|
||||
@@ -70,6 +62,8 @@ static inline Vec4<int> LightColorFactor(uint32_t c, const Vec4<int> &ones) {
|
||||
return LightColorFactor(Vec4<int>::FromRGBA(c), ones);
|
||||
}
|
||||
|
||||
// Whether a color factor (2c + 1 per channel, see LightColorFactor) has any channel above zero. The alpha
|
||||
// lane is 1 (light colors have no alpha), so the sum is above 4 exactly then, and so is the maximum above 1.
|
||||
static inline bool IsLargerThanHalf(const Vec4<int> &v) {
|
||||
#if defined(_M_SSE) && !PPSSPP_ARCH(X86)
|
||||
__m128i add23 = _mm_add_epi32(v.ivec, _mm_shuffle_epi32(v.ivec, _MM_SHUFFLE(3, 2, 3, 2)));
|
||||
@@ -78,12 +72,9 @@ static inline bool IsLargerThanHalf(const Vec4<int> &v) {
|
||||
#elif PPSSPP_ARCH(ARM64_NEON)
|
||||
int32x2_t add02 = vpmax_s32(vget_low_s32(v.ivec), vget_high_s32(v.ivec));
|
||||
int32x2_t add1 = vpmax_s32(add02, add02);
|
||||
return vget_lane_s32(add1, 0) > 4;
|
||||
return vget_lane_s32(add1, 0) > 1;
|
||||
#else
|
||||
bool larger = false;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
larger = v[i] > 1;
|
||||
return larger;
|
||||
return v[0] > 1 || v[1] > 1 || v[2] > 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -126,7 +117,8 @@ void ComputeState(State *state, bool hasColor0) {
|
||||
lstate.pos = GetLightVec(gstate.lpos, light);
|
||||
lstate.directional = gstate.isDirectionalLight(light);
|
||||
if (lstate.directional) {
|
||||
lstate.pos.NormalizeOr001();
|
||||
// A zero direction stays zero: no diffuse, and the half vector is just the eye's (gpu/probe exp164).
|
||||
GENormalize(lstate.pos);
|
||||
} else {
|
||||
lstate.att = GetLightVec(gstate.latt, light);
|
||||
anyNonDirectional = true;
|
||||
@@ -134,8 +126,17 @@ void ComputeState(State *state, bool hasColor0) {
|
||||
|
||||
lstate.spot = gstate.isSpotLight(light);
|
||||
if (lstate.spot) {
|
||||
// The direction isn't normalized: the dot with L is scaled by its rsqrt (gpu/probe exp100).
|
||||
lstate.spotDir = GetLightVec(gstate.ldir, light);
|
||||
lstate.spotDir.Normalize();
|
||||
// A component with exponent 255 (inf or NaN) acts as the largest value of its sign, so after the
|
||||
// scaling below the finite ones vanish next to it (gpu/commands/light: -NAN and -INFINITY light
|
||||
// like (-1, -1, -1), NAN and INFINITY like (1, 1, 1)).
|
||||
if (!std::isfinite(lstate.spotDir.x) || !std::isfinite(lstate.spotDir.y) || !std::isfinite(lstate.spotDir.z)) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
lstate.spotDir[i] = std::isfinite(lstate.spotDir[i]) ? 0.0f : (std::signbit(lstate.spotDir[i]) ? -1.0f : 1.0f);
|
||||
}
|
||||
const float dirLen2 = GEDot(lstate.spotDir, lstate.spotDir);
|
||||
lstate.spotDirRsqrt = dirLen2 > 0.0f && std::isfinite(dirLen2) ? GERsqrt(dirLen2) : 0.0f;
|
||||
lstate.spotCutoff = getFloat24(gstate.lcutoff[light]);
|
||||
if (std::isnan(lstate.spotCutoff) && std::signbit(lstate.spotCutoff))
|
||||
lstate.spotCutoff = 0.0f;
|
||||
@@ -194,60 +195,51 @@ void ComputeState(State *state, bool hasColor0) {
|
||||
state->usesWorldNormal = gstate.getUVGenMode() == GE_TEXMAP_ENVIRONMENT_MAP || anyDiffuse || anySpecular;
|
||||
}
|
||||
|
||||
void GenerateLightST(VertexData &vertex, const WorldCoords &worldpos, const WorldCoords &worldnormal, const Vec3f &viewDir) {
|
||||
// PSPShadeMapCoord in the GE's arithmetic, with L as lighting computes it.
|
||||
// v . N for the unnormalized normal N, scaled by its reciprocal length (the spot direction works the same).
|
||||
static inline float GENormalDot(const Vec3f &v, const Vec3f &n, float nRsqrt) {
|
||||
return ProductToFloat24((double)GEDot(v, n) * nRsqrt);
|
||||
}
|
||||
|
||||
// The vector from a vertex to a point light as the GE computes it (gpu/probe exp153-158): the light position
|
||||
// minus the world translation (with the GE's adder), minus the model position times the world matrix, summed
|
||||
// like a matrix row. No world space position is formed; lighting with one in float24 loses the products' low
|
||||
// bits to a large translation (Syphon Filter #13568).
|
||||
static Vec3f GELightVector(const Vec3f &lpos, const Vec3f &modelpos) {
|
||||
const float *m = gstate.worldMatrix;
|
||||
Vec3f L;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const GERowTerm terms[4] = {
|
||||
GEProduct(1.0f, GEAddFloat24(lpos[i], -TruncateToFloat24(m[9 + i]))),
|
||||
GEProduct(TruncateToFloat24(modelpos.x), -m[i]),
|
||||
GEProduct(TruncateToFloat24(modelpos.y), -m[3 + i]),
|
||||
GEProduct(TruncateToFloat24(modelpos.z), -m[6 + i]),
|
||||
};
|
||||
L[i] = GERowSum(terms, 4);
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
static float GEShadeMapCoord(int l, const Vec3f &modelpos, const WorldCoords &worldnormal, float normalRsqrt, const Vec3f &viewDir) {
|
||||
Vec3f L(getFloat24(gstate.lpos[l * 3]), getFloat24(gstate.lpos[l * 3 + 1]), getFloat24(gstate.lpos[l * 3 + 2]));
|
||||
if (gstate.getLightType(l) != GE_LIGHTTYPE_DIRECTIONAL)
|
||||
L = GELightVector(L, modelpos);
|
||||
GENormalize(L);
|
||||
if (gstate.isUsingSpecularLight(l)) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
L[i] = GEAddFloat24(L[i], viewDir[i]);
|
||||
GENormalize(L);
|
||||
}
|
||||
return GEAddFloat24(GENormalDot(L, worldnormal, normalRsqrt), 1.0f) * 0.5f;
|
||||
}
|
||||
|
||||
void GenerateLightST(VertexData &vertex, const Vec3f &modelpos, const WorldCoords &worldnormal, float normalRsqrt, const Vec3f &viewDir) {
|
||||
// Always calculate texture coords from lighting results if environment mapping is active
|
||||
// This should be done even if lighting is disabled altogether.
|
||||
vertex.texturecoords.s() = PSPShadeMapCoord(gstate.getUVLS0(), worldpos, worldnormal, viewDir);
|
||||
vertex.texturecoords.t() = PSPShadeMapCoord(gstate.getUVLS1(), worldpos, worldnormal, viewDir);
|
||||
vertex.texturecoords.s() = GEShadeMapCoord(gstate.getUVLS0(), modelpos, worldnormal, normalRsqrt, viewDir);
|
||||
vertex.texturecoords.t() = GEShadeMapCoord(gstate.getUVLS1(), modelpos, worldnormal, normalRsqrt, viewDir);
|
||||
}
|
||||
|
||||
#if defined(_M_SSE)
|
||||
#if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)
|
||||
[[gnu::target("sse4.1")]]
|
||||
#endif
|
||||
static inline int LightCeilSSE4(float f) {
|
||||
__m128 v = _mm_set_ss(f);
|
||||
// This isn't terribly fast, but seems to be better than calling ceilf().
|
||||
return _mm_cvt_ss2si(_mm_ceil_ss(v, v));
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER)
|
||||
[[gnu::target("sse4.1")]]
|
||||
#endif
|
||||
static inline __m128i LightColorScaleBy512SSE4(__m128i factor, __m128i color, __m128i scale) {
|
||||
// We can use 16-bit multiply here (faster than 32-bit multiply) since our top bits are zero.
|
||||
__m128i result18 = _mm_madd_epi16(factor, color);
|
||||
// But now with 18 bits, we need a full multiply.
|
||||
__m128i multiplied = _mm_mullo_epi32(result18, scale);
|
||||
return _mm_srai_epi32(multiplied, 10 + 9);
|
||||
}
|
||||
#endif
|
||||
|
||||
template <bool useSSE4>
|
||||
static inline int LightCeil(float f) {
|
||||
#if defined(_M_SSE)
|
||||
if (useSSE4)
|
||||
return LightCeilSSE4(f);
|
||||
#elif PPSSPP_ARCH(ARM64_NEON)
|
||||
return vcvtps_s32_f32(f);
|
||||
#endif
|
||||
return (int)ceilf(f);
|
||||
}
|
||||
|
||||
template <bool useSSE4>
|
||||
static Vec4<int> LightColorScaleBy512(const Vec4<int> &factor, const Vec4<int> &color, int scale) {
|
||||
// We multiply s9 * s9 * s9, resulting in s27, then shift off 19 to get 8-bit.
|
||||
// The reason all factors are s9 is to account for rounding.
|
||||
// Also note that all values are positive, so can be treated as unsigned.
|
||||
#if defined(_M_SSE) && !PPSSPP_ARCH(X86)
|
||||
if (useSSE4)
|
||||
return LightColorScaleBy512SSE4(factor.ivec, color.ivec, _mm_set1_epi32(scale));
|
||||
#elif PPSSPP_ARCH(ARM64_NEON)
|
||||
int32x4_t multiplied = vmulq_n_s32(vmulq_s32(factor.ivec, color.ivec), scale);
|
||||
return vshrq_n_s32(multiplied, 10 + 9);
|
||||
#endif
|
||||
return (factor * color * scale) >> (10 + 9);
|
||||
}
|
||||
|
||||
static inline void LightColorSum(Vec4<int> &sum, const Vec4<int> &src) {
|
||||
#if defined(_M_SSE) && !PPSSPP_ARCH(X86)
|
||||
@@ -277,7 +269,7 @@ static inline float Dot33(const Vec3f &a, const Vec3f &b) {
|
||||
}
|
||||
|
||||
template <bool useSSE4>
|
||||
static void ProcessSIMD(VertexData &vertex, const WorldCoords &worldpos, const WorldCoords &worldnormal, const State &state) {
|
||||
static void ProcessSIMD(VertexData &vertex, const Vec3f &modelpos, const WorldCoords &worldnormal, float normalRsqrt, const State &state) {
|
||||
// Lighting blending rounds using the half offset method (like alpha blend.)
|
||||
Vec4<int> colorFactor;
|
||||
if (state.colorForAmbient || state.colorForDiffuse || state.colorForSpecular) {
|
||||
@@ -301,77 +293,85 @@ static void ProcessSIMD(VertexData &vertex, const WorldCoords &worldpos, const W
|
||||
// L = vector from vertex to light source
|
||||
// TODO: Should transfer the light positions to world/view space for these calculations?
|
||||
Vec3<float> L = lstate.pos;
|
||||
float attspot = 1.0f;
|
||||
// Attenuation and spot each scale the light's colors as their own 8-bit factor (gpu/probe exp100).
|
||||
float att = 1.0f;
|
||||
float spot = 1.0f;
|
||||
if (!lstate.directional) {
|
||||
L -= worldpos;
|
||||
L = GELightVector(L, modelpos);
|
||||
// TODO: Should this normalize (0, 0, 0) to (0, 0, 1)?
|
||||
float d = L.NormalizeOr001();
|
||||
// The quadratic term takes the squared length from the normalization, not d * d (gpu/probe exp63).
|
||||
const float d2 = GEDot(L, L);
|
||||
float d = GENormalize(L);
|
||||
if (d == 0.0f)
|
||||
L = Vec3f(0.0f, 0.0f, 1.0f);
|
||||
|
||||
float att = 1.0f / Dot33(lstate.att, Vec3f(1.0f, d, d * d));
|
||||
const float den = GEDot(lstate.att, Vec3f(1.0f, d, d2));
|
||||
att = den > 0.0f ? GERecip(den) : 0.0f;
|
||||
if (!(att > 0.0f))
|
||||
att = 0.0f;
|
||||
else if (att > 1.0f)
|
||||
att = 1.0f;
|
||||
attspot = att;
|
||||
}
|
||||
|
||||
if (lstate.spot) {
|
||||
float rawSpot = Dot33(lstate.spotDir, L);
|
||||
float rawSpot = ProductToFloat24((double)GEDot(lstate.spotDir, L) * lstate.spotDirRsqrt);
|
||||
if (std::isnan(rawSpot))
|
||||
rawSpot = std::signbit(rawSpot) ? 0.0f : 1.0f;
|
||||
|
||||
float spot = 1.0f;
|
||||
if (rawSpot >= lstate.spotCutoff) {
|
||||
spot = pspLightPow(rawSpot, lstate.spotExp);
|
||||
spot = GELightPow(rawSpot, lstate.spotExp);
|
||||
if (std::isnan(spot))
|
||||
spot = 0.0f;
|
||||
} else {
|
||||
spot = 0.0f;
|
||||
}
|
||||
|
||||
attspot *= spot;
|
||||
}
|
||||
auto scaleAttSpot = [&](Vec4<int> c) {
|
||||
if (att < 1.0f)
|
||||
c = GELightColorScale(c, att);
|
||||
if (spot < 1.0f)
|
||||
c = GELightColorScale(c, spot);
|
||||
return c;
|
||||
};
|
||||
|
||||
// ambient lighting
|
||||
if (lstate.ambient) {
|
||||
int attspot512 = (int)LightCeil<useSSE4>(256 * 2 * attspot + 1);
|
||||
if (attspot512 > 512)
|
||||
attspot512 = 512;
|
||||
Vec4<int> lambient = LightColorScaleBy512<useSSE4>(lstate.ambientColorFactor, mac, attspot512);
|
||||
Vec4<int> lambient = GELightColorProduct(lstate.ambientColorFactor, mac);
|
||||
lambient = scaleAttSpot(lambient);
|
||||
LightColorSum(final_color, lambient);
|
||||
}
|
||||
|
||||
// diffuse lighting
|
||||
float diffuse_factor;
|
||||
if (lstate.diffuse || lstate.specular) {
|
||||
diffuse_factor = Dot33(L, worldnormal);
|
||||
diffuse_factor = GENormalDot(L, worldnormal, normalRsqrt);
|
||||
if (lstate.poweredDiffuse) {
|
||||
diffuse_factor = pspLightPow(diffuse_factor, state.specularExp);
|
||||
diffuse_factor = GELightPow(diffuse_factor, state.specularExp);
|
||||
}
|
||||
}
|
||||
|
||||
if (lstate.diffuse && diffuse_factor > 0.0f) {
|
||||
int diffuse_attspot = (int)LightCeil<useSSE4>(256 * 2 * attspot * diffuse_factor + 1);
|
||||
if (diffuse_attspot > 512)
|
||||
diffuse_attspot = 512;
|
||||
Vec4<int> mdc = state.colorForDiffuse ? colorFactor : state.material.diffuseColorFactor;
|
||||
Vec4<int> ldiffuse = LightColorScaleBy512<useSSE4>(lstate.diffuseColorFactor, mdc, diffuse_attspot);
|
||||
Vec4<int> ldiffuse = GELightColorScale(GELightColorProduct(lstate.diffuseColorFactor, mdc), diffuse_factor);
|
||||
ldiffuse = scaleAttSpot(ldiffuse);
|
||||
LightColorSum(final_color, ldiffuse);
|
||||
}
|
||||
|
||||
if (lstate.specular && diffuse_factor >= 0.0f) {
|
||||
Vec3<float> H = L + state.viewDir;
|
||||
Vec3<float> H;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
H[i] = GEAddFloat24(L[i], state.viewDir[i]);
|
||||
if (GENormalize(H) == 0.0f)
|
||||
H = Vec3f(0.0f, 0.0f, 1.0f);
|
||||
|
||||
float specular_factor = Dot33(H.NormalizedOr001(useSSE4), worldnormal);
|
||||
specular_factor = pspLightPow(specular_factor, state.specularExp);
|
||||
float specular_factor = GENormalDot(H, worldnormal, normalRsqrt);
|
||||
specular_factor = GELightPow(specular_factor, state.specularExp);
|
||||
|
||||
if (specular_factor > 0.0f) {
|
||||
int specular_attspot = (int)LightCeil<useSSE4>(256 * 2 * attspot * specular_factor + 1);
|
||||
if (specular_attspot > 512)
|
||||
specular_attspot = 512;
|
||||
|
||||
Vec4<int> msc = state.colorForSpecular ? colorFactor : state.material.specularColorFactor;
|
||||
Vec4<int> lspecular = LightColorScaleBy512<useSSE4>(lstate.specularColorFactor, msc, specular_attspot);
|
||||
Vec4<int> lspecular = GELightColorScale(GELightColorProduct(lstate.specularColorFactor, msc), specular_factor);
|
||||
lspecular = scaleAttSpot(lspecular);
|
||||
LightColorSum(specular_color, lspecular);
|
||||
}
|
||||
}
|
||||
@@ -388,14 +388,14 @@ static void ProcessSIMD(VertexData &vertex, const WorldCoords &worldpos, const W
|
||||
}
|
||||
}
|
||||
|
||||
void Process(VertexData &vertex, const WorldCoords &worldpos, const WorldCoords &worldnormal, const State &state) {
|
||||
void Process(VertexData &vertex, const Vec3f &modelpos, const WorldCoords &worldnormal, float normalRsqrt, const State &state) {
|
||||
#ifdef _M_SSE
|
||||
if (cpu_info.bSSE4_1) {
|
||||
ProcessSIMD<true>(vertex, worldpos, worldnormal, state);
|
||||
ProcessSIMD<true>(vertex, modelpos, worldnormal, normalRsqrt, state);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ProcessSIMD<false>(vertex, worldpos, worldnormal, state);
|
||||
ProcessSIMD<false>(vertex, modelpos, worldnormal, normalRsqrt, state);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -27,6 +27,7 @@ struct State {
|
||||
Vec3f pos;
|
||||
Vec3f att;
|
||||
Vec3f spotDir;
|
||||
float spotDirRsqrt;
|
||||
float spotCutoff;
|
||||
float spotExp;
|
||||
|
||||
@@ -68,7 +69,8 @@ struct State {
|
||||
|
||||
void ComputeState(State *state, bool hasColor0);
|
||||
|
||||
void GenerateLightST(VertexData &vertex, const WorldCoords &worldpos, const WorldCoords &worldnormal, const Vec3f &viewDir);
|
||||
void Process(VertexData &vertex, const WorldCoords &worldpos, const WorldCoords &worldnormal, const State &state);
|
||||
// worldnormal isn't normalized; normalRsqrt is its reciprocal length.
|
||||
void GenerateLightST(VertexData &vertex, const Vec3f &modelpos, const WorldCoords &worldnormal, float normalRsqrt, const Vec3f &viewDir);
|
||||
void Process(VertexData &vertex, const Vec3f &modelpos, const WorldCoords &worldnormal, float normalRsqrt, const State &state);
|
||||
|
||||
}
|
||||
+656
-104
File diff suppressed because it is too large.
Load diff
@@ -75,6 +75,8 @@ struct RasterizerState {
|
||||
uint16_t texbufw[8]{};
|
||||
const u8 *texptr[8]{};
|
||||
float textureLodSlope;
|
||||
// TEXFLUSHes before this state (BinManager::SelfTextureSnapshot).
|
||||
uint32_t texFlushGen = 0;
|
||||
RasterizerStateFlags flags = RasterizerStateFlags::NONE;
|
||||
RasterizerStateFlags lastFlags = RasterizerStateFlags::INVALID;
|
||||
|
||||
@@ -90,6 +92,8 @@ struct RasterizerState {
|
||||
bool magFilt : 1;
|
||||
bool antialiasLines : 1;
|
||||
bool textureProj : 1;
|
||||
// Textures from the buffer it draws to (BinManager::SelfTextureSnapshot).
|
||||
bool selfTexture : 1;
|
||||
};
|
||||
|
||||
#if defined(SOFTGPU_MEMORY_TAGGING_DETAILED) || defined(SOFTGPU_MEMORY_TAGGING_BASIC)
|
||||
|
||||
@@ -361,21 +361,15 @@ void DrawSprite(const VertexData &v0, const VertexData &v1, const BinCoords &ran
|
||||
if (pixelID.applyDepthRange && (z < pixelID.cached.minz || z > pixelID.cached.maxz))
|
||||
return;
|
||||
|
||||
bool isWhite = v1.color0 == 0xFFFFFFFF;
|
||||
// White doesn't change the texel, unless color doubling doubles it (gpu/probe exp53).
|
||||
bool isWhite = v1.color0 == 0xFFFFFFFF && !samplerID.useColorDoubling;
|
||||
|
||||
if (state.enableTextures) {
|
||||
// 1:1 (but with mirror support) texture mapping!
|
||||
int s_start = v0.texturecoords.x;
|
||||
int t_start = v0.texturecoords.y;
|
||||
// 1:1 (but with mirror support) texture mapping! The texel is the one at the first pixel's center.
|
||||
int ds = v1.texturecoords.x > v0.texturecoords.x ? 1 : -1;
|
||||
int dt = v1.texturecoords.y > v0.texturecoords.y ? 1 : -1;
|
||||
|
||||
if (ds < 0) {
|
||||
s_start += ds;
|
||||
}
|
||||
if (dt < 0) {
|
||||
t_start += dt;
|
||||
}
|
||||
int s_start = (int)floorf(v0.texturecoords.x + 0.5f * ds);
|
||||
int t_start = (int)floorf(v0.texturecoords.y + 0.5f * dt);
|
||||
|
||||
// First clip the right and bottom sides, since we don't need to adjust the deltas.
|
||||
if (pos1.x > scissorBR.x) pos1.x = scissorBR.x + 1;
|
||||
@@ -390,7 +384,8 @@ void DrawSprite(const VertexData &v0, const VertexData &v1, const BinCoords &ran
|
||||
pos0.y = scissorTL.y;
|
||||
}
|
||||
|
||||
if (UseDrawSinglePixel(pixelID) && (samplerID.TexFunc() == GE_TEXFUNC_MODULATE || samplerID.TexFunc() == GE_TEXFUNC_REPLACE) && samplerID.useTextureAlpha) {
|
||||
const bool fastFunc = samplerID.TexFunc() == GE_TEXFUNC_MODULATE || (samplerID.TexFunc() == GE_TEXFUNC_REPLACE && !samplerID.useColorDoubling);
|
||||
if (UseDrawSinglePixel(pixelID) && fastFunc && samplerID.useTextureAlpha) {
|
||||
if (isWhite || samplerID.TexFunc() == GE_TEXFUNC_REPLACE) {
|
||||
DrawSpriteTex<true>(pos0, pos1, s_start, t_start, ds, dt, v1.color0, state, fetchFunc);
|
||||
} else {
|
||||
@@ -536,6 +531,8 @@ bool RectangleFastPath(const VertexData &v0, const VertexData &v1, BinManager &b
|
||||
bool coord_check = true;
|
||||
if (state.enableTextures) {
|
||||
state_check = state_check && NoClampOrWrap(state, v0.texturecoords.uv()) && NoClampOrWrap(state, v1.texturecoords.uv());
|
||||
// DrawSprite samples texel centers, but bilinear 1:1 sprites don't land on them (gpu/probe exp87).
|
||||
state_check = state_check && !state.minFilt && !state.magFilt;
|
||||
coord_check = (xdiff == udiff || xdiff == -udiff) && (ydiff == vdiff || ydiff == -vdiff);
|
||||
}
|
||||
// This doesn't work well with offset drawing, see #15876. Through never has a subpixel offset.
|
||||
@@ -600,6 +597,26 @@ static bool AreCoordsRectangleCompatible(const RasterizerState &state, const Cli
|
||||
return true;
|
||||
}
|
||||
|
||||
// Whether triangles forming a rectangle can be drawn as a sprite with the same result. A rectangle drawn
|
||||
// as triangles samples pixel centers, so its first column is (x + 7) >> 4; a sprite's is (x + 6) >> 4
|
||||
// (gpu/probe exp30). They differ only for a left edge at 9/16 into a pixel, where the triangles must stay
|
||||
// triangles (NBA 2K13's menu boxes). Rows and the far edges agree. Each triangle also has its own UV
|
||||
// planes, which a sprite's corners don't reproduce, so textured ones only qualify in through mode with
|
||||
// nearest filtering (Gears of Destiny's text boxes), and 1:1, where every sample is half a texel from an
|
||||
// edge: a scaled one can land right on a texel edge, where the planes' anchors decide the side (Tokiden's
|
||||
// 480x33 bar over 280x20 texels, gpu/probe exp167).
|
||||
bool RectangleMatchesTriangles(const RasterizerState &state, const VertexData &a, const VertexData &b) {
|
||||
if (state.enableTextures) {
|
||||
if (!state.throughMode || state.minFilt || state.magFilt)
|
||||
return false;
|
||||
const float du = std::fabs(b.texturecoords.x - a.texturecoords.x) * (float)SCREEN_SCALE_FACTOR;
|
||||
const float dv = std::fabs(b.texturecoords.y - a.texturecoords.y) * (float)SCREEN_SCALE_FACTOR;
|
||||
if (du != (float)std::abs(b.screenpos.x - a.screenpos.x) || dv != (float)std::abs(b.screenpos.y - a.screenpos.y))
|
||||
return false;
|
||||
}
|
||||
return (std::min(a.screenpos.x, b.screenpos.x) & (SCREEN_SCALE_FACTOR - 1)) != 9;
|
||||
}
|
||||
|
||||
bool DetectRectangleFromStrip(const RasterizerState &state, const ClipVertexData data[4], int *tlIndex, int *brIndex) {
|
||||
if (!IsCoordRectangleCompatible(state, data[0]))
|
||||
return false;
|
||||
@@ -782,6 +799,11 @@ bool DetectRectangleThroughModeSlices(const RasterizerState &state, const ClipVe
|
||||
if (br1.x == tl2.x && tl1.x < br1.x && tl2.x < br2.x) {
|
||||
if (!state.enableTextures)
|
||||
return true;
|
||||
// Each sprite gets its own UV planes, and only a power of two area makes their gradient exact,
|
||||
// so bilinear samples land differently in the pieces than in one joined sprite (gpu/probe
|
||||
// exp163, ULJM05302's track map).
|
||||
if (state.minFilt || state.magFilt)
|
||||
return false;
|
||||
|
||||
const auto &textl1 = data[0].v.texturecoords, &texbr1 = data[1].v.texturecoords;
|
||||
const auto &textl2 = data[2].v.texturecoords, &texbr2 = data[3].v.texturecoords;
|
||||
|
||||
@@ -24,4 +24,5 @@ namespace Rasterizer {
|
||||
bool DetectRectangleFromFan(const RasterizerState &state, const ClipVertexData *data, int *tlIndex, int *brIndex);
|
||||
bool DetectRectangleFromPair(const RasterizerState &state, const ClipVertexData data[6], int *tlIndex, int *brIndex);
|
||||
bool DetectRectangleThroughModeSlices(const RasterizerState &state, const ClipVertexData data[4]);
|
||||
bool RectangleMatchesTriangles(const RasterizerState &state, const VertexData &a, const VertexData &b);
|
||||
}
|
||||
+33
-21
@@ -120,7 +120,7 @@ void SamplerJitCache::Clear() {
|
||||
const10Low_ = nullptr;
|
||||
const10All8_ = nullptr;
|
||||
|
||||
constWidthHeight256f_ = nullptr;
|
||||
constWidthHeight16f_ = nullptr;
|
||||
constWidthMinus1i_ = nullptr;
|
||||
constHeightMinus1i_ = nullptr;
|
||||
|
||||
@@ -289,7 +289,9 @@ static inline int GetPixelDataOffset(uint32_t row_pitch_pixels, uint32_t u, uint
|
||||
}
|
||||
|
||||
static inline u32 LookupColor(unsigned int index, unsigned int level, const SamplerID &samplerID) {
|
||||
const int clutSharingOffset = samplerID.useSharedClut ? 0 : level * 16;
|
||||
int clutSharingOffset = 0;
|
||||
if (!samplerID.useSharedClut)
|
||||
clutSharingOffset = samplerID.TexFmt() == GE_TFMT_CLUT4 ? level * 16 : (level & 1) * 256;
|
||||
|
||||
switch (samplerID.ClutFmt()) {
|
||||
case GE_CMODE_16BIT_BGR5650:
|
||||
@@ -390,7 +392,7 @@ inline static Nearest4 SOFTRAST_CALL SampleNearest(const int u[N], const int v[N
|
||||
for (int i = 0; i < N; ++i) {
|
||||
const u8 *src = srcptr + GetPixelDataOffset<8>(texbufw, u[i], v[i], samplerID.swizzle);
|
||||
u8 val = *src;
|
||||
res.v[i] = LookupColor(TransformClutIndex(val, samplerID), 0, samplerID);
|
||||
res.v[i] = LookupColor(TransformClutIndex(val, samplerID), level, samplerID);
|
||||
}
|
||||
return res;
|
||||
|
||||
@@ -398,7 +400,6 @@ inline static Nearest4 SOFTRAST_CALL SampleNearest(const int u[N], const int v[N
|
||||
for (int i = 0; i < N; ++i) {
|
||||
const u8 *src = srcptr + GetPixelDataOffset<4>(texbufw, u[i], v[i], samplerID.swizzle);
|
||||
u8 val = (u[i] & 1) ? (src[0] >> 4) : (src[0] & 0xF);
|
||||
// Only CLUT4 uses separate mipmap palettes.
|
||||
res.v[i] = LookupColor(TransformClutIndex(val, samplerID), level, samplerID);
|
||||
}
|
||||
return res;
|
||||
@@ -467,15 +468,22 @@ static inline void ApplyTexelClamp(int out_u[N], int out_v[N], const int u[N], c
|
||||
}
|
||||
}
|
||||
|
||||
// A texel coordinate in 1/16 texel, truncated. Out of int range it's INT_MIN, as the x86 sampler JIT's
|
||||
// conversion gives (gpu/probe exp5: s >= 2^18 on a 512 wide repeating texture reads texel 0 on a PSP).
|
||||
static inline int TexelFixed(float f) {
|
||||
return f >= -2147483648.0f && f < 2147483648.0f ? (int)f : INT_MIN;
|
||||
}
|
||||
|
||||
static inline void GetTexelCoordinates(int level, float s, float t, int &out_u, int &out_v, const SamplerID &samplerID) {
|
||||
int width = samplerID.cached.sizes[level].w;
|
||||
int height = samplerID.cached.sizes[level].h;
|
||||
|
||||
int base_u = (int)(s * width * 256.0f);
|
||||
int base_v = (int)(t * height * 256.0f);
|
||||
// The GE truncates to 1/16 texel, toward zero (gpu/probe exp57).
|
||||
int base_u = TexelFixed(s * width * 16.0f);
|
||||
int base_v = TexelFixed(t * height * 16.0f);
|
||||
|
||||
base_u >>= 8;
|
||||
base_v >>= 8;
|
||||
base_u >>= 4;
|
||||
base_v >>= 4;
|
||||
|
||||
ApplyTexelClamp<1>(&out_u, &out_v, &base_u, &base_v, width, height, samplerID);
|
||||
}
|
||||
@@ -693,9 +701,10 @@ static inline Vec4IntResult SOFTRAST_CALL ApplyTexelClampQuadT(bool clamp, int v
|
||||
static inline Vec4IntResult SOFTRAST_CALL GetTexelCoordinatesQuadS(int level, float in_s, int &frac_u, const SamplerID &samplerID) {
|
||||
int width = samplerID.cached.sizes[level].w;
|
||||
|
||||
int base_u = (int)(in_s * width * 256) - 128;
|
||||
frac_u = (int)(base_u >> 4) & 0x0F;
|
||||
base_u >>= 8;
|
||||
// The GE truncates to 1/16 texel, toward zero (gpu/probe exp57).
|
||||
int base_u = TexelFixed(in_s * width * 16) - 8;
|
||||
frac_u = base_u & 0x0F;
|
||||
base_u >>= 4;
|
||||
|
||||
// Need to generate and individually wrap/clamp the four sample coordinates. Ugh.
|
||||
return ApplyTexelClampQuadS(samplerID.clampS, base_u, width);
|
||||
@@ -704,9 +713,9 @@ static inline Vec4IntResult SOFTRAST_CALL GetTexelCoordinatesQuadS(int level, fl
|
||||
static inline Vec4IntResult SOFTRAST_CALL GetTexelCoordinatesQuadT(int level, float in_t, int &frac_v, const SamplerID &samplerID) {
|
||||
int height = samplerID.cached.sizes[level].h;
|
||||
|
||||
int base_v = (int)(in_t * height * 256) - 128;
|
||||
frac_v = (int)(base_v >> 4) & 0x0F;
|
||||
base_v >>= 8;
|
||||
int base_v = TexelFixed(in_t * height * 16) - 8;
|
||||
frac_v = base_v & 0x0F;
|
||||
base_v >>= 4;
|
||||
|
||||
// Need to generate and individually wrap/clamp the four sample coordinates. Ugh.
|
||||
return ApplyTexelClampQuadT(samplerID.clampT, base_v, height);
|
||||
@@ -730,12 +739,14 @@ static Vec4IntResult SOFTRAST_CALL SampleLinearLevel(float s, float t, const u8
|
||||
__m128i mul_u = _mm_set1_epi16(frac_u);
|
||||
mul_u = _mm_xor_si128(mul_u, _mm_setr_epi16(0xF, 0xF, 0xF, 0xF, 0x0, 0x0, 0x0, 0x0));
|
||||
mul_u = _mm_add_epi16(mul_u, _mm_setr_epi16(0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0));
|
||||
// Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52).
|
||||
top = _mm_mullo_epi16(top, mul_u);
|
||||
bot = _mm_mullo_epi16(bot, mul_u);
|
||||
top = _mm_srli_epi16(_mm_add_epi16(top, _mm_shuffle_epi32(top, _MM_SHUFFLE(3, 2, 3, 2))), 4);
|
||||
bot = _mm_srli_epi16(_mm_add_epi16(bot, _mm_shuffle_epi32(bot, _MM_SHUFFLE(3, 2, 3, 2))), 4);
|
||||
top = _mm_mullo_epi16(top, _mm_set1_epi16(0x10 - frac_v));
|
||||
bot = _mm_mullo_epi16(bot, _mm_set1_epi16(frac_v));
|
||||
__m128i sum = _mm_add_epi16(top, bot);
|
||||
sum = _mm_mullo_epi16(sum, mul_u);
|
||||
sum = _mm_add_epi16(sum, _mm_shuffle_epi32(sum, _MM_SHUFFLE(3, 2, 3, 2)));
|
||||
sum = _mm_srli_epi16(sum, 8);
|
||||
__m128i sum = _mm_srli_epi16(_mm_add_epi16(top, bot), 4);
|
||||
sum = _mm_unpacklo_epi16(sum, zero);
|
||||
return sum;
|
||||
#else
|
||||
@@ -743,9 +754,10 @@ static Vec4IntResult SOFTRAST_CALL SampleLinearLevel(float s, float t, const u8
|
||||
Vec4<int> texcolor_tr = Vec4<int>::FromRGBA(c.v[1]);
|
||||
Vec4<int> texcolor_bl = Vec4<int>::FromRGBA(c.v[2]);
|
||||
Vec4<int> texcolor_br = Vec4<int>::FromRGBA(c.v[3]);
|
||||
Vec4<int> top = texcolor_tl * (0x10 - frac_u) + texcolor_tr * frac_u;
|
||||
Vec4<int> bot = texcolor_bl * (0x10 - frac_u) + texcolor_br * frac_u;
|
||||
return ToVec4IntResult((top * (0x10 - frac_v) + bot * frac_v) >> (4 + 4));
|
||||
// Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52).
|
||||
Vec4<int> top = (texcolor_tl * (0x10 - frac_u) + texcolor_tr * frac_u) >> 4;
|
||||
Vec4<int> bot = (texcolor_bl * (0x10 - frac_u) + texcolor_br * frac_u) >> 4;
|
||||
return ToVec4IntResult((top * (0x10 - frac_v) + bot * frac_v) >> 4);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ private:
|
||||
int stackUV1Offset_ = 0;
|
||||
#endif
|
||||
|
||||
const u8 *constWidthHeight256f_ = nullptr;
|
||||
const u8 *constWidthHeight16f_ = nullptr;
|
||||
const u8 *constWidthMinus1i_ = nullptr;
|
||||
const u8 *constHeightMinus1i_ = nullptr;
|
||||
const u8 *constUNext_ = nullptr;
|
||||
|
||||
+93
-82
@@ -924,18 +924,18 @@ void SamplerJitCache::WriteConstantPool(const SamplerID &id) {
|
||||
|
||||
// These are unique to the sampler ID.
|
||||
if (!id.hasAnyMips) {
|
||||
float w256f = (1 << id.width0Shift) * 256;
|
||||
float h256f = (1 << id.height0Shift) * 256;
|
||||
constWidthHeight256f_ = AlignCode16();
|
||||
Write32(*(uint32_t *)&w256f);
|
||||
Write32(*(uint32_t *)&h256f);
|
||||
Write32(*(uint32_t *)&w256f);
|
||||
Write32(*(uint32_t *)&h256f);
|
||||
float w16f = (1 << id.width0Shift) * 16;
|
||||
float h16f = (1 << id.height0Shift) * 16;
|
||||
constWidthHeight16f_ = AlignCode16();
|
||||
Write32(*(uint32_t *)&w16f);
|
||||
Write32(*(uint32_t *)&h16f);
|
||||
Write32(*(uint32_t *)&w16f);
|
||||
Write32(*(uint32_t *)&h16f);
|
||||
|
||||
WriteDynamicConst4x32(constWidthMinus1i_, id.width0Shift > 9 ? 511 : (1 << id.width0Shift) - 1);
|
||||
WriteDynamicConst4x32(constHeightMinus1i_, id.height0Shift > 9 ? 511 : (1 << id.height0Shift) - 1);
|
||||
} else {
|
||||
constWidthHeight256f_ = nullptr;
|
||||
constWidthHeight16f_ = nullptr;
|
||||
constWidthMinus1i_ = nullptr;
|
||||
constHeightMinus1i_ = nullptr;
|
||||
}
|
||||
@@ -1236,8 +1236,13 @@ bool SamplerJitCache::Jit_ReadClutQuad(const SamplerID &id, bool level1) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Now we multiply by 16, and add.
|
||||
PSLLD(vecLevelReg, 4);
|
||||
// Now we multiply by 16 (CLUT4) or take (level & 1) * 256 (CLUT8), and add.
|
||||
if (id.TexFmt() == GE_TFMT_CLUT4) {
|
||||
PSLLD(vecLevelReg, 4);
|
||||
} else {
|
||||
PSLLD(vecLevelReg, 31);
|
||||
PSRLD(vecLevelReg, 23);
|
||||
}
|
||||
PADDD(indexReg, R(vecLevelReg));
|
||||
regCache_.Release(vecLevelReg, RegCache::VEC_TEMP0);
|
||||
}
|
||||
@@ -1329,69 +1334,71 @@ bool SamplerJitCache::Jit_BlendQuad(const SamplerID &id, bool level1) {
|
||||
Describe(level1 ? "BlendQuadMips" : "BlendQuad");
|
||||
|
||||
if (cpu_info.bSSE4_1 && cpu_info.bSSSE3) {
|
||||
// Like the GE, lerp horizontally first, truncating to 8 bits, then vertically (gpu/probe exp52).
|
||||
// Let's start by rearranging from TL TR BL BR like this:
|
||||
// ABCD EFGH IJKL MNOP -> AI BJ CK DL EM FN GO HP -> AIEM BJFN CKGO DLHP
|
||||
// This way, all the RGBAs are next to each other, and in order TL BL TR BR.
|
||||
// ABCD EFGH IJKL MNOP -> ABCD IJKL EFGH MNOP -> AE BF CG DH IM JN KO LP -> AEIM BFJN CGKO DHLP
|
||||
// This way, all the RGBAs are next to each other, and in order TL TR BL BR.
|
||||
X64Reg quadReg = regCache_.Find(level1 ? RegCache::VEC_RESULT1 : RegCache::VEC_RESULT);
|
||||
X64Reg tempArrangeReg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
PSHUFD(quadReg, R(quadReg), _MM_SHUFFLE(3, 1, 2, 0));
|
||||
PSHUFD(tempArrangeReg, R(quadReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PUNPCKLBW(quadReg, R(tempArrangeReg));
|
||||
// Okay, that's top and bottom interleaved, now for left and right.
|
||||
// Okay, that's left and right interleaved, now for top and bottom.
|
||||
PSHUFD(tempArrangeReg, R(quadReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PUNPCKLWD(quadReg, R(tempArrangeReg));
|
||||
regCache_.Release(tempArrangeReg, RegCache::VEC_TEMP0);
|
||||
|
||||
// Next up, we want to multiply and add using a repeated TB frac pair.
|
||||
// That's (0x10 - frac_v) in byte 1, frac_v in byte 2, repeating.
|
||||
// Next up, we want to multiply and add using a repeated LR frac pair.
|
||||
// That's (0x10 - frac_u) in byte 1, frac_u in byte 2, repeating.
|
||||
X64Reg fracReg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
X64Reg allFracReg = regCache_.Find(RegCache::VEC_FRAC);
|
||||
X64Reg zeroReg = GetZeroVec();
|
||||
if (level1) {
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(3, 3, 3, 3));
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(2, 2, 2, 2));
|
||||
} else {
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(1, 1, 1, 1));
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(0, 0, 0, 0));
|
||||
}
|
||||
PSHUFB(fracReg, R(zeroReg));
|
||||
regCache_.Unlock(zeroReg, RegCache::VEC_ZERO);
|
||||
regCache_.Unlock(allFracReg, RegCache::VEC_FRAC);
|
||||
|
||||
// Now, inverse fracReg, then interleave into the actual multiplier.
|
||||
// This gives us the repeated TB pairs we wanted.
|
||||
X64Reg multTBReg = regCache_.Alloc(RegCache::VEC_TEMP1);
|
||||
MOVDQA(multTBReg, M(const10All8_));
|
||||
PSUBB(multTBReg, R(fracReg));
|
||||
PUNPCKLBW(multTBReg, R(fracReg));
|
||||
// This gives us the repeated LR pairs we wanted.
|
||||
X64Reg multLRReg = regCache_.Alloc(RegCache::VEC_TEMP1);
|
||||
MOVDQA(multLRReg, M(const10All8_));
|
||||
PSUBB(multLRReg, R(fracReg));
|
||||
PUNPCKLBW(multLRReg, R(fracReg));
|
||||
regCache_.Release(fracReg, RegCache::VEC_TEMP0);
|
||||
|
||||
// Now we can multiply and add paired lanes in one go.
|
||||
// Note that since T+B=0x10, this gives us exactly 12 bits.
|
||||
PMADDUBSW(quadReg, R(multTBReg));
|
||||
regCache_.Release(multTBReg, RegCache::VEC_TEMP1);
|
||||
// Now we can multiply and add paired lanes in one go, then truncate back to 8 bits.
|
||||
PMADDUBSW(quadReg, R(multLRReg));
|
||||
PSRLW(quadReg, 4);
|
||||
regCache_.Release(multLRReg, RegCache::VEC_TEMP1);
|
||||
|
||||
// With that done, we need to multiply by LR, or rather 0L0R, and sum again.
|
||||
// Since RRRR was all next to each other, this gives us a clean total R.
|
||||
// With that done, we need to multiply by TB, or rather 0T0B, and sum again.
|
||||
// Since the top and bottom of each channel are next to each other, this gives a clean total.
|
||||
fracReg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
allFracReg = regCache_.Find(RegCache::VEC_FRAC);
|
||||
if (level1) {
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(2, 2, 2, 2));
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(3, 3, 3, 3));
|
||||
} else {
|
||||
// We can ignore the high bits, since we'll interleave those away anyway.
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(0, 0, 0, 0));
|
||||
PSHUFLW(fracReg, R(allFracReg), _MM_SHUFFLE(1, 1, 1, 1));
|
||||
}
|
||||
regCache_.Unlock(allFracReg, RegCache::VEC_FRAC);
|
||||
|
||||
// Again, we're inversing into an interleaved multiplier. L is the inversed one.
|
||||
// 0L0R is (0x10 - frac_u), frac_u - 2x16 repeated four times.
|
||||
X64Reg multLRReg = regCache_.Alloc(RegCache::VEC_TEMP1);
|
||||
MOVDQA(multLRReg, M(const10All16_));
|
||||
PSUBW(multLRReg, R(fracReg));
|
||||
PUNPCKLWD(multLRReg, R(fracReg));
|
||||
// Again, we're inversing into an interleaved multiplier. T is the inversed one.
|
||||
// 0T0B is (0x10 - frac_v), frac_v - 2x16 repeated four times.
|
||||
X64Reg multTBReg = regCache_.Alloc(RegCache::VEC_TEMP1);
|
||||
MOVDQA(multTBReg, M(const10All16_));
|
||||
PSUBW(multTBReg, R(fracReg));
|
||||
PUNPCKLWD(multTBReg, R(fracReg));
|
||||
regCache_.Release(fracReg, RegCache::VEC_TEMP0);
|
||||
|
||||
// This gives us RGBA as dwords, but they're all shifted left by 8 from the multiplies.
|
||||
PMADDWD(quadReg, R(multLRReg));
|
||||
PSRLD(quadReg, 8);
|
||||
regCache_.Release(multLRReg, RegCache::VEC_TEMP1);
|
||||
// This gives us RGBA as dwords, but they're all shifted left by 4 from the multiplies.
|
||||
PMADDWD(quadReg, R(multTBReg));
|
||||
PSRLD(quadReg, 4);
|
||||
regCache_.Release(multTBReg, RegCache::VEC_TEMP1);
|
||||
|
||||
// Shrink to 16-bit, it's more convenient for later.
|
||||
if (level1) {
|
||||
@@ -1444,9 +1451,15 @@ bool SamplerJitCache::Jit_BlendQuad(const SamplerID &id, bool level1) {
|
||||
regCache_.Release(fracReg, RegCache::VEC_TEMP2);
|
||||
|
||||
// Okay, we have 8-bits in the top and bottom rows for the color.
|
||||
// Multiply by frac to get 12, which we keep for the next stage.
|
||||
// Multiply by frac, sum left and right, and truncate back to 8 bits like the GE (gpu/probe exp52).
|
||||
PMULLW(topReg, R(fracMulReg));
|
||||
PMULLW(bottomReg, R(fracMulReg));
|
||||
PSHUFD(fracMulReg, R(topReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PADDW(topReg, R(fracMulReg));
|
||||
PSRLW(topReg, 4);
|
||||
PSHUFD(fracMulReg, R(bottomReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PADDW(bottomReg, R(fracMulReg));
|
||||
PSRLW(bottomReg, 4);
|
||||
regCache_.Release(fracMulReg, RegCache::VEC_TEMP3);
|
||||
|
||||
// Time for frac_v. This time, we want it in all 8 lanes.
|
||||
@@ -1465,41 +1478,33 @@ bool SamplerJitCache::Jit_BlendQuad(const SamplerID &id, bool level1) {
|
||||
MOVDQA(fracTopReg, M(const10All16_));
|
||||
PSUBW(fracTopReg, R(fracReg));
|
||||
|
||||
// We had 12, plus 4 frac, that gives us 16.
|
||||
// 8 bits plus 4 frac, that gives us 12.
|
||||
PMULLW(bottomReg, R(fracReg));
|
||||
PMULLW(topReg, R(fracTopReg));
|
||||
regCache_.Release(fracReg, RegCache::VEC_TEMP2);
|
||||
regCache_.Release(fracTopReg, RegCache::VEC_TEMP3);
|
||||
|
||||
// Finally, time to sum them all up and divide by 256 to get back to 8 bits.
|
||||
PADDUSW(bottomReg, R(topReg));
|
||||
// Finally, time to sum them up and divide by 16 to get back to 8 bits.
|
||||
PADDW(bottomReg, R(topReg));
|
||||
PSRLW(bottomReg, 4);
|
||||
regCache_.Release(topReg, RegCache::VEC_TEMP0);
|
||||
|
||||
if (level1) {
|
||||
PSHUFD(quadReg, R(bottomReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PADDUSW(quadReg, R(bottomReg));
|
||||
PSRLW(quadReg, 8);
|
||||
MOVDQA(quadReg, R(bottomReg));
|
||||
regCache_.Release(bottomReg, RegCache::VEC_TEMP1);
|
||||
regCache_.Unlock(quadReg, RegCache::VEC_RESULT1);
|
||||
} else {
|
||||
bool changeSuccess = regCache_.ChangeReg(XMM0, RegCache::VEC_RESULT);
|
||||
if (!changeSuccess) {
|
||||
_assert_msg_(XMM0 == bottomReg, "Unexpected other reg locked as destReg");
|
||||
X64Reg otherReg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
PSHUFD(otherReg, R(bottomReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PADDUSW(bottomReg, R(otherReg));
|
||||
regCache_.Release(otherReg, RegCache::VEC_TEMP0);
|
||||
regCache_.Release(bottomReg, RegCache::VEC_TEMP1);
|
||||
|
||||
// Okay, now it can be changed.
|
||||
regCache_.ChangeReg(XMM0, RegCache::VEC_RESULT);
|
||||
} else {
|
||||
PSHUFD(XMM0, R(bottomReg), _MM_SHUFFLE(3, 2, 3, 2));
|
||||
PADDUSW(XMM0, R(bottomReg));
|
||||
MOVDQA(XMM0, R(bottomReg));
|
||||
regCache_.Release(bottomReg, RegCache::VEC_TEMP1);
|
||||
}
|
||||
|
||||
PSRLW(XMM0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2617,16 +2622,17 @@ bool SamplerJitCache::Jit_GetTexelCoords(const SamplerID &id) {
|
||||
regCache_.Unlock(zeroReg, RegCache::VEC_ZERO);
|
||||
}
|
||||
|
||||
// We just want this value as a float, times 256.
|
||||
PSLLD(sizesReg, 8);
|
||||
// We just want this value as a float, times 16.
|
||||
PSLLD(sizesReg, 4);
|
||||
CVTDQ2PS(sizesReg, R(sizesReg));
|
||||
|
||||
// Okay, we can multiply now, and convert back to integer.
|
||||
// Okay, we can multiply now, and convert back to integer: the GE truncates to 1/16 texel,
|
||||
// toward zero (gpu/probe exp57).
|
||||
MULPS(sReg, R(sizesReg));
|
||||
CVTTPS2DQ(sReg, R(sReg));
|
||||
regCache_.Release(sizesReg, RegCache::VEC_TEMP0);
|
||||
|
||||
PSRAD(sReg, 8);
|
||||
PSRAD(sReg, 4);
|
||||
|
||||
// Reuse tempXYReg for the level1 values.
|
||||
if (!cpu_info.bSSE4_1)
|
||||
@@ -2681,12 +2687,12 @@ bool SamplerJitCache::Jit_GetTexelCoords(const SamplerID &id) {
|
||||
regCache_.Release(tempReg, RegCache::GEN_TEMP0);
|
||||
regCache_.Unlock(levelReg, RegCache::GEN_ARG_LEVEL);
|
||||
} else {
|
||||
// Multiply, then convert to integer...
|
||||
// Multiply, then convert to integer (1/16 texel, truncated toward zero like the GE)...
|
||||
UNPCKLPS(sReg, R(tReg));
|
||||
MULPS(sReg, M(constWidthHeight256f_));
|
||||
MULPS(sReg, M(constWidthHeight16f_));
|
||||
CVTTPS2DQ(sReg, R(sReg));
|
||||
// Great, shift out the fraction.
|
||||
PSRAD(sReg, 8);
|
||||
PSRAD(sReg, 4);
|
||||
|
||||
// Square textures are kinda common.
|
||||
bool clampApplied = false;
|
||||
@@ -2793,17 +2799,17 @@ bool SamplerJitCache::Jit_GetTexelCoordsQuad(const SamplerID &id) {
|
||||
regCache_.Unlock(levelReg, RegCache::GEN_ARG_LEVEL);
|
||||
UnlockSamplerID(idReg);
|
||||
|
||||
// Now make a float version of sizesReg, times 256.
|
||||
X64Reg sizes256Reg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
PSLLD(sizes256Reg, sizesReg, 8);
|
||||
CVTDQ2PS(sizes256Reg, R(sizes256Reg));
|
||||
// Now make a float version of sizesReg, times 16.
|
||||
X64Reg sizes16Reg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
PSLLD(sizes16Reg, sizesReg, 4);
|
||||
CVTDQ2PS(sizes16Reg, R(sizes16Reg));
|
||||
|
||||
// Next off, move S and T into a single reg, which will become U0 V0 U1 V1.
|
||||
UNPCKLPS(sReg, R(tReg));
|
||||
SHUFPS(sReg, R(sReg), _MM_SHUFFLE(1, 0, 1, 0));
|
||||
// And multiply by the sizes, all lined up already.
|
||||
MULPS(sReg, R(sizes256Reg));
|
||||
regCache_.Release(sizes256Reg, RegCache::VEC_TEMP0);
|
||||
MULPS(sReg, R(sizes16Reg));
|
||||
regCache_.Release(sizes16Reg, RegCache::VEC_TEMP0);
|
||||
|
||||
// For wrap/clamp purposes, we want width or height minus one. Do that now.
|
||||
PSUBD(sizesReg, M(constOnes32_));
|
||||
@@ -2811,17 +2817,18 @@ bool SamplerJitCache::Jit_GetTexelCoordsQuad(const SamplerID &id) {
|
||||
} else {
|
||||
// Easy mode.
|
||||
UNPCKLPS(sReg, R(tReg));
|
||||
MULPS(sReg, M(constWidthHeight256f_));
|
||||
MULPS(sReg, M(constWidthHeight16f_));
|
||||
}
|
||||
|
||||
// And now, convert to integers for all later processing.
|
||||
CVTPS2DQ(sReg, R(sReg));
|
||||
// And now, convert to integers for all later processing: 1/16 texel, truncated toward zero like
|
||||
// the GE (gpu/probe exp57).
|
||||
CVTTPS2DQ(sReg, R(sReg));
|
||||
|
||||
// Now adjust X and Y...
|
||||
// Now adjust X and Y by half a texel...
|
||||
X64Reg tempXYReg = regCache_.Alloc(RegCache::VEC_TEMP0);
|
||||
// Product a -128 constant.
|
||||
// Product a -8 constant.
|
||||
PCMPEQD(tempXYReg, R(tempXYReg));
|
||||
PSLLD(tempXYReg, 7);
|
||||
PSLLD(tempXYReg, 3);
|
||||
PADDD(sReg, R(tempXYReg));
|
||||
regCache_.Release(tempXYReg, RegCache::VEC_TEMP0);
|
||||
|
||||
@@ -2831,8 +2838,8 @@ bool SamplerJitCache::Jit_GetTexelCoordsQuad(const SamplerID &id) {
|
||||
allFracReg = regCache_.Find(RegCache::VEC_FRAC);
|
||||
else
|
||||
allFracReg = regCache_.Alloc(RegCache::VEC_FRAC);
|
||||
// We only want the four bits after the first four, though.
|
||||
PSLLD(allFracReg, sReg, 24);
|
||||
// We only want the low four bits, though.
|
||||
PSLLD(allFracReg, sReg, 28);
|
||||
PSRLD(allFracReg, 28);
|
||||
// It's convenient later if this is in the low words only.
|
||||
PACKSSDW(allFracReg, R(allFracReg));
|
||||
@@ -2840,7 +2847,7 @@ bool SamplerJitCache::Jit_GetTexelCoordsQuad(const SamplerID &id) {
|
||||
regCache_.ForceRetain(RegCache::VEC_FRAC);
|
||||
|
||||
// With those extracted, we can now get rid of the fractional bits.
|
||||
PSRAD(sReg, 8);
|
||||
PSRAD(sReg, 4);
|
||||
|
||||
// Now it's time to separate the lanes into separate registers and add next UV offsets.
|
||||
if (id.hasAnyMips) {
|
||||
@@ -3657,8 +3664,7 @@ bool SamplerJitCache::Jit_ReadClutColor(const SamplerID &id) {
|
||||
|
||||
if (regCache_.Has(RegCache::GEN_ARG_LEVEL)) {
|
||||
X64Reg levelReg = regCache_.Find(RegCache::GEN_ARG_LEVEL);
|
||||
// We need to multiply by 16 and add, LEA allows us to copy too.
|
||||
LEA(32, temp2Reg, MScaled(levelReg, SCALE_4, 0));
|
||||
MOV(32, R(temp2Reg), R(levelReg));
|
||||
regCache_.Unlock(levelReg, RegCache::GEN_ARG_LEVEL);
|
||||
if (id.fetch)
|
||||
regCache_.ForceRelease(RegCache::GEN_ARG_LEVEL);
|
||||
@@ -3666,11 +3672,16 @@ bool SamplerJitCache::Jit_ReadClutColor(const SamplerID &id) {
|
||||
_assert_(stackLevelOffset_ != -1);
|
||||
// The argument was saved on the stack.
|
||||
MOV(32, R(temp2Reg), MDisp(RSP, stackArgPos_ + stackLevelOffset_));
|
||||
LEA(32, temp2Reg, MScaled(temp2Reg, SCALE_4, 0));
|
||||
}
|
||||
|
||||
// Second step of the multiply by 16 (since we only multiplied by 4 before.)
|
||||
LEA(64, resultReg, MComplex(resultReg, temp2Reg, SCALE_4, 0));
|
||||
// CLUT4 adds level * 16, CLUT8 (level & 1) * 256.
|
||||
if (id.TexFmt() == GE_TFMT_CLUT4) {
|
||||
SHL(32, R(temp2Reg), Imm8(4));
|
||||
} else {
|
||||
SHL(32, R(temp2Reg), Imm8(31));
|
||||
SHR(32, R(temp2Reg), Imm8(23));
|
||||
}
|
||||
ADD(64, R(resultReg), R(temp2Reg));
|
||||
regCache_.Release(temp2Reg, RegCache::GEN_TEMP2);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,10 +163,10 @@ const SoftwareCommandTableEntry softgpuCommandTable[] = {
|
||||
{ GE_CMD_TEXMAPMODE, 0, SoftDirty::TRANSFORM_BASIC | SoftDirty::RAST_TEX },
|
||||
|
||||
// These are read on every SubmitPrim, no need for dirtying or flushing.
|
||||
{ GE_CMD_TEXSCALEU },
|
||||
{ GE_CMD_TEXSCALEV },
|
||||
{ GE_CMD_TEXOFFSETU },
|
||||
{ GE_CMD_TEXOFFSETV },
|
||||
{ GE_CMD_TEXSCALEU, 0, SoftDirty::TRANSFORM_BASIC },
|
||||
{ GE_CMD_TEXSCALEV, 0, SoftDirty::TRANSFORM_BASIC },
|
||||
{ GE_CMD_TEXOFFSETU, 0, SoftDirty::TRANSFORM_BASIC },
|
||||
{ GE_CMD_TEXOFFSETV, 0, SoftDirty::TRANSFORM_BASIC },
|
||||
|
||||
{ GE_CMD_TEXSIZE0, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
|
||||
{ GE_CMD_TEXSIZE1, 0, SoftDirty::SAMPLER_TEXLIST | SoftDirty::BINNER_OVERLAP },
|
||||
@@ -317,8 +317,9 @@ const SoftwareCommandTableEntry softgpuCommandTable[] = {
|
||||
{ GE_CMD_LDC3, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_3 },
|
||||
{ GE_CMD_LSC3, 0, SoftDirty::LIGHT_BASIC | SoftDirty::LIGHT_MATERIAL | SoftDirty::LIGHT_3 },
|
||||
|
||||
{ GE_CMD_TEXFLUSH, FLAG_EXECUTE, SoftDirty::NONE, &SoftGPU::Execute_TexFlush },
|
||||
|
||||
// These are currently ignored, but might do flushing later.
|
||||
{ GE_CMD_TEXFLUSH },
|
||||
{ GE_CMD_TEXSYNC },
|
||||
|
||||
// These are just nop or part of other later commands.
|
||||
@@ -966,6 +967,7 @@ void SoftGPU::Execute_Bezier(u32 op, u32 diff) {
|
||||
surface.num_patches_v = (surface.num_points_v - 1) / 3;
|
||||
surface.primType = gstate.getPatchPrimitiveType();
|
||||
surface.patchFacing = gstate.patchfacing & 1;
|
||||
surface.geExact = true;
|
||||
|
||||
SetDrawType(DRAW_BEZIER, PatchPrimToPrim(surface.primType));
|
||||
|
||||
@@ -1020,6 +1022,7 @@ void SoftGPU::Execute_Spline(u32 op, u32 diff) {
|
||||
surface.num_patches_v = surface.num_points_v - 3;
|
||||
surface.primType = gstate.getPatchPrimitiveType();
|
||||
surface.patchFacing = gstate.patchfacing & 1;
|
||||
surface.geExact = true;
|
||||
|
||||
SetDrawType(DRAW_SPLINE, PatchPrimToPrim(surface.primType));
|
||||
|
||||
@@ -1083,6 +1086,10 @@ void SoftGPU::Execute_FramebufFormat(u32 op, u32 diff) {
|
||||
drawEngine_->transformUnit.Flush(this, "framebuf");
|
||||
}
|
||||
|
||||
void SoftGPU::Execute_TexFlush(u32 op, u32 diff) {
|
||||
drawEngine_->transformUnit.NotifyTexFlush();
|
||||
}
|
||||
|
||||
void SoftGPU::Execute_ZbufPtr(u32 op, u32 diff) {
|
||||
// We assume depthbuf.data won't change while we're drawing.
|
||||
if (diff) {
|
||||
|
||||
@@ -183,6 +183,7 @@ public:
|
||||
void Execute_FramebufPtr(u32 op, u32 diff);
|
||||
void Execute_FramebufFormat(u32 op, u32 diff);
|
||||
void Execute_ZbufPtr(u32 op, u32 diff);
|
||||
void Execute_TexFlush(u32 op, u32 diff);
|
||||
void Execute_VertexType(u32 op, u32 diff);
|
||||
|
||||
// Overridden to change flushing behavior.
|
||||
|
||||
+345
-63
@@ -17,10 +17,13 @@
|
||||
|
||||
#include "ppsspp_config.h"
|
||||
|
||||
#include <cfloat>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
|
||||
#include "Common/Common.h"
|
||||
#include "Common/CPUDetect.h"
|
||||
#include "Common/Data/Convert/ColorConv.h"
|
||||
#include "Common/Math/math_util.h"
|
||||
#include "Common/MemoryUtil.h"
|
||||
#include "Common/Profiler/Profiler.h"
|
||||
@@ -145,12 +148,65 @@ VertexDecoder *SoftwareDrawEngine::FindVertexDecoder(u32 vtype) {
|
||||
return DrawEngineCommon::GetVertexDecoder(vertTypeID);
|
||||
}
|
||||
|
||||
WorldCoords TransformUnit::ModelToWorld(const ModelCoords &coords) {
|
||||
return Vec3ByMatrix43(coords, gstate.worldMatrix);
|
||||
WorldCoords TransformUnit::ModelToWorldNormal(const ModelCoords &coords) {
|
||||
// Each component summed like a matrix row (gpu/probe exp61).
|
||||
const float *m = gstate.worldMatrix;
|
||||
return WorldCoords(GEDot(coords, Vec3f(m[0], m[3], m[6])), GEDot(coords, Vec3f(m[1], m[4], m[7])), GEDot(coords, Vec3f(m[2], m[5], m[8])));
|
||||
}
|
||||
|
||||
WorldCoords TransformUnit::ModelToWorldNormal(const ModelCoords &coords) {
|
||||
return Norm3ByMatrix43(coords, gstate.worldMatrix);
|
||||
// A clip space component from the combined matrix (gpu/probe exp32, exp34, exp42). The position is a
|
||||
// float24; the translation is a term of its own.
|
||||
static inline float GEClipComponent(const Vec3f &v, const float m[16], int c) {
|
||||
GERowTerm terms[4] = {
|
||||
GEProduct(TruncateToFloat24(v.x), m[c]),
|
||||
GEProduct(TruncateToFloat24(v.y), m[4 + c]),
|
||||
GEProduct(TruncateToFloat24(v.z), m[8 + c]),
|
||||
GEProduct(1.0f, m[12 + c]),
|
||||
};
|
||||
return GERowSum(terms, 4);
|
||||
}
|
||||
|
||||
|
||||
// A texture coordinate from the 4x3 texture matrix, summed like a clip space row (gpu/probe exp64).
|
||||
static inline float GETexGenComponent(const Vec3f &v, const float m[12], int c) {
|
||||
GERowTerm terms[4] = {
|
||||
GEProduct(TruncateToFloat24(v.x), m[c]),
|
||||
GEProduct(TruncateToFloat24(v.y), m[3 + c]),
|
||||
GEProduct(TruncateToFloat24(v.z), m[6 + c]),
|
||||
GEProduct(1.0f, m[9 + c]),
|
||||
};
|
||||
return GERowSum(terms, 4);
|
||||
}
|
||||
|
||||
// Multiplies two matrices the way the GE combines world, view and projection (gpu/probe exp35, exp42):
|
||||
// in the order (world * view) * projection, each entry summed like a row in GEClipComponent.
|
||||
static void GECombineMatrices(float out[16], const float a[16], const float b[16]) {
|
||||
for (int r = 0; r < 4; ++r) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
GERowTerm terms[4];
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
terms[k] = GEProduct(a[r * 4 + k], b[k * 4 + c]);
|
||||
}
|
||||
out[r * 4 + c] = GERowSum(terms, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A screen coordinate as the GE computes it (gpu/depth/transformprecision for Z, gpu/probe for X and Y):
|
||||
// the component divided by w is it times the reciprocal above, truncated to a float24, then scaled and
|
||||
// offset with GEAdd. Around a center of 2048, that lands X and Y on the 1/16 subpixel grid.
|
||||
static inline float GEViewport(float clipC, float clipW, float scale, float center) {
|
||||
const float w = TruncateToFloat24(clipW);
|
||||
if (!std::isfinite(w) || !std::isfinite(clipC) || fabsf(w) < FLT_MIN) {
|
||||
return clipC * scale / clipW + center;
|
||||
}
|
||||
const float ndc = ProductToFloat24((double)TruncateToFloat24(clipC) * GERecip(w));
|
||||
return GEAdd(ProductToFloat24((double)ndc * scale), center);
|
||||
}
|
||||
|
||||
// Screen Z is floored.
|
||||
static inline float GEScreenZ(float clipZ, float clipW, float zScale, float zCenter) {
|
||||
return floorf(GEViewport(clipZ, clipW, zScale, zCenter));
|
||||
}
|
||||
|
||||
template <bool depthClamp, bool alwaysCheckRange>
|
||||
@@ -161,8 +217,9 @@ static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords,
|
||||
|
||||
// This matches hardware tests - depth is clamped when this flag is on.
|
||||
if constexpr (depthClamp) {
|
||||
// Note: if the depth is clipped (z/w <= -1.0), the outside_range_flag should NOT be set, even for x and y.
|
||||
if ((alwaysCheckRange || coords.z > -coords.w) && (scaled.x >= SCREEN_BOUND || scaled.y >= SCREEN_BOUND || scaled.x < 0 || scaled.y < 0)) {
|
||||
// A vertex the near plane clips away (z < -w) doesn't set the flag, even for x and y. One exactly on
|
||||
// the plane isn't clipped, so its range counts (gpu/clipping/guardband).
|
||||
if ((alwaysCheckRange || !(coords.z < -coords.w)) && (scaled.x >= SCREEN_BOUND || scaled.y >= SCREEN_BOUND || scaled.x < 0 || scaled.y < 0)) {
|
||||
*outside_range_flag = true;
|
||||
}
|
||||
|
||||
@@ -175,10 +232,9 @@ static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords,
|
||||
}
|
||||
|
||||
// 16 = 0xFFFF / 4095.9375
|
||||
// Round up at 0.625 to the nearest subpixel.
|
||||
static_assert(SCREEN_SCALE_FACTOR == 16, "Currently only supports scale 16");
|
||||
int x = (int)(scaled.x * 16.0f + 0.375f - gstate.getOffsetX16());
|
||||
int y = (int)(scaled.y * 16.0f + 0.375f - gstate.getOffsetY16());
|
||||
int x = (int)floorf(scaled.x * 16.0f) - gstate.getOffsetX16();
|
||||
int y = (int)floorf(scaled.y * 16.0f) - gstate.getOffsetY16();
|
||||
return ScreenCoords(x, y, scaled.z);
|
||||
}
|
||||
|
||||
@@ -193,9 +249,9 @@ static inline ScreenCoords ClipToScreenInternal(const ClipCoords &coords, bool *
|
||||
float zScale = gstate.getViewportZScale();
|
||||
float zCenter = gstate.getViewportZCenter();
|
||||
|
||||
float x = coords.x * xScale / coords.w + xCenter;
|
||||
float y = coords.y * yScale / coords.w + yCenter;
|
||||
float z = coords.z * zScale / coords.w + zCenter;
|
||||
float x = GEViewport(coords.x, coords.w, xScale, xCenter);
|
||||
float y = GEViewport(coords.y, coords.w, yScale, yCenter);
|
||||
float z = GEScreenZ(coords.z, coords.w, zScale, zCenter);
|
||||
|
||||
if (gstate.isDepthClipEnabled()) {
|
||||
return ClipToScreenInternal<true, true>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
@@ -207,6 +263,24 @@ ScreenCoords TransformUnit::ClipToScreen(const ClipCoords &coords, bool *outside
|
||||
return ClipToScreenInternal(coords, outsideRangeFlag);
|
||||
}
|
||||
|
||||
// Near plane clipping (gpu/probe exp43-46): from the inside vertex, t = d_in / (d_in - d_out) with
|
||||
// d = z + w, using the GE's reciprocal, and each coordinate is in + t * (out - in), all in float24 math.
|
||||
float TransformUnit::NearPlaneT(const ClipCoords &in, const ClipCoords &out) {
|
||||
const float dIn = GEAdd(in.z, in.w);
|
||||
const float dOut = GEAdd(out.z, out.w);
|
||||
const float den = TruncateToFloat24(GEAdd(dIn, -dOut));
|
||||
return ProductToFloat24((double)TruncateToFloat24(dIn) * GERecip(den));
|
||||
}
|
||||
|
||||
ClipCoords TransformUnit::NearPlanePoint(const ClipCoords &in, const ClipCoords &out, float t) {
|
||||
ClipCoords result;
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const float delta = TruncateToFloat24(GEAdd(out[c], -in[c]));
|
||||
result[c] = TruncateToFloat24(GEAdd(ProductToFloat24((double)t * delta), in[c]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ScreenCoords TransformUnit::DrawingToScreen(const DrawingCoords &coords, u16 z) {
|
||||
ScreenCoords ret;
|
||||
ret.x = (u32)coords.x * SCREEN_SCALE_FACTOR;
|
||||
@@ -225,6 +299,12 @@ struct TransformState {
|
||||
|
||||
float matrix[16];
|
||||
Vec4f posToFog;
|
||||
// With finite fog parameters, the GE's own arithmetic (gpu/probe exp20): the view z as a row of the
|
||||
// combined world-view matrix, then float24(GEAdd(z, end) * slope).
|
||||
bool fogGE;
|
||||
float viewZColumn[4];
|
||||
float fogEnd;
|
||||
float fogSlope;
|
||||
Vec3f screenScale;
|
||||
Vec3f screenAdd;
|
||||
|
||||
@@ -239,13 +319,28 @@ struct TransformState {
|
||||
uint8_t uvGenMode : 2;
|
||||
uint8_t matrixMode : 2;
|
||||
};
|
||||
// The UV scale and offset, applied in ReadVertex with the GE's arithmetic (the vertex decoder leaves
|
||||
// them out then, see UsesGEUVScale).
|
||||
bool geUVScale;
|
||||
float uvScale[2];
|
||||
float uvOffset[2];
|
||||
};
|
||||
|
||||
// UV gen mode 0 in transform mode: u * scale + offset as the GE computes it (gpu/probe exp66).
|
||||
static bool UsesGEUVScale(u32 vertexType) {
|
||||
return (vertexType & GE_VTYPE_THROUGH_MASK) == 0 && gstate.getUVGenMode() == GE_TEXMAP_TEXTURE_COORDS;
|
||||
}
|
||||
|
||||
void ComputeTransformState(TransformState *state, const VertexReader &vreader) {
|
||||
state->enableTransform = !vreader.isThrough();
|
||||
state->enableLighting = gstate.isLightingEnabled();
|
||||
state->enableFog = gstate.isFogEnabled();
|
||||
state->readUV = !gstate.isModeClear() && gstate.isTextureMapEnabled() && vreader.hasUV();
|
||||
state->geUVScale = !vreader.isThrough() && gstate.getUVGenMode() == GE_TEXMAP_TEXTURE_COORDS;
|
||||
state->uvScale[0] = TruncateToFloat24(getFloat24(gstate.texscaleu));
|
||||
state->uvScale[1] = TruncateToFloat24(getFloat24(gstate.texscalev));
|
||||
state->uvOffset[0] = TruncateToFloat24(getFloat24(gstate.texoffsetu));
|
||||
state->uvOffset[1] = TruncateToFloat24(getFloat24(gstate.texoffsetv));
|
||||
state->negateNormals = gstate.areNormalsReversed();
|
||||
|
||||
state->uvGenMode = gstate.getUVGenMode();
|
||||
@@ -266,24 +361,23 @@ void ComputeTransformState(TransformState *state, const VertexReader &vreader) {
|
||||
canSkipWorldPos = false;
|
||||
}
|
||||
}
|
||||
state->lightingState.viewDir = PSPViewDirection(gstate.viewMatrix);
|
||||
// The viewer direction (see PSPViewDirection), normalized like the GE does.
|
||||
Vec3f viewDir(gstate.viewMatrix[2], gstate.viewMatrix[5], gstate.viewMatrix[8]);
|
||||
if (GENormalize(viewDir) == 0.0f)
|
||||
viewDir = Vec3f(0.0f, 0.0f, 1.0f);
|
||||
state->lightingState.viewDir = viewDir;
|
||||
|
||||
float world[16];
|
||||
float view[16];
|
||||
float worldview[16];
|
||||
ConvertMatrix4x3To4x4(view, gstate.viewMatrix);
|
||||
if (state->enableFog || canSkipWorldPos) {
|
||||
ConvertMatrix4x3To4x4(world, gstate.worldMatrix);
|
||||
Matrix4ByMatrix4(worldview, world, view);
|
||||
}
|
||||
ConvertMatrix4x3To4x4(world, gstate.worldMatrix);
|
||||
GECombineMatrices(worldview, world, view);
|
||||
|
||||
if (canSkipWorldPos) {
|
||||
state->matrixMode = (uint8_t)MatrixMode::POS_TO_CLIP;
|
||||
Matrix4ByMatrix4(state->matrix, worldview, gstate.projMatrix);
|
||||
} else {
|
||||
state->matrixMode = (uint8_t)MatrixMode::WORLD_TO_CLIP;
|
||||
Matrix4ByMatrix4(state->matrix, view, gstate.projMatrix);
|
||||
}
|
||||
// Clip coordinates always come from the model position and the matrices combined like the GE does
|
||||
// it, in the order (world * view) * projection. The world position is only needed for lighting.
|
||||
state->matrixMode = (uint8_t)(canSkipWorldPos ? MatrixMode::POS_TO_CLIP : MatrixMode::WORLD_TO_CLIP);
|
||||
GECombineMatrices(state->matrix, worldview, gstate.projMatrix);
|
||||
|
||||
if (state->enableFog) {
|
||||
float fogEnd = getFloat24(gstate.fog1);
|
||||
@@ -291,6 +385,11 @@ void ComputeTransformState(TransformState *state, const VertexReader &vreader) {
|
||||
|
||||
// We bake fog end and slope into the dot product.
|
||||
state->posToFog = Vec4f(worldview[2], worldview[6], worldview[10], worldview[14] + fogEnd);
|
||||
state->fogGE = !my_isnanorinf(fogEnd) && !my_isnanorinf(fogSlope);
|
||||
for (int i = 0; i < 4; ++i)
|
||||
state->viewZColumn[i] = worldview[2 + 4 * i];
|
||||
state->fogEnd = TruncateToFloat24(fogEnd);
|
||||
state->fogSlope = TruncateToFloat24(fogSlope);
|
||||
|
||||
// If either are NAN/INF, we simplify so there's no inf + -inf muddying things.
|
||||
// This is required for Outrun to render proper skies, for example.
|
||||
@@ -365,6 +464,13 @@ ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const Tran
|
||||
if (state.readUV) {
|
||||
vreader.ReadUV(vertex.v.texturecoords.AsArray());
|
||||
vertex.v.texturecoords.q() = 0.0f;
|
||||
if (state.geUVScale) {
|
||||
// The decoder only normalized them (8 and 16 bit UVs are unsigned).
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const float scaled = ProductToFloat24((double)TruncateToFloat24(vertex.v.texturecoords[i]) * state.uvScale[i]);
|
||||
vertex.v.texturecoords[i] = TruncateToFloat24(GEAdd(scaled, state.uvOffset[i]));
|
||||
}
|
||||
}
|
||||
lastTC = vertex.v.texturecoords;
|
||||
} else {
|
||||
vertex.v.texturecoords = lastTC;
|
||||
@@ -386,27 +492,18 @@ ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const Tran
|
||||
vertex.v.color1 = 0;
|
||||
|
||||
if (state.enableTransform) {
|
||||
WorldCoords worldpos;
|
||||
|
||||
switch (MatrixMode(state.matrixMode)) {
|
||||
case MatrixMode::POS_TO_CLIP:
|
||||
vertex.clippos = Vec3ByMatrix44(pos, state.matrix);
|
||||
break;
|
||||
|
||||
case MatrixMode::WORLD_TO_CLIP:
|
||||
worldpos = TransformUnit::ModelToWorld(pos);
|
||||
vertex.clippos = Vec3ByMatrix44(worldpos, state.matrix);
|
||||
break;
|
||||
// Clip coordinates with the GE's precision; the Test Drive map depends on it together with the
|
||||
// depth math below (#12786).
|
||||
{
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
vertex.clippos[c] = GEClipComponent(pos, state.matrix, c);
|
||||
}
|
||||
}
|
||||
|
||||
Vec3f screenScaled;
|
||||
#ifdef _M_SSE
|
||||
screenScaled.vec = _mm_mul_ps(vertex.clippos.vec, state.screenScale.vec);
|
||||
screenScaled.vec = _mm_div_ps(screenScaled.vec, _mm_shuffle_ps(vertex.clippos.vec, vertex.clippos.vec, _MM_SHUFFLE(3, 3, 3, 3)));
|
||||
screenScaled.vec = _mm_add_ps(screenScaled.vec, state.screenAdd.vec);
|
||||
#else
|
||||
screenScaled = vertex.clippos.xyz() * state.screenScale / vertex.clippos.w + state.screenAdd;
|
||||
#endif
|
||||
screenScaled.x = GEViewport(vertex.clippos.x, vertex.clippos.w, state.screenScale.x, state.screenAdd.x);
|
||||
screenScaled.y = GEViewport(vertex.clippos.y, vertex.clippos.w, state.screenScale.y, state.screenAdd.y);
|
||||
screenScaled.z = GEScreenZ(vertex.clippos.z, vertex.clippos.w, state.screenScale.z, state.screenAdd.z);
|
||||
bool outside_range_flag = false;
|
||||
vertex.v.screenpos = state.roundToScreen(screenScaled, vertex.clippos, &outside_range_flag);
|
||||
if (outside_range_flag) {
|
||||
@@ -415,17 +512,37 @@ ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const Tran
|
||||
return vertex;
|
||||
}
|
||||
|
||||
if (state.enableFog) {
|
||||
vertex.v.fogdepth = Dot43(state.posToFog, pos);
|
||||
if (state.enableFog && state.fogGE) {
|
||||
GERowTerm terms[4] = {
|
||||
GEProduct(TruncateToFloat24(pos.x), state.viewZColumn[0]),
|
||||
GEProduct(TruncateToFloat24(pos.y), state.viewZColumn[1]),
|
||||
GEProduct(TruncateToFloat24(pos.z), state.viewZColumn[2]),
|
||||
GEProduct(1.0f, state.viewZColumn[3]),
|
||||
};
|
||||
const float viewZ = GERowSum(terms, 4);
|
||||
const float f = ProductToFloat24((double)TruncateToFloat24(GEAdd(viewZ, state.fogEnd)) * state.fogSlope);
|
||||
vertex.v.fogdepth = GEFogFactor(f) * (1.0f / 256.0f);
|
||||
} else if (state.enableFog) {
|
||||
vertex.v.fogdepth = GEFogFactor(Dot43(state.posToFog, pos)) * (1.0f / 256.0f);
|
||||
} else {
|
||||
vertex.v.fogdepth = 1.0f;
|
||||
}
|
||||
vertex.v.clipw = vertex.clippos.w;
|
||||
|
||||
// The normal stays as the world matrix leaves it: lighting scales its dot products by the
|
||||
// reciprocal length instead (gpu/probe exp3, world matrix cases).
|
||||
Vec3<float> worldnormal;
|
||||
float normalRsqrt = 1.0f;
|
||||
if (state.lightingState.usesWorldNormal) {
|
||||
worldnormal = TransformUnit::ModelToWorldNormal(normal);
|
||||
worldnormal.NormalizeOr001();
|
||||
const float len2 = GEDot(worldnormal, worldnormal);
|
||||
if (len2 > 0.0f && std::isfinite(len2)) {
|
||||
normalRsqrt = GERsqrt(len2);
|
||||
} else if (len2 != 0.0f) {
|
||||
worldnormal = Vec3f(0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
// A zero normal stays zero: no diffuse or specular from any light (gpu/probe exp173; SOCOM
|
||||
// UCES01242 has meshes without normals but lit).
|
||||
}
|
||||
|
||||
// Time to generate some texture coords. Lighting will handle shade mapping.
|
||||
@@ -442,7 +559,8 @@ ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const Tran
|
||||
|
||||
case GE_PROJMAP_NORMALIZED_NORMAL:
|
||||
// This does not use 0, 0, 1 if length is zero.
|
||||
source = normal.Normalized(cpu_info.bSSE4_1);
|
||||
source = normal;
|
||||
GENormalize(source);
|
||||
break;
|
||||
|
||||
case GE_PROJMAP_NORMAL:
|
||||
@@ -451,15 +569,14 @@ ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const Tran
|
||||
}
|
||||
|
||||
// Note that UV scale/offset are not used in this mode.
|
||||
Vec3<float> stq = Vec3ByMatrix43(source, gstate.tgenMatrix);
|
||||
vertex.v.texturecoords = Vec3Packedf(stq.x, stq.y, stq.z);
|
||||
vertex.v.texturecoords = Vec3Packedf(GETexGenComponent(source, gstate.tgenMatrix, 0), GETexGenComponent(source, gstate.tgenMatrix, 1), GETexGenComponent(source, gstate.tgenMatrix, 2));
|
||||
} else if (state.uvGenMode == GE_TEXMAP_ENVIRONMENT_MAP) {
|
||||
Lighting::GenerateLightST(vertex.v, worldpos, worldnormal, state.lightingState.viewDir);
|
||||
Lighting::GenerateLightST(vertex.v, pos, worldnormal, normalRsqrt, state.lightingState.viewDir);
|
||||
}
|
||||
|
||||
PROFILE_THIS_SCOPE("light");
|
||||
if (state.enableLighting)
|
||||
Lighting::Process(vertex.v, worldpos, worldnormal, state.lightingState);
|
||||
Lighting::Process(vertex.v, pos, worldnormal, normalRsqrt, state.lightingState);
|
||||
} else {
|
||||
vertex.v.screenpos.x = (int)(pos[0] * SCREEN_SCALE_FACTOR);
|
||||
vertex.v.screenpos.y = (int)(pos[1] * SCREEN_SCALE_FACTOR);
|
||||
@@ -478,6 +595,157 @@ SoftDirty TransformUnit::GetDirty() {
|
||||
return binner_->GetDirty();
|
||||
}
|
||||
|
||||
static float ReadRawComponent(const u8 *p, int fmt, int i) {
|
||||
switch (fmt) {
|
||||
case 1: return ((const s8 *)p)[i] * (1.0f / 128.0f);
|
||||
case 2: { s16 v; memcpy(&v, p + 2 * i, 2); return v * (1.0f / 32768.0f); }
|
||||
case 3: { float v; memcpy(&v, p + 4 * i, 4); return v; }
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
static float ReadRawWeight(const u8 *p, int fmt, int i) {
|
||||
switch (fmt) {
|
||||
case 1: return p[i] * (1.0f / 128.0f);
|
||||
case 2: { u16 v; memcpy(&v, p + 2 * i, 2); return v * (1.0f / 32768.0f); }
|
||||
case 3: { float v; memcpy(&v, p + 4 * i, 4); return v; }
|
||||
default: return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// One morphed component as the GE computes it (gpu/probe exp67, exp107): each frame's value times its
|
||||
// weight as a float24, summed in frame order with the GE's adder.
|
||||
template <typename Read>
|
||||
static float GEMorphComponent(const VertexDecoder &dec, const u8 *in, int off, int c, Read read) {
|
||||
float acc = 0.0f;
|
||||
for (int k = 0; k < dec.morphcount; ++k) {
|
||||
const float v = TruncateToFloat24(read(in + k * dec.onesize_ + off, c));
|
||||
const float w = TruncateToFloat24(gstate_c.morphWeights[k]);
|
||||
const float term = v == 0.0f || w == 0.0f ? 0.0f : ProductToFloat24((double)v * w);
|
||||
if (term != 0.0f)
|
||||
acc = acc == 0.0f ? term : TruncateToFloat24(GEAdd(acc, term));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Skinning as the GE does it (gpu/probe exp68, exp70, exp71, bit exact): each bone matrix is scaled by
|
||||
// its weight, every entry a float24 product, and one accumulator then runs through all the bones in
|
||||
// order, adding each bone's translation, then x, y and z times its column, with the GE's adder.
|
||||
// Normals the same without the translation. With morph targets, the morph comes first, weights included
|
||||
// (gpu/probe exp108). Overwrites what the vertex decoder skinned in float.
|
||||
static void ApplyGESkinning(u8 *decoded, const VertexDecoder &dec, const u8 *raw, int count) {
|
||||
const DecVtxFormat &fmt = dec.GetDecVtxFmt();
|
||||
const int nweights = dec.nweights;
|
||||
for (int v = 0; v < count; ++v) {
|
||||
const u8 *in = raw + v * dec.VertexSize();
|
||||
u8 *out = decoded + v * fmt.stride;
|
||||
auto component = [&](int off, int c, auto read) {
|
||||
return dec.morphcount > 1 ? GEMorphComponent(dec, in, off, c, read) : read(in + off, c);
|
||||
};
|
||||
float bones[8][12];
|
||||
for (int b = 0; b < nweights; ++b) {
|
||||
const float w = TruncateToFloat24(component(dec.weightoff, b, [&](const u8 *p, int i) { return ReadRawWeight(p, dec.weighttype, i); }));
|
||||
for (int k = 0; k < 12; ++k) {
|
||||
const float m = gstate.boneMatrix[b * 12 + k];
|
||||
bones[b][k] = m == 0.0f || w == 0.0f ? 0.0f : ProductToFloat24((double)TruncateToFloat24(m) * w);
|
||||
}
|
||||
}
|
||||
auto skin = [&](const float src[3], bool translate, float dst[3]) {
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
float acc = 0.0f;
|
||||
auto add = [&](float term) {
|
||||
if (term != 0.0f)
|
||||
acc = acc == 0.0f ? TruncateToFloat24(term) : TruncateToFloat24(GEAdd(acc, term));
|
||||
};
|
||||
for (int b = 0; b < nweights; ++b) {
|
||||
if (translate)
|
||||
add(bones[b][9 + c]);
|
||||
for (int j = 0; j < 3; ++j)
|
||||
add(GEProduct(TruncateToFloat24(src[j]), bones[b][j * 3 + c]).Value());
|
||||
}
|
||||
dst[c] = acc;
|
||||
}
|
||||
};
|
||||
if (dec.pos) {
|
||||
float pos[3], skinned[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
pos[i] = component(dec.posoff, i, [&](const u8 *p, int c) { return ReadRawComponent(p, dec.pos, c); });
|
||||
skin(pos, true, skinned);
|
||||
memcpy(out + fmt.posoff, skinned, sizeof(skinned));
|
||||
}
|
||||
if (dec.nrm && fmt.nrmfmt == DEC_FLOAT_3) {
|
||||
float nrm[3], skinned[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
nrm[i] = component(dec.nrmoff, i, [&](const u8 *p, int c) { return ReadRawComponent(p, dec.nrm, c); });
|
||||
skin(nrm, false, skinned);
|
||||
memcpy(out + fmt.nrmoff, skinned, sizeof(skinned));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A vertex color channel (0 r, 1 g, 2 b, 3 a) expanded to 8 bits.
|
||||
static float ReadRawColorChannel(const u8 *p, int fmt, int c) {
|
||||
u16 v16;
|
||||
memcpy(&v16, p, 2);
|
||||
switch (fmt) {
|
||||
case GE_VTYPE_COL_565 >> GE_VTYPE_COL_SHIFT: {
|
||||
const u32 c8 = RGB565ToRGBA8888(v16);
|
||||
return (float)((c8 >> (8 * c)) & 0xFF);
|
||||
}
|
||||
case GE_VTYPE_COL_5551 >> GE_VTYPE_COL_SHIFT: {
|
||||
const u32 c8 = RGBA5551ToRGBA8888(v16);
|
||||
return (float)((c8 >> (8 * c)) & 0xFF);
|
||||
}
|
||||
case GE_VTYPE_COL_4444 >> GE_VTYPE_COL_SHIFT: {
|
||||
const u32 c8 = RGBA4444ToRGBA8888(v16);
|
||||
return (float)((c8 >> (8 * c)) & 0xFF);
|
||||
}
|
||||
case GE_VTYPE_COL_8888 >> GE_VTYPE_COL_SHIFT:
|
||||
return (float)p[c];
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Morphing as the GE does it (gpu/probe exp67, exp107): each frame's value times its weight as a
|
||||
// float24, summed in frame order with the GE's adder. Positions and normals as read; UVs too (8 and
|
||||
// 16 bit ones unsigned); colors per channel expanded to 8 bits, the sum floored. Overwrites the
|
||||
// decoder's float result.
|
||||
static void ApplyGEMorph(u8 *decoded, const VertexDecoder &dec, const u8 *raw, int count) {
|
||||
const DecVtxFormat &fmt = dec.GetDecVtxFmt();
|
||||
auto morphWith = [&](const u8 *in, int off, int n, float *dst, auto read) {
|
||||
for (int c = 0; c < n; ++c)
|
||||
dst[c] = GEMorphComponent(dec, in, off, c, read);
|
||||
};
|
||||
auto morph = [&](const u8 *in, int off, int cfmt, float dst[3]) {
|
||||
morphWith(in, off, 3, dst, [&](const u8 *p, int c) { return ReadRawComponent(p, cfmt, c); });
|
||||
};
|
||||
for (int v = 0; v < count; ++v) {
|
||||
const u8 *in = raw + v * dec.VertexSize();
|
||||
u8 *out = decoded + v * fmt.stride;
|
||||
float r[3];
|
||||
if (dec.pos) {
|
||||
morph(in, dec.posoff, dec.pos, r);
|
||||
memcpy(out + fmt.posoff, r, sizeof(r));
|
||||
}
|
||||
if (dec.nrm && fmt.nrmfmt == DEC_FLOAT_3) {
|
||||
morph(in, dec.nrmoff, dec.nrm, r);
|
||||
memcpy(out + fmt.nrmoff, r, sizeof(r));
|
||||
}
|
||||
if (dec.tc && fmt.uvfmt == DEC_FLOAT_2) {
|
||||
float uv[2];
|
||||
morphWith(in, dec.tcoff, 2, uv, [&](const u8 *p, int c) { return ReadRawWeight(p, dec.tc, c); });
|
||||
memcpy(out + fmt.uvoff, uv, sizeof(uv));
|
||||
}
|
||||
if (dec.col && fmt.c0fmt == DEC_U8_4) {
|
||||
float ch[4];
|
||||
morphWith(in, dec.coloff, 4, ch, [&](const u8 *p, int c) { return ReadRawColorChannel(p, dec.col, c); });
|
||||
for (int c = 0; c < 4; ++c)
|
||||
out[fmt.c0off + c] = (u8)std::clamp((int)std::floor(ch[c]), 0, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SoftwareVertexReader {
|
||||
public:
|
||||
SoftwareVertexReader(u8 *base, VertexDecoder &vdecoder, u32 vertex_type, int vertex_count, const void *vertices, const void *indices, const TransformState &transformState, TransformUnit &transform)
|
||||
@@ -490,8 +758,13 @@ public:
|
||||
GetIndexBounds(indices, vertex_count, vertex_type, &lowerBound_, &upperBound_);
|
||||
if (vertex_count != 0) {
|
||||
const int count = upperBound_ - lowerBound_ + 1;
|
||||
const UVScale uvScale = LoadUVScaleOffset(gstate);
|
||||
const UVScale uvScale = UsesGEUVScale(vertex_type) ? UVScale{ 1.0f, 1.0f, 0.0f, 0.0f } : LoadUVScaleOffset(gstate);
|
||||
vdecoder.DecodeVerts(base, (const u8 *)vertices + vdecoder.VertexSize() * lowerBound_, &uvScale, count);
|
||||
const u8 *raw = (const u8 *)vertices + vdecoder.VertexSize() * lowerBound_;
|
||||
if (vdecoder.morphcount > 1 && !vdecoder.throughmode)
|
||||
ApplyGEMorph(base, vdecoder, raw, count);
|
||||
if (vdecoder.weighttype != 0 && !vdecoder.throughmode)
|
||||
ApplyGESkinning(base, vdecoder, raw, count);
|
||||
}
|
||||
|
||||
// If we're only using a subset of verts, it's better to decode with random access (usually.)
|
||||
@@ -599,7 +872,7 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
continue;
|
||||
|
||||
int tl = -1, br = -1;
|
||||
if (Rasterizer::DetectRectangleFromPair(binner_->State(), buf, &tl, &br)) {
|
||||
if (Rasterizer::DetectRectangleFromPair(binner_->State(), buf, &tl, &br) && Rasterizer::RectangleMatchesTriangles(binner_->State(), buf[tl].v, buf[br].v)) {
|
||||
Clipper::ProcessRect(buf[tl], buf[br], *binner_);
|
||||
} else {
|
||||
SendTriangle(cullType, &buf[0]);
|
||||
@@ -733,7 +1006,7 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
|
||||
// If a strip is effectively a rectangle, draw it as such!
|
||||
int tl = -1, br = -1;
|
||||
if (Rasterizer::DetectRectangleFromStrip(binner_->State(), data_, &tl, &br)) {
|
||||
if (Rasterizer::DetectRectangleFromStrip(binner_->State(), data_, &tl, &br) && Rasterizer::RectangleMatchesTriangles(binner_->State(), data_[tl].v, data_[br].v)) {
|
||||
Clipper::ProcessRect(data_[tl], data_[br], *binner_);
|
||||
start_vtx += 2;
|
||||
skip_count = 2;
|
||||
@@ -767,7 +1040,8 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
|
||||
int wind = (data_index_ - 1) % 2;
|
||||
CullType altCullType = cullType == CullType::OFF ? cullType : CullType((int)cullType ^ wind);
|
||||
SendTriangle(altCullType, &data_[0], provoking_index);
|
||||
// Odd triangles reach us in the opposite order from the GE's (gpu/probe exp45).
|
||||
SendTriangle(altCullType, &data_[0], provoking_index, wind != 0);
|
||||
}
|
||||
|
||||
// If this is from immediate-mode drawing, we always had one new vert (already in data_.)
|
||||
@@ -775,7 +1049,8 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
int provoking_index = (data_index_ - 1) % 3;
|
||||
int wind = (data_index_ - 1) % 2;
|
||||
CullType altCullType = cullType == CullType::OFF ? cullType : CullType((int)cullType ^ wind);
|
||||
SendTriangle(altCullType, &data_[0], provoking_index);
|
||||
// Odd triangles reach us in the opposite order from the GE's (gpu/probe exp45).
|
||||
SendTriangle(altCullType, &data_[0], provoking_index, wind != 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -800,7 +1075,7 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
}
|
||||
|
||||
int tl = -1, br = -1;
|
||||
if (Rasterizer::DetectRectangleFromFan(binner_->State(), data_, &tl, &br)) {
|
||||
if (Rasterizer::DetectRectangleFromFan(binner_->State(), data_, &tl, &br) && Rasterizer::RectangleMatchesTriangles(binner_->State(), data_[tl].v, data_[br].v)) {
|
||||
Clipper::ProcessRect(data_[tl], data_[br], *binner_);
|
||||
break;
|
||||
}
|
||||
@@ -819,7 +1094,8 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
|
||||
int wind = (data_index_ - 1) % 2;
|
||||
CullType altCullType = cullType == CullType::OFF ? cullType : CullType((int)cullType ^ wind);
|
||||
SendTriangle(altCullType, &data_[0], provoking_index);
|
||||
// Odd triangles reach us in the opposite order from the GE's (gpu/probe exp45).
|
||||
SendTriangle(altCullType, &data_[0], provoking_index, wind != 0);
|
||||
}
|
||||
|
||||
// If this is from immediate-mode drawing, we always had one new vert (already in data_.)
|
||||
@@ -827,7 +1103,7 @@ void TransformUnit::SubmitPrimitive(const void* vertices, const void* indices, G
|
||||
int wind = (data_index_ - 1) % 2;
|
||||
int provoking_index = 2 - wind;
|
||||
CullType altCullType = cullType == CullType::OFF ? cullType : CullType((int)cullType ^ wind);
|
||||
SendTriangle(altCullType, &data_[0], provoking_index);
|
||||
SendTriangle(altCullType, &data_[0], provoking_index, wind != 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -876,18 +1152,20 @@ void TransformUnit::SubmitImmVertex(const ClipVertexData &vert, SoftwareDrawEngi
|
||||
uint32_t vertTypeID = GetVertTypeID(gstate.vertType | GE_VTYPE_POS_FLOAT, gstate.getUVGenMode());
|
||||
// This now processes the step with shared logic, given the existing data_.
|
||||
isImmDraw_ = true;
|
||||
Clipper::SetCullXY(false);
|
||||
SubmitPrimitive(nullptr, nullptr, GE_PRIM_KEEP_PREVIOUS, 0, vertTypeID, nullptr, drawEngine);
|
||||
Clipper::SetCullXY(true);
|
||||
isImmDraw_ = false;
|
||||
}
|
||||
|
||||
void TransformUnit::SendTriangle(CullType cullType, const ClipVertexData *verts, int provoking) {
|
||||
void TransformUnit::SendTriangle(CullType cullType, const ClipVertexData *verts, int provoking, bool orderReversed) {
|
||||
if (cullType == CullType::OFF) {
|
||||
Clipper::ProcessTriangle(verts[0], verts[1], verts[2], verts[provoking], *binner_);
|
||||
Clipper::ProcessTriangle(verts[2], verts[1], verts[0], verts[provoking], *binner_);
|
||||
Clipper::ProcessTriangle(verts[0], verts[1], verts[2], verts[provoking], *binner_, orderReversed);
|
||||
Clipper::ProcessTriangle(verts[2], verts[1], verts[0], verts[provoking], *binner_, !orderReversed);
|
||||
} else if (cullType == CullType::CW) {
|
||||
Clipper::ProcessTriangle(verts[2], verts[1], verts[0], verts[provoking], *binner_);
|
||||
Clipper::ProcessTriangle(verts[2], verts[1], verts[0], verts[provoking], *binner_, !orderReversed);
|
||||
} else {
|
||||
Clipper::ProcessTriangle(verts[0], verts[1], verts[2], verts[provoking], *binner_);
|
||||
Clipper::ProcessTriangle(verts[0], verts[1], verts[2], verts[provoking], *binner_, orderReversed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,6 +1193,10 @@ void TransformUnit::FlushIfOverlap(GPUCommon *common, const char *reason, bool m
|
||||
Flush(common, reason);
|
||||
}
|
||||
|
||||
void TransformUnit::NotifyTexFlush() {
|
||||
binner_->NotifyTexFlush();
|
||||
}
|
||||
|
||||
void TransformUnit::NotifyClutUpdate(const void *src) {
|
||||
binner_->UpdateClut(src);
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "GPU/GPUCommon.h"
|
||||
#include "GPU/Software/SoftGpu.h"
|
||||
#include "GPU/Math3D.h"
|
||||
#include "GPU/Software/GEMath.h"
|
||||
|
||||
using namespace Math3D;
|
||||
|
||||
@@ -88,9 +89,9 @@ struct ClipVertexData {
|
||||
clippos = ::Lerp(a.clippos, b.clippos, t);
|
||||
// Ignore screenpos because Lerp() is only used pre-calculation of screenpos.
|
||||
v.texturecoords = ::Lerp(a.v.texturecoords, b.v.texturecoords, t);
|
||||
v.fogdepth = ::Lerp(a.v.fogdepth, b.v.fogdepth, t);
|
||||
|
||||
u16 t_int = (u16)(t * 256);
|
||||
// Colors and fog (already 8-bit per vertex) take t rounded to 1/256 (gpu/probe exp136).
|
||||
const int t_int = (int)(t * 256.0f + 0.5f);
|
||||
v.fogdepth = (float)(((int)(a.v.fogdepth * 256.0f) * (256 - t_int) + (int)(b.v.fogdepth * 256.0f) * t_int) >> 8) * (1.0f / 256.0f);
|
||||
v.color0 = LerpInt<Vec4<int>, 256>(Vec4<int>::FromRGBA(a.v.color0), Vec4<int>::FromRGBA(b.v.color0), t_int).ToRGBA();
|
||||
v.color1 = LerpInt<Vec3<int>, 256>(Vec3<int>::FromRGB(a.v.color1), Vec3<int>::FromRGB(b.v.color1), t_int).ToRGB();
|
||||
}
|
||||
@@ -105,6 +106,7 @@ struct ClipVertexData {
|
||||
|
||||
class VertexReader;
|
||||
|
||||
|
||||
class SoftwareDrawEngine;
|
||||
class SoftwareVertexReader;
|
||||
class StringWriter;
|
||||
@@ -117,8 +119,10 @@ public:
|
||||
~TransformUnit();
|
||||
|
||||
static WorldCoords ModelToWorldNormal(const ModelCoords& coords);
|
||||
static WorldCoords ModelToWorld(const ModelCoords& coords);
|
||||
static ScreenCoords ClipToScreen(const ClipCoords &coords, bool *outsideRangeFlag);
|
||||
// Where an edge from an inside vertex crosses the near plane, as the GE computes it.
|
||||
static float NearPlaneT(const ClipCoords &in, const ClipCoords &out);
|
||||
static ClipCoords NearPlanePoint(const ClipCoords &in, const ClipCoords &out, float t);
|
||||
static inline DrawingCoords ScreenToDrawing(int x, int y) {
|
||||
DrawingCoords ret;
|
||||
// When offset > coord, this is negative and force-scissors.
|
||||
@@ -137,6 +141,7 @@ public:
|
||||
void Flush(GPUCommon *common, const char *reason);
|
||||
void FlushIfOverlap(GPUCommon *common, const char *reason, bool modifying, uint32_t addr, uint32_t stride, uint32_t w, uint32_t h);
|
||||
void NotifyClutUpdate(const void *src);
|
||||
void NotifyTexFlush();
|
||||
|
||||
void GetStats(StringWriter &w);
|
||||
|
||||
@@ -145,7 +150,8 @@ public:
|
||||
|
||||
private:
|
||||
ClipVertexData ReadVertex(const VertexReader &vreader, const TransformState &state);
|
||||
void SendTriangle(CullType cullType, const ClipVertexData *verts, int provoking = 2);
|
||||
// orderReversed: verts are in the opposite order of how the GE takes the triangle (matters for clipping).
|
||||
void SendTriangle(CullType cullType, const ClipVertexData *verts, int provoking = 2, bool orderReversed = false);
|
||||
|
||||
u8 *decoded_ = nullptr;
|
||||
BinManager *binner_ = nullptr;
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
<ClInclude Include="..\..\GPU\Software\Sampler.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\SoftGpu.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\TransformUnit.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\GEMath.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
</ItemGroup>
|
||||
@@ -216,6 +217,7 @@
|
||||
<ClCompile Include="..\..\GPU\Software\Sampler.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\SoftGpu.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\TransformUnit.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\GEMath.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
<ClCompile Include="..\..\GPU\Software\Sampler.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\SoftGpu.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\TransformUnit.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\GEMath.cpp" />
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\RasterizerRectangle.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Software\RasterizerRegCache.cpp" />
|
||||
@@ -126,6 +127,7 @@
|
||||
<ClInclude Include="..\..\GPU\Software\Sampler.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\SoftGpu.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\TransformUnit.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\GEMath.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="..\..\GPU\Software\RasterizerRectangle.h" />
|
||||
|
||||
@@ -589,6 +589,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/GPU/Software/Clipper.cpp \
|
||||
$(SRC)/GPU/Software/DrawPixel.cpp.arm \
|
||||
$(SRC)/GPU/Software/FuncId.cpp \
|
||||
$(SRC)/GPU/Software/GEMath.cpp \
|
||||
$(SRC)/GPU/Software/Lighting.cpp \
|
||||
$(SRC)/GPU/Software/Rasterizer.cpp.arm \
|
||||
$(SRC)/GPU/Software/RasterizerRectangle.cpp.arm \
|
||||
@@ -1059,6 +1060,7 @@ ifeq ($(UNITTEST),1)
|
||||
$(SRC)/unittest/TestLzrc.cpp \
|
||||
$(SRC)/unittest/TestMpegCsc.cpp \
|
||||
$(SRC)/unittest/TestSplineTessellation.cpp \
|
||||
$(SRC)/unittest/TestGEMath.cpp \
|
||||
$(SRC)/unittest/TestZipSlip.cpp \
|
||||
$(SRC)/unittest/UnitTest.cpp
|
||||
|
||||
|
||||
@@ -211,6 +211,23 @@ binary, or it runs your native build instead. The full suite takes a couple of m
|
||||
qemu reports LSX and LASX as present, so CI only exercises the LoongArch vector paths.
|
||||
The scalar fallbacks never run here, so passing tells you nothing about them.
|
||||
|
||||
Without a cross toolchain (on macOS, say) two things still help:
|
||||
|
||||
- **A syntax check of the other JITs.** The RISC-V and LoongArch backends (`Core/MIPS/RiscV`,
|
||||
`Core/MIPS/LoongArch64` and their emitters) have no architecture guards, so the host compiler can check
|
||||
them: `clang++ -fsyntax-only` per file, with the `-I`/`-D`/`-std`/`-isystem` flags of any Core file from
|
||||
`compile_commands.json`, plus `-I<repo> -I<repo>/ext -I<repo>/Common` (the database's include paths are
|
||||
relative to the build directory). Check that a copy with a deliberate syntax error does report errors: zsh
|
||||
doesn't word-split a `$FLAGS` variable, which once made a broken check look clean. This proves the code
|
||||
compiles, nothing about its output.
|
||||
- **The portable software-renderer paths.** riscv64 and loongarch64 run the software renderer's non-SSE,
|
||||
non-NEON C++ paths, so their test failures reproduce natively: wrap the two NEON defines in the
|
||||
`__aarch64__` block of `ppsspp_config.h` in `#ifndef PPSSPP_TEST_NO_SIMD`, configure a separate
|
||||
`build-nosimd` with `-DHEADLESS=ON` and `-DPPSSPP_TEST_NO_SIMD` in `CMAKE_C_FLAGS` and `CMAKE_CXX_FLAGS`,
|
||||
and build `PPSSPPHeadless` there. Delete it and revert `ppsspp_config.h` afterwards, since `test.py` keeps
|
||||
picking the newest binary. A failure that's the same on both architectures points at the portable path, not
|
||||
the JITs.
|
||||
|
||||
## Quick rebuild on Linux
|
||||
|
||||
You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut:
|
||||
|
||||
@@ -215,6 +215,11 @@ For game runs, prefer a hardware backend: 30 emulated seconds of God of War take
|
||||
The pspautotests pass on Vulkan and OpenGL except for 17 GPU tests, whose references are hardware screenshots
|
||||
that the hardware backends don't match exactly.
|
||||
|
||||
To smoke-test a CPU or GPU change on real games, run the old and the new binary on the same games with
|
||||
`--timeout-emulated` and `--screenshot-save=`, and compare the screenshots. Most games are deterministic from
|
||||
run to run, so a different screenshot means different behaviour. God of War: Chains of Olympus isn't: its menu
|
||||
animation follows wall-clock time, so even the same binary lands on different frames.
|
||||
|
||||
Pass `--graphics` explicitly even when you want the default, so a copied command line doesn't depend on it. If a
|
||||
run goes silent with no CPU use, a host thread is blocked, and neither `--timeout-wall` nor `--timeout-emulated`
|
||||
will end it, as both are only checked when the emulation loop comes around.
|
||||
|
||||
@@ -217,3 +217,57 @@ new flag combinations, as long as the headless binary supports them.
|
||||
The CI test set (dumps and references) is maintained separately from the
|
||||
runner; the `frametests/` submodule is just one of several possible test sets
|
||||
- custom machines can use their own (possibly much larger) ones.
|
||||
|
||||
## Replaying a dump on a real PSP
|
||||
|
||||
The references above are PPSSPP's own renders, so they catch regressions, not
|
||||
wrong output. To see what the hardware draws, replay the dump on a PSP:
|
||||
`pspautotests/utils/ppdmp-playback` is a PSP program that runs a `.ppdmp` natively
|
||||
(all dump versions PPSSPP writes), and its `run.py` drives it over PSPLink from
|
||||
the host:
|
||||
|
||||
```bash
|
||||
cd pspautotests
|
||||
python3 utils/ppdmp-playback/run.py --out /tmp/shots \
|
||||
--headless ../build/PPSSPPHeadless "../frametests/dumps/Depth/21391 Coded arms ULUS10019_0001.zip"
|
||||
```
|
||||
|
||||
It saves what the PSP displays as `NAME-psp.png`, and with `--headless` renders
|
||||
the same dump in PPSSPPHeadless (`--graphics=software` by default), saves
|
||||
`NAME-ppsspp.png` and prints the MSE between the two. Each frame stays on the
|
||||
PSP's screen for 1.5 seconds (`--hold`). It needs the PSPSDK (it rebuilds the PRX) and a PSP in
|
||||
PSPLink with `usbhostfs_pc` serving the pspautotests root, the same setup as
|
||||
[pspautotests-hardware.md](pspautotests-hardware.md); like every hardware run,
|
||||
one at a time. See its README for replaying a subset of the primitives.
|
||||
|
||||
A zip holding several dumps plays its last `.ppdmp`, in PPSSPP and in the replayer, so extract that one
|
||||
when you need the file itself.
|
||||
|
||||
If a replay hangs, `run.py` resets the PSP and prints the last lines the replayer wrote. `--progress=N`
|
||||
prints a line every N dump commands and `--trace-from=N` every command from N on, and `--cmds=N` replays
|
||||
only the first N dump commands, which bisects a hang in a few runs. The replayer's Makefile has no header
|
||||
dependencies: after changing `replay.h`, delete the `.o` files, or a stale object corrupts the `Replay`
|
||||
object.
|
||||
|
||||
To find the draw where the two diverge, stop both at the same primitive:
|
||||
`run.py --end=N` on the PSP and `--replay-end=N` on PPSSPPHeadless. A draw into
|
||||
an offscreen buffer can be compared directly: `run.py --display=ADDR,STRIDE,FMT`
|
||||
shows and screenshots that buffer instead of the display (e.g.
|
||||
`--display=04000000,512,3` for an 8888 target at the start of VRAM), and
|
||||
PPSSPPHeadless takes `--screenshot-render-target` to save the current render
|
||||
target.
|
||||
|
||||
PPSSPP's software renderer now matches the PSP bit for bit on nearly every dump
|
||||
here, so a difference is worth chasing. Hardware backends still differ slightly
|
||||
(filtering, edge rules), so compare their MSEs between versions instead.
|
||||
|
||||
To record a dump from a running game without the UI, use the WebSocket debugger's
|
||||
`gpu.record.dump` through `Tools/wsdbg`. Its reply comes later, so wait for it:
|
||||
|
||||
```bash
|
||||
printf ':sleep 6\ngpu.record.dump\n:wait gpu.record.dump 60\n:quit\n' | \
|
||||
Tools/wsdbg/target/release/wsdbg --sync --compact --quiet --launch \
|
||||
build/PPSSPPHeadless game.iso --state=STATE.ppst --graphics=software --debugger-run=0 > out.txt
|
||||
```
|
||||
|
||||
The reply's `uri` is a base64 `data:` URI holding the `.ppdmp`.
|
||||
@@ -182,6 +182,18 @@ python3 test.py --graphics=software io/shortname/shortname
|
||||
- **A test that hangs leaves the PSP wedged.** `gentest.py` issues `pspsh -e reset` after a
|
||||
timeout, but if you ran the PRX by hand, do that yourself. Default timeout is 10s; raise it with
|
||||
`-t SECONDS`.
|
||||
- **Bound every wait on the PSP, including the whole command.** A test that hasn't finished in 10
|
||||
seconds is hung; a longer timeout only delays finding out which call blocks. To check whether the
|
||||
PSP is alive, run `pspsh -p 3000 -e ls` in the background and give up after 10 seconds (macOS has
|
||||
no `timeout` command). `gentest.py -t` doesn't cap the run either: its recovery `pspsh -e reset`
|
||||
blocks forever if the PSP is really gone, so put a limit on the shell command as well.
|
||||
- **A PSP that doesn't answer may only need the host side restarted.** If it still enumerates over
|
||||
USB (`ioreg -p IOUSB -l -w 0 | grep -i "USB Product Name"` shows "PSP Type B" on macOS), kill
|
||||
`usbhostfs_pc`, start it again from the pspautotests root, and probe once more. Otherwise someone has
|
||||
to restart PSPLink or reconnect the cable.
|
||||
- **Never let a test exit with a utility dialog still active.** A dialog left in INIT, RUNNING or
|
||||
FINISHED keeps the utility module busy, and the next `reset` hangs PSPLink until it's restarted by
|
||||
hand. In utility tests, bound every wait, and run a case that might not finish last and alone.
|
||||
- **`tests_to_generate` in `gentest.py`** - the list used when you pass no arguments - is stale;
|
||||
several paths in it no longer exist. Always name the test you want.
|
||||
- **`--sdkver` matters for some APIs.** `gentest.py --sdkver=6060010 --sdkver-func=606` makes the
|
||||
@@ -189,6 +201,10 @@ python3 test.py --graphics=software io/shortname/shortname
|
||||
known version reporting which ones behave differently - a fast way to find version-gated
|
||||
behavior. A test can also call `sceKernelSetCompiledSdkVersion*()` mid-run to cover several
|
||||
versions in one `.expected`.
|
||||
- **On a PSP, printed output is also drawn on the screen**, into the display framebuffer. A GPU test that
|
||||
renders into that framebuffer and prints between draws reads its own text back: each line changed the
|
||||
alpha of a few pixels under it in the next draw, deterministically on the PSP and never in PPSSPP. Set
|
||||
`HAS_DISPLAY = 0` (`extern "C" int HAS_DISPLAY;`, from `common.c`) after setting up the display.
|
||||
- **The module must be named `TESTMODULE`** (`common.c` does this) or `gentest.py` prints the load
|
||||
line as an unexpected result.
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ This is a developer tool for understanding the PSP, not something a user ever ru
|
||||
--re-out /tmp/re
|
||||
```
|
||||
|
||||
To copy a file out of a disc image as it is (a movie, a data archive), use `--dump-file
|
||||
disc0:/PSP_GAME/USRDIR/X.PMF --dump-file-out OUT game.iso`, which also lists a directory. Use it, or
|
||||
`--re-module` for the game's own binaries, rather than writing an ISO or CSO reader.
|
||||
|
||||
`--re-module` takes either a host path or a PSP-style `flash0:/kd/foo.prx`, which is resolved
|
||||
against the configured NAND directory - so `--memstick` (or `--nand`) has to point at a dump.
|
||||
PPSSPP can produce one itself from a firmware updater with `--unpack-updater`; see
|
||||
|
||||
+1
-1
Submodule frametests updated: 49a8baadc9...a21764de0c.
+26
-1
@@ -43,6 +43,7 @@
|
||||
#include "Common/File/VFS/ZipFileReader.h"
|
||||
#include "Common/File/VFS/DirectoryReader.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "GPU/Debugger/Playback.h"
|
||||
#include "Common/GPU/GraphicsContext.h"
|
||||
#include "Common/Net/Resolve.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
@@ -79,6 +80,8 @@
|
||||
|
||||
static Path g_comparisonScreenshot;
|
||||
static Path g_screenshotSavePath;
|
||||
static Path g_depthSavePath;
|
||||
static bool g_screenshotRenderTarget = false;
|
||||
static Path g_screenshotDiffPath;
|
||||
static bool g_screenshotSaveKeepAlpha = false;
|
||||
static bool g_screenshotSaved = false;
|
||||
@@ -223,7 +226,7 @@ void SendDebugScreenshot(const DebugScreenshotDesc &desc) {
|
||||
const static u32 FRAME_HEIGHT = 272;
|
||||
|
||||
GPUDebugBuffer buffer;
|
||||
gpu->GetCurrentFramebuffer(buffer, GPU_DBG_FRAMEBUF_DISPLAY);
|
||||
gpu->GetCurrentFramebuffer(buffer, g_screenshotRenderTarget ? GPU_DBG_FRAMEBUF_RENDER : GPU_DBG_FRAMEBUF_DISPLAY);
|
||||
const std::vector<u32> pixels = TranslateDebugBufferToCompare(&buffer, FRAME_STRIDE, FRAME_HEIGHT);
|
||||
|
||||
// If a screenshot save path is set, save unconditionally.
|
||||
@@ -235,6 +238,21 @@ void SendDebugScreenshot(const DebugScreenshotDesc &desc) {
|
||||
SendAndCollectOutput("Screenshot saved to: " + g_screenshotSavePath.ToVisualString() + "\n");
|
||||
}
|
||||
|
||||
if (!g_depthSavePath.empty()) {
|
||||
GPUDebugBuffer depth;
|
||||
if (gpu->GetCurrentDepthbuffer(depth) && depth.GetFormat() == GPU_DBG_FORMAT_16BIT) {
|
||||
FILE *f = File::OpenCFile(g_depthSavePath, "wb");
|
||||
if (f) {
|
||||
const u32 header[2] = { depth.GetStride(), depth.GetHeight() };
|
||||
fwrite(header, sizeof(header), 1, f);
|
||||
fwrite(depth.GetData(), 2, depth.GetStride() * depth.GetHeight(), f);
|
||||
fclose(f);
|
||||
}
|
||||
} else {
|
||||
SendAndCollectOutput("Depth buffer not saved: needs a 16-bit depth buffer (the software renderer)\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Only compare if we have a reference.
|
||||
if (g_comparisonScreenshot.empty()) {
|
||||
return;
|
||||
@@ -1115,6 +1133,13 @@ int main(int argc, const char* argv[]) {
|
||||
if (cmdLineOptions.screenshotFilenameSave.has_value()) {
|
||||
SetScreenshotSavePath(Path(std::string(cmdLineOptions.screenshotFilenameSave.value())));
|
||||
}
|
||||
if (cmdLineOptions.replayEnd.has_value()) {
|
||||
GPURecord::SetReplayDrawLimit(cmdLineOptions.replayEnd.value());
|
||||
}
|
||||
g_screenshotRenderTarget = cmdLineOptions.screenshotRenderTarget.value_or(false);
|
||||
if (cmdLineOptions.depthFilenameSave.has_value()) {
|
||||
g_depthSavePath = Path(std::string(cmdLineOptions.depthFilenameSave.value()));
|
||||
}
|
||||
if (cmdLineOptions.screenshotFilenameDiff.has_value()) {
|
||||
g_screenshotDiffPath = Path(std::string(cmdLineOptions.screenshotFilenameDiff.value()));
|
||||
}
|
||||
|
||||
@@ -585,6 +585,7 @@ SOURCES_CXX += \
|
||||
$(GPUDIR)/Common/DepthRaster.cpp \
|
||||
$(GPUDIR)/Common/StencilCommon.cpp \
|
||||
$(GPUDIR)/Software/TransformUnit.cpp \
|
||||
$(GPUDIR)/Software/GEMath.cpp \
|
||||
$(GPUDIR)/Software/SoftGpu.cpp \
|
||||
$(GPUDIR)/Software/Sampler.cpp \
|
||||
$(GPUDIR)/GeConstants.cpp \
|
||||
|
||||
+1
-1
Submodule pspautotests updated: 8c0d77c7d2...1f078c578f.
@@ -205,7 +205,9 @@ tests_good = [
|
||||
"gpu/bounding/vertexaddr",
|
||||
"gpu/bounding/viewport",
|
||||
"gpu/callbacks/ge_callbacks",
|
||||
"gpu/clipping/guardband",
|
||||
"gpu/clipping/homogeneous",
|
||||
"gpu/clipping/xycull",
|
||||
"gpu/clut/address",
|
||||
"gpu/clut/masks",
|
||||
"gpu/clut/offset",
|
||||
@@ -216,31 +218,58 @@ tests_good = [
|
||||
"gpu/commands/blocktransfer",
|
||||
"gpu/commands/cull",
|
||||
"gpu/commands/fog",
|
||||
"gpu/commands/light",
|
||||
"gpu/commands/material",
|
||||
"gpu/complex/complex",
|
||||
"gpu/depth/precision",
|
||||
"gpu/depth/transformprecision",
|
||||
"gpu/displaylist/alignment",
|
||||
"gpu/dither/dither",
|
||||
"gpu/exact/coverage",
|
||||
"gpu/exact/curves",
|
||||
"gpu/exact/lighting",
|
||||
"gpu/exact/lines",
|
||||
"gpu/exact/nearclip",
|
||||
"gpu/exact/pixel",
|
||||
"gpu/exact/planes",
|
||||
"gpu/exact/texture",
|
||||
"gpu/exact/transform",
|
||||
"gpu/filtering/linear",
|
||||
"gpu/filtering/mipmaplinear",
|
||||
"gpu/filtering/nearest",
|
||||
"gpu/filtering/precisionlinear2d",
|
||||
"gpu/filtering/precisionlinear3d",
|
||||
"gpu/filtering/precisionnearest2d",
|
||||
"gpu/filtering/precisionnearest3d",
|
||||
"gpu/ge/break",
|
||||
"gpu/ge/breakwait",
|
||||
"gpu/ge/callbackstate",
|
||||
"gpu/ge/context",
|
||||
"gpu/ge/edram",
|
||||
"gpu/ge/enqueueparam",
|
||||
"gpu/ge/get",
|
||||
"gpu/ge/intrsuspend",
|
||||
"gpu/ge/queue",
|
||||
"gpu/ge/queue2",
|
||||
"gpu/lighting/shademap",
|
||||
"gpu/lighting/specular",
|
||||
"gpu/primitives/bezier",
|
||||
"gpu/primitives/continue",
|
||||
"gpu/primitives/immediate",
|
||||
"gpu/primitives/indices",
|
||||
"gpu/primitives/invalidprim",
|
||||
"gpu/primitives/lines",
|
||||
"gpu/primitives/linestrip",
|
||||
"gpu/primitives/points",
|
||||
"gpu/primitives/rectangles",
|
||||
"gpu/primitives/spline",
|
||||
"gpu/primitives/trianglefan",
|
||||
"gpu/primitives/trianglestrip",
|
||||
"gpu/primitives/triangles",
|
||||
"gpu/reflection/reflection",
|
||||
"gpu/rendertarget/copy",
|
||||
"gpu/rendertarget/depal",
|
||||
"gpu/rendertarget/rendertarget",
|
||||
"gpu/signals/pause",
|
||||
"gpu/signals/pause2",
|
||||
"gpu/signals/suspend",
|
||||
@@ -256,11 +285,17 @@ tests_good = [
|
||||
"gpu/texfunc/decal",
|
||||
"gpu/texfunc/modulate",
|
||||
"gpu/texfunc/replace",
|
||||
"gpu/texmtx/normals",
|
||||
"gpu/texmtx/prims",
|
||||
"gpu/texmtx/source",
|
||||
"gpu/texmtx/uvs",
|
||||
"gpu/textures/mipmap",
|
||||
"gpu/textures/rotate",
|
||||
"gpu/textures/size",
|
||||
"gpu/transfer/invalid",
|
||||
"gpu/transfer/mirrors",
|
||||
"gpu/transfer/overlap",
|
||||
"gpu/triangle/triangle",
|
||||
"gpu/vertices/colors",
|
||||
"gpu/vertices/morph",
|
||||
# "gpu/vertices/texcoords", # See issue #19093
|
||||
@@ -551,39 +586,15 @@ tests_next = [
|
||||
"font/shadowglyphimage",
|
||||
"font/shadowglyphimageclip",
|
||||
"font/shadowinfo",
|
||||
"gpu/clipping/guardband",
|
||||
"gpu/commands/light",
|
||||
"gpu/depth/precision",
|
||||
"gpu/displaylist/state",
|
||||
"gpu/filtering/linear",
|
||||
"gpu/filtering/nearest",
|
||||
"gpu/filtering/precisionlinear2d",
|
||||
"gpu/filtering/precisionlinear3d",
|
||||
"gpu/filtering/precisionnearest2d",
|
||||
"gpu/filtering/precisionnearest3d",
|
||||
"gpu/ge/edramswizzle",
|
||||
"gpu/ge/get",
|
||||
"gpu/primitives/bezier",
|
||||
"gpu/primitives/continue",
|
||||
"gpu/primitives/immediate",
|
||||
"gpu/primitives/indices32",
|
||||
"gpu/primitives/lines",
|
||||
"gpu/primitives/linestrip",
|
||||
"gpu/primitives/spline",
|
||||
"gpu/reflection/reflection",
|
||||
"gpu/rendertarget/rendertarget",
|
||||
"gpu/signals/continue",
|
||||
# Old SDK: a stall update from inside a SUSPEND handler is remembered, but not applied to the GE. See docs/sceGe.md.
|
||||
"gpu/signals/handlercalls",
|
||||
"gpu/signals/jumps",
|
||||
"gpu/signals/simple",
|
||||
"gpu/simple/simple",
|
||||
"gpu/texmtx/normals",
|
||||
"gpu/texmtx/prims",
|
||||
"gpu/texmtx/source",
|
||||
"gpu/texmtx/uvs",
|
||||
"gpu/textures/size",
|
||||
"gpu/triangle/triangle",
|
||||
"intr/waits",
|
||||
"sysmem/kernel/heapgrow",
|
||||
"io/file/file",
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
// 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/.
|
||||
|
||||
// The GE arithmetic in GPU/Software/GEMath.h against values measured on a PSP. The probes are in
|
||||
// pspautotests/tests/gpu/probe (expNN.py), with notes in the ppsspp-re repository: reciprocals from
|
||||
// exp15 and exp38-41, the AA line pixels from exp112. The rest are cases those probes pinned down.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "GPU/Software/GEMath.h"
|
||||
|
||||
#include "unittest/UnitTest.h"
|
||||
|
||||
static float FromBits(uint32_t u) {
|
||||
float f;
|
||||
memcpy(&f, &u, sizeof(f));
|
||||
return f;
|
||||
}
|
||||
|
||||
static uint32_t ToBits(float f) {
|
||||
uint32_t u;
|
||||
memcpy(&u, &f, sizeof(u));
|
||||
return u;
|
||||
}
|
||||
|
||||
static bool TestFloat24() {
|
||||
// 15 stored mantissa bits, truncated toward zero.
|
||||
EXPECT_EQ_FLOAT(TruncateToFloat24(1.0f + ldexpf(1.0f, -16)), 1.0f);
|
||||
EXPECT_EQ_FLOAT(TruncateToFloat24(1.0f + ldexpf(1.0f, -15)), 1.0f + ldexpf(1.0f, -15));
|
||||
EXPECT_EQ_FLOAT(TruncateToFloat24(-1.0f - ldexpf(3.0f, -17)), -1.0f);
|
||||
EXPECT_EQ_FLOAT(TruncateToFloat24(-1.0f - ldexpf(5.0f, -17)), -1.0f - ldexpf(1.0f, -15));
|
||||
// The product is truncated in double, not rounded to a float first.
|
||||
EXPECT_EQ_FLOAT(ProductToFloat24(1.0 + ldexp(1.0, -15) + ldexp(1.0, -16) - ldexp(1.0, -40)), 1.0f + ldexpf(1.0f, -15));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGERecip() {
|
||||
// 1 / (1 + i / 32768) in units of 2^-16, measured on a PSP for every mantissa (exp15).
|
||||
static const int measured[][2] = {
|
||||
{ 0, 65536 }, { 1, 65534 }, { 255, 65030 }, { 256, 65027 }, { 909, 63768 }, { 1149, 63316 }, { 2816, 60350 }, { 4727, 57274 },
|
||||
{ 8168, 52459 }, { 8423, 52135 }, { 8953, 51472 }, { 10218, 49957 }, { 10809, 49280 }, { 13408, 46507 }, { 13869, 46047 }, { 14837, 45110 },
|
||||
{ 15497, 44494 }, { 16762, 43357 }, { 17635, 42606 }, { 18007, 42294 }, { 21093, 39871 }, { 24689, 37376 }, { 24729, 37350 }, { 26789, 36057 },
|
||||
{ 26951, 35960 }, { 27112, 35863 }, { 27453, 35660 }, { 28359, 35131 }, { 28463, 35072 }, { 31067, 33641 }, { 31123, 33612 }, { 32767, 32769 },
|
||||
};
|
||||
for (const auto &m : measured) {
|
||||
const float w = FromBits(0x3F800000 | (m[0] << 8));
|
||||
const float expected = ldexpf((float)m[1], -16);
|
||||
if (GERecip(w) != expected) {
|
||||
printf("GERecip(1 + %d / 32768) = %.9f, PSP %.9f\n", m[0], GERecip(w), expected);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Sign and exponent are carried over.
|
||||
EXPECT_EQ_FLOAT(GERecip(-2.0f), -0.5f);
|
||||
EXPECT_EQ_FLOAT(GERecip(0.25f), 4.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGERsqrt() {
|
||||
// The table for odd exponents stores 1 / sqrt(2 * 1.i) (exp69).
|
||||
EXPECT_EQ_FLOAT(GERsqrt(1.0f), 1.0f);
|
||||
EXPECT_EQ_FLOAT(GERsqrt(4.0f), 0.5f);
|
||||
EXPECT_EQ_FLOAT(GERsqrt(0.25f), 2.0f);
|
||||
EXPECT_EQ_FLOAT(GERsqrt(2.0f), ldexpf(46340.0f, -16));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGEAdd() {
|
||||
// No guard bits: the smaller term is truncated (toward zero) to the larger one's last bit.
|
||||
EXPECT_EQ_FLOAT(GEAddFloat24(1.0f, ldexpf(3.0f, -17)), 1.0f);
|
||||
EXPECT_EQ_FLOAT(GEAddFloat24(1.0f, ldexpf(5.0f, -17)), 1.0f + ldexpf(1.0f, -15));
|
||||
EXPECT_EQ_FLOAT(GEAddFloat24(1.0f, -ldexpf(5.0f, -17)), 1.0f - ldexpf(1.0f, -15));
|
||||
EXPECT_EQ_FLOAT(GEAddFloat24(0.0f, 1.0f + ldexpf(1.0f, -20)), 1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGEDot() {
|
||||
// A row is summed in one go: every product truncated at the largest term's last bit, so two small
|
||||
// terms that would carry together in pairwise adds are both dropped.
|
||||
EXPECT_EQ_FLOAT(GEDot(Vec3f(1.0f, 1.0f, 1.0f), Vec3f(1.0f, ldexpf(1.0f, -16), ldexpf(1.0f, -16))), 1.0f);
|
||||
EXPECT_EQ_FLOAT(GEDot(Vec3f(2.0f, 3.0f, 0.0f), Vec3f(0.5f, 0.25f, 9.0f)), 1.75f);
|
||||
// Normalization goes through GERsqrt, and returns the length the same way.
|
||||
Vec3f v(3.0f, 4.0f, 0.0f);
|
||||
const float len = GENormalize(v);
|
||||
EXPECT_EQ_FLOAT(len, ProductToFloat24(25.0 * GERsqrt(25.0f)));
|
||||
EXPECT_EQ_FLOAT(v.x, ProductToFloat24(3.0 * GERsqrt(25.0f)));
|
||||
EXPECT_EQ_FLOAT(v.y, ProductToFloat24(4.0 * GERsqrt(25.0f)));
|
||||
EXPECT_TRUE(fabsf(len - 5.0f) < 0.001f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGESetupRecip() {
|
||||
// The triangle setup's reciprocal for 16-bit significand indices, measured on a PSP (exp38-41).
|
||||
static const int measured[][2] = {
|
||||
{ 0, 65536 }, { 2, 65534 }, { 254, 65282 }, { 256, 65280 }, { 2594, 63040 }, { 4086, 61689 }, { 7588, 58735 }, { 10520, 56471 },
|
||||
{ 18132, 51333 }, { 19190, 50692 }, { 21126, 49560 }, { 23770, 48092 }, { 24120, 47905 }, { 27582, 46123 }, { 27812, 46010 }, { 27998, 45918 },
|
||||
{ 29846, 45029 }, { 30100, 44909 }, { 33802, 43235 }, { 34636, 42876 }, { 35440, 42534 }, { 40160, 40635 }, { 40842, 40374 }, { 47082, 38137 },
|
||||
{ 47536, 37984 }, { 50630, 36973 }, { 50882, 36892 }, { 57148, 35008 }, { 60100, 34186 }, { 60570, 34058 }, { 63526, 33278 }, { 65534, 32768 },
|
||||
};
|
||||
for (const auto &m : measured) {
|
||||
int e = 0;
|
||||
const int64_t q = GESetupRecip(65536 + m[0], &e);
|
||||
if (q != m[1] || e != 16) {
|
||||
printf("GESetupRecip(index %d) = %lld (e %d), PSP %d\n", m[0], (long long)q, e, m[1]);
|
||||
return false;
|
||||
}
|
||||
// Only the top 16 bits count.
|
||||
const int64_t q2 = GESetupRecip((uint64_t)(65536 + m[0]) << 5 | 31, &e);
|
||||
EXPECT_EQ_INT(q2, m[1]);
|
||||
EXPECT_EQ_INT(e, 21);
|
||||
}
|
||||
// exp54's Gouraud triangle (det 854491) needs at least 40211 here; exp39 and exp41 bound it from above.
|
||||
int e = 0;
|
||||
EXPECT_EQ_INT(GESetupRecip(65536 + 41275, &e), 40211);
|
||||
// Blade Dancer's large triangle (det 9668772) needs at least 56859 for its depth (frame dump, index 10001).
|
||||
EXPECT_EQ_INT(GESetupRecip(9668772, &e), 56859);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGELog16() {
|
||||
// The exponent and the top 4 mantissa bits, in 1/16 (exp58-60).
|
||||
EXPECT_EQ_INT(GELog16(1.0f), 0);
|
||||
EXPECT_EQ_INT(GELog16(2.0f), 16);
|
||||
EXPECT_EQ_INT(GELog16(0.5f), -16);
|
||||
EXPECT_EQ_INT(GELog16(1.5f), 8);
|
||||
EXPECT_EQ_INT(GELog16(0.75f), -8);
|
||||
EXPECT_EQ_INT(GELog16(1.0625f), 1);
|
||||
EXPECT_EQ_INT(GELog16(1.0624f), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestTexCoordPrecision() {
|
||||
// A UV plane keeps 15 significant bits of a lone value (exp83).
|
||||
EXPECT_EQ_FLOAT(GETruncateTexCoord(300015.75f), 300000.0f);
|
||||
EXPECT_EQ_FLOAT(GETruncateTexCoord(-300015.75f), -300000.0f);
|
||||
EXPECT_EQ_FLOAT(GETruncateTexCoord(100.75f), 100.75f);
|
||||
// u = s * R(q) keeps 24 significant bits, truncated (exp82).
|
||||
EXPECT_EQ_FLOAT(GEUVProduct(1.0 + ldexp(1.0, -23) + ldexp(1.0, -30)), 1.0f + ldexpf(1.0f, -23));
|
||||
EXPECT_EQ_FLOAT(GEUVProduct(1.0 + ldexp(1.0, -24)), 1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGELightPow() {
|
||||
// Mitchell's approximation: straight lines between powers of two (exp98, exp102).
|
||||
EXPECT_EQ_FLOAT(GELightPow(0.5f, 2.0f), 0.25f);
|
||||
EXPECT_EQ_FLOAT(GELightPow(0.75f, 2.0f), 0.5f);
|
||||
EXPECT_EQ_FLOAT(GELightPow(0.3f, 0.0f), 1.0f);
|
||||
EXPECT_EQ_FLOAT(GELightPow(-0.5f, 2.0f), -0.5f);
|
||||
// The exponent times the bits is exact, truncated toward zero to units of 16: a rounded float
|
||||
// multiply gives 0x3DA64400, and flooring 0x3F69FFFC (one step lower in the 8-bit factor, exp102).
|
||||
EXPECT_EQ_HEX(ToBits(GELightPow(FromBits(0x3DC22200), 1.0625f)), 0x3DA64420U);
|
||||
EXPECT_EQ_HEX(ToBits(GELightPow(FromBits(0x3F35E500), 0.296875f)), 0x3F6A0000U);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGELightColor() {
|
||||
// 8-bit color products and scaling, ((2a + 1) * (2b + 1)) >> 10 (exp61-63; exp62 all 65536 pairs).
|
||||
// GELightColorProduct takes the colors already expanded as 2c + 1.
|
||||
const Vec4<int> white = Vec4<int>::AssignToAll(2 * 255 + 1);
|
||||
EXPECT_EQ_INT(GELightColorProduct(white, white).x, 255);
|
||||
EXPECT_EQ_INT(GELightColorProduct(Vec4<int>::AssignToAll(2 * 128 + 1), Vec4<int>::AssignToAll(2 * 200 + 1)).x, 100);
|
||||
EXPECT_EQ_INT(GELightColorProduct(Vec4<int>::AssignToAll(1), Vec4<int>::AssignToAll(1)).x, 0);
|
||||
// A factor becomes s = floor(256 f), expanded like a color; 1 leaves the color alone.
|
||||
EXPECT_EQ_INT(GELightColorScale(Vec4<int>::AssignToAll(200), 1.0f).x, 200);
|
||||
EXPECT_EQ_INT(GELightColorScale(Vec4<int>::AssignToAll(200), 0.5f).x, 100);
|
||||
EXPECT_EQ_INT(GELightColorScale(Vec4<int>::AssignToAll(255), 0.999f).x, 255);
|
||||
EXPECT_EQ_INT(GELightColorScale(Vec4<int>::AssignToAll(255), 0.99f).x, 253);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestGELineCoverageAlpha() {
|
||||
// Antialiased line pixels measured on a PSP (exp112): line ends in subpixels, the pixel, its alpha.
|
||||
// The first three have the offset exactly on a step, which the reciprocal decides.
|
||||
static const int measured[][7] = {
|
||||
{ 5832, 1205, 6108, 1039, 364, 75, 80 }, { 2470, 191, 2544, 154, 158, 9, 31 }, { 2470, 191, 2544, 154, 156, 10, 31 },
|
||||
{ 328, 240, 198, 171, 13, 11, 73 }, { 7310, 112, 7502, 157, 461, 8, 22 }, { 5529, 766, 5559, 569, 347, 37, 37 }, { 4038, 929, 4232, 1136, 253, 59, 71 },
|
||||
{ 2801, 1456, 2632, 1657, 172, 94, 31 }, { 1616, 2013, 1699, 1796, 101, 124, 127 }, { 2279, 2535, 2310, 2229, 144, 141, 40 }, { 7629, 2356, 7550, 2350, 474, 147, 20 },
|
||||
{ 6299, 2652, 6609, 2836, 403, 171, 109 }, { 4407, 3243, 4531, 3239, 275, 202, 80 },
|
||||
};
|
||||
for (const auto &m : measured) {
|
||||
const int a = GELineCoverageAlpha(m[0], m[1], m[2], m[3], m[4], m[5]);
|
||||
if (a != m[6]) {
|
||||
printf("GELineCoverageAlpha((%d, %d) - (%d, %d), pixel %d, %d) = %d, PSP %d\n", m[0], m[1], m[2], m[3], m[4], m[5], a, m[6]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TestGEMath() {
|
||||
bool ok = true;
|
||||
ok = TestFloat24() && ok;
|
||||
ok = TestGERecip() && ok;
|
||||
ok = TestGERsqrt() && ok;
|
||||
ok = TestGEAdd() && ok;
|
||||
ok = TestGEDot() && ok;
|
||||
ok = TestGESetupRecip() && ok;
|
||||
ok = TestGELog16() && ok;
|
||||
ok = TestTexCoordPrecision() && ok;
|
||||
ok = TestGELightPow() && ok;
|
||||
ok = TestGELightColor() && ok;
|
||||
ok = TestGELineCoverageAlpha() && ok;
|
||||
return ok;
|
||||
}
|
||||
@@ -2943,6 +2943,7 @@ bool TestZipSlip();
|
||||
bool TestLzrc();
|
||||
bool TestMpegCsc();
|
||||
bool TestSplineTessellation();
|
||||
bool TestGEMath();
|
||||
bool TestDemangle();
|
||||
|
||||
// The 8.3 short names games read out of d_private. These aren't verified against hardware yet (no
|
||||
@@ -3165,6 +3166,7 @@ TestItem availableTests[] = {
|
||||
TEST_ITEM(Lzrc),
|
||||
TEST_ITEM(MpegCsc),
|
||||
TEST_ITEM(SplineTessellation),
|
||||
TEST_ITEM(GEMath),
|
||||
TEST_ITEM(Demangle),
|
||||
TEST_ITEM(TextureReplacer),
|
||||
TEST_ITEM(UITabOrder),
|
||||
|
||||
@@ -297,6 +297,7 @@
|
||||
<ClCompile Include="TestLzrc.cpp" />
|
||||
<ClCompile Include="TestMpegCsc.cpp" />
|
||||
<ClCompile Include="TestSplineTessellation.cpp" />
|
||||
<ClCompile Include="TestGEMath.cpp" />
|
||||
<ClCompile Include="TestThreadManager.cpp" />
|
||||
<ClCompile Include="TestTextureReplacer.cpp" />
|
||||
<ClCompile Include="TestVertexJit.cpp" />
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<ClCompile Include="TestLzrc.cpp" />
|
||||
<ClCompile Include="TestMpegCsc.cpp" />
|
||||
<ClCompile Include="TestSplineTessellation.cpp" />
|
||||
<ClCompile Include="TestGEMath.cpp" />
|
||||
<ClCompile Include="TestTextureReplacer.cpp" />
|
||||
<ClCompile Include="TestRiscVEmitter.cpp" />
|
||||
<ClCompile Include="TestVFS.cpp" />
|
||||
|
||||
Reference in new issue
Block a user