Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 43 additions & 42 deletions ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1166,26 +1166,31 @@ void GSCpuBackend::DrawSprite(const GSPrimitiveBatch &batch)
const GSVertex &v1 = batch.vertices[1];
const auto &ctx = state.context;

int ofx = ctx.xyoffset.ofx >> 4;
int ofy = ctx.xyoffset.ofy >> 4;
const float ofx = static_cast<float>(ctx.xyoffset.ofx) / 16.0f;
const float ofy = static_cast<float>(ctx.xyoffset.ofy) / 16.0f;

int x0 = static_cast<int>(v0.x) - ofx;
int y0 = static_cast<int>(v0.y) - ofy;
int x1 = static_cast<int>(v1.x) - ofx;
int y1 = static_cast<int>(v1.y) - ofy;
float x0 = v0.x - ofx;
float y0 = v0.y - ofy;
float x1 = v1.x - ofx;
float y1 = v1.y - ofy;
u32 z1 = static_cast<u32>(v1.z);

if (x0 > x1)
const bool reverseX = x0 > x1;
const bool reverseY = y0 > y1;
if (reverseX)
std::swap(x0, x1);
if (y0 > y1)
if (reverseY)
std::swap(y0, y1);

const int unclippedX0 = x0;
const int unclippedY0 = y0;
const int spanX = std::max(1, x1 - x0);
const int spanY = std::max(1, y1 - y0);
const int unclippedX1 = unclippedX0 + spanX - 1;
const int unclippedY1 = unclippedY0 + spanY - 1;
// GS samples sprites at integer pixel coordinates; the lower bounds are
// inclusive and the upper bounds exclusive, after rounding up. Keep the
// fractional endpoints for texture interpolation and sort UV per axis too.
const int unclippedX0 = static_cast<int>(std::ceil(x0));
const int unclippedY0 = static_cast<int>(std::ceil(y0));
const int unclippedX1 = static_cast<int>(std::ceil(x1)) - 1;
const int unclippedY1 = static_cast<int>(std::ceil(y1)) - 1;
if (unclippedX0 > unclippedX1 || unclippedY0 > unclippedY1)
return;

// If the sprite rectangle is fully outside scissor, nothing should render.
if (unclippedX1 < ctx.scissor.x0 || unclippedX0 > ctx.scissor.x1 ||
Expand All @@ -1212,50 +1217,44 @@ void GSCpuBackend::DrawSprite(const GSPrimitiveBatch &batch)
float u0f, v0f, u1f, v1f;
if (state.prim.fst)
{
u0f = static_cast<float>(v0.u >> 4);
v0f = static_cast<float>(v0.v >> 4);
u1f = static_cast<float>(v1.u >> 4);
v1f = static_cast<float>(v1.v >> 4);
u0f = static_cast<float>(v0.u) / 16.0f;
v0f = static_cast<float>(v0.v) / 16.0f;
u1f = static_cast<float>(v1.u) / 16.0f;
v1f = static_cast<float>(v1.v) / 16.0f;
}
else
{
const float q0 = fabsQ(v0.q);
// Sprite Q is flat, taken from the second vertex.
const float q1 = fabsQ(v1.q);
u0f = (v0.s / q0) * static_cast<float>(texW);
v0f = (v0.t / q0) * static_cast<float>(texH);
u0f = (v0.s / q1) * static_cast<float>(texW);
v0f = (v0.t / q1) * static_cast<float>(texH);
u1f = (v1.s / q1) * static_cast<float>(texW);
v1f = (v1.t / q1) * static_cast<float>(texH);
}

float spriteW = static_cast<float>(spanX);
float spriteH = static_cast<float>(spanY);
if (spriteW < 1.0f)
spriteW = 1.0f;
if (spriteH < 1.0f)
spriteH = 1.0f;
if (reverseX)
std::swap(u0f, u1f);
if (reverseY)
std::swap(v0f, v1f);

const float spriteW = x1 - x0;
const float spriteH = y1 - y0;
// Use the floating-coordinate sampler for interpolated UV as well as
// STQ, avoiding a second quantization to the vertex UV's four bits.
GSDrawState textureState = state;
textureState.prim.fst = false;

for (int y = drawY0; y <= drawY1; ++y)
{
float ty = (static_cast<float>(y - unclippedY0) + 0.5f) / spriteH;
float ty = (static_cast<float>(y) - y0) / spriteH;
float texVf = v0f + (v1f - v0f) * ty;

for (int x = drawX0; x <= drawX1; ++x)
{
float tx = (static_cast<float>(x - unclippedX0) + 0.5f) / spriteW;
float tx = (static_cast<float>(x) - x0) / spriteW;
float texUf = u0f + (u1f - u0f) * tx;
uint32_t texel = 0xFFFF00FFu;
if (state.prim.fst)
{
const int fixedU = static_cast<int>((texUf * 16.0f) + 0.5f);
const int fixedV = static_cast<int>((texVf * 16.0f) + 0.5f);
const uint16_t sampleU = static_cast<uint16_t>(clampInt(fixedU, 0, 0xFFFF));
const uint16_t sampleV = static_cast<uint16_t>(clampInt(fixedV, 0, 0xFFFF));
texel = SampleTexture(state, 0.0f, 0.0f, 1.0f, sampleU, sampleV);
}
else
{
texel = SampleTexture(state, texUf / static_cast<float>(texW), texVf / static_cast<float>(texH), 1.0f, 0u, 0u);
}
const uint32_t texel = SampleTexture(textureState,
texUf / static_cast<float>(texW), texVf / static_cast<float>(texH), 1.0f, 0u, 0u);

uint8_t tr = static_cast<uint8_t>(texel & 0xFF);
uint8_t tg = static_cast<uint8_t>((texel >> 8) & 0xFF);
Expand All @@ -1270,8 +1269,10 @@ void GSCpuBackend::DrawSprite(const GSPrimitiveBatch &batch)
else
{
for (int y = drawY0; y <= drawY1; ++y)
{
for (int x = drawX0; x <= drawX1; ++x)
WritePixel(state, x, y, z1, r, g, b, a, v1.fog);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions ps2xTest/gs_cache/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ add_gs_cache_suite(ps2_gs_clut_cache_tests gs_clut_cache_tests.cpp clut
reserved_cld nonindexed_cld tex2_reload shared_contexts
csa_ct32 csa_ct16 csa_ct16s texa_without_reload palette_before_filtering high_planes)

add_gs_cache_suite(ps2_gs_sprite_alignment_tests gs_sprite_alignment_tests.cpp sprite
coordinates feedback)

if(PS2_GS_CACHE_BUILD_MEMORY_TESTS)
add_gs_cache_suite(ps2_gs_memory_cache_tests gs_memory_cache_tests.cpp memory
ct32 ct24 ct16 ct16s t8 t4 t8h t4hl t4hh z32 z24 z16 z16s
Expand Down
59 changes: 59 additions & 0 deletions ps2xTest/gs_cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# GS backend tests

This standalone package compiles the actual CPU backend, frontend and local
memory implementation. It uses synthetic VRAM and public GS APIs, with no game
assets, generated guest functions or runtime substitutes.

```sh
cmake -S ps2xTest/gs_cache -B build/gs-cache
cmake --build build/gs-cache --config RelWithDebInfo
ctest --test-dir build/gs-cache -C RelWithDebInfo --output-on-failure
```

For native Windows MSVC, configure with `-G "Visual Studio 17 2022" -A x64` and
`-DCMAKE_CXX_FLAGS_RELWITHDEBINFO="/O2 /Ob1 /DNDEBUG /Zi /fp:strict"`.
`PS2_GS_RUNTIME_DIR` selects another runtime source checkout; the default is
this repository's `ps2xRuntime`. Optional GCC/Clang sanitizers use
`PS2_GS_CACHE_SANITIZERS=ON`.

The two `gs_cache.sprite.*` cases add sprite coverage and texture alignment to
the 45 existing texture, CLUT and memory-cache cases:

- `coordinates` uses an independent scalar interpolation oracle across all 16
UV fractional nibbles, nearest and bilinear filtering, reversed X/Y axes,
fractional XYOFFSET, clipping, integer pixel sample locations, ceil-exclusive
bounds, second-vertex flat Q/color, degenerate sprites and untextured bounds.
- `feedback` draws four reversed 64-pixel CT32 identity strips through the real
shared single-page texture cache without intervening TEXFLUSH. Every pixel
stays unchanged and every tested boundary retains the original gradient.

Bilinear color checks permit one unit for floating-point operation-order
rounding. Coverage, untouched pixels and feedback results are exact. Identity
feedback has the same expected pixels for any valid cache-invalidation policy;
the test does not impose a full-frame snapshot rule on arbitrary feedback.

The fixture sets TEST.ZTE=1/ZTST=ALWAYS and ZMSK=1 to isolate sprite alignment
from the separate disabled-depth-test issue. This change does not include a
depth-pipeline fix. This upstream backend is synchronous: the package does not
claim to test the private runtime's asynchronous raster workers.

Coordinate rules are supported by pinned primary PCSX2 implementations:
[vertex conversion](https://github.com/PCSX2/pcsx2/blob/355608952714678b3c57832fb82dc6a42956ed25/pcsx2/GS/Renderers/SW/GSRendererSW.cpp#L219)
preserves fractional XYOFFSET/UV and supplies the second vertex's Q for sprites;
[sprite rasterization](https://github.com/PCSX2/pcsx2/blob/355608952714678b3c57832fb82dc6a42956ed25/pcsx2/GS/Renderers/SW/GSRasterizer.cpp#L1088)
sorts the matching texture component per axis, uses ceil-exclusive bounds and
integer pixel sample locations. Linear sampling applies its separate half-texel
offset.

Native Windows MSVC 19.44 validation passed all 47 CTest entries with
RelWithDebInfo `/O2 /fp:strict`. The two alignment cases passed 853,552 checks
with zero failures, zero changed feedback pixels and zero artificial boundary
steps. Compiling the same fixture against the unchanged main-branch backend
failed 38,810 coordinate checks and 8,029 feedback checks, including 7,812 altered
pixels and 217 extra boundary steps. This control changed only the backend
source under test, keeping the fixture and compiler configuration identical.

Filter new cases with `ctest --test-dir build/gs-cache -C RelWithDebInfo
-R gs_cache.sprite --output-on-failure`. Full runtime, retail gameplay and Linux
validation are outside this package. The fixture is GPL-3.0-only, like the
linked runtime; see the repository license.
Loading