Merge pull request #22414 from hrydgard/ge-bit-exact

SoftGPU: Make it mostly bit exact vs PSP hardware
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-05 09:15:24 -06:00
commit 9a2f5eb191
53 files changed
+3086 -640

No files matched your search

+1 -1
View File
@@ -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 |
+1
View File
@@ -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
+3
View File
@@ -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},
+3
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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,
+4
View File
@@ -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();
+16
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -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
View File
@@ -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 });
+2
View File
@@ -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" />
+6
View File
@@ -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
View File
@@ -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
+88 -7
View File
@@ -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 {
+20
View File
@@ -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
View File
@@ -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);
}
}
+4 -1
View File
@@ -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
+39 -5
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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;
+265
View File
@@ -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);
}
+189
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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);
}
File diff suppressed because it is too large. Load diff
+4
View File
@@ -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)
+34 -12
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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);
}
+12 -5
View File
@@ -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) {
+1
View File
@@ -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
View File
@@ -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);
}
+11 -5
View File
@@ -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;
+2
View File
@@ -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>
+2
View File
@@ -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" />
+2
View File
@@ -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
+17
View File
@@ -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:
+5
View File
@@ -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.
+54
View File
@@ -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`.
+16
View File
@@ -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.
+4
View File
@@ -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
+26 -1
View File
@@ -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()));
}
+1
View File
@@ -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 \
+35 -24
View File
@@ -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",
+220
View 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;
}
+2
View File
@@ -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),
+1
View File
@@ -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" />
+1
View File
@@ -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" />