#include "psx.h" #include "../../rsx/rsx_intf.h" #include extern "C" uint8_t psx_gpu_upscale_shift_hw; // Determine whether to offset UVs to account for difference in interpolation between PS1 and modern GPUs void Calc_UVOffsets_Adjust_Verts(PS_GPU *gpu, tri_vertex *vertices, unsigned count) { // iCB: Just borrowing this from \parallel-psx\renderer\renderer.cpp uint16 off_u = 0; uint16 off_v = 0; bool may_be_2d = false; if (gpu->InCmd == INCMD_QUAD) { off_u = gpu->off_u; off_v = gpu->off_v; may_be_2d = gpu->may_be_2d; } // For X/Y flipped 2D sprites, PSX games rely on a very specific rasterization behavior. // If U or V is decreasing in X or Y, and we use the provided U/V as is, we will sample the wrong texel as interpolation // covers an entire pixel, while PSX samples its interpolation essentially in the top-left corner and splats that interpolant across the entire pixel. // While we could emulate this reasonably well in native resolution by shifting our vertex coords by 0.5, // this breaks in upscaling scenarios, because we have several samples per native sample and we need NN rules to hit the same UV every time. // One approach here is to use interpolate at offset or similar tricks to generalize the PSX interpolation patterns, // but the problem is that vertices sharing an edge will no longer see the same UV (due to different plane derivatives), // we end up sampling outside the intended boundary and artifacts are inevitable, so the only case where we can apply this fixup is for "sprites" // or similar which should not share edges, which leads to this unfortunate code below. // #if 0 // Only apply this workaround for quads. if (count == 4) #endif { // It might be faster to do more direct checking here, but the code below handles primitives in any order // and orientation, and is far more SIMD-friendly if needed. float abx = vertices[1].x - vertices[0].x; float aby = vertices[1].y - vertices[0].y; float bcx = vertices[2].x - vertices[1].x; float bcy = vertices[2].y - vertices[1].y; float cax = vertices[0].x - vertices[2].x; float cay = vertices[0].y - vertices[2].y; // Compute static derivatives, just assume W is uniform across the primitive // and that the plane equation remains the same across the quad. float dudx = -aby * float(vertices[2].u) - bcy * float(vertices[0].u) - cay * float(vertices[1].u); float dvdx = -aby * float(vertices[2].v) - bcy * float(vertices[0].v) - cay * float(vertices[1].v); float dudy = +abx * float(vertices[2].u) + bcx * float(vertices[0].u) + cax * float(vertices[1].u); float dvdy = +abx * float(vertices[2].v) + bcx * float(vertices[0].v) + cax * float(vertices[1].v); float area = bcx * cay - bcy * cax; // iCB: Detect and reject any triangles with 0 size texture area float texArea = (vertices[1].u - vertices[0].u) * (vertices[2].v - vertices[0].v) - (vertices[2].u - vertices[0].u) * (vertices[1].v - vertices[0].v); // Leverage PGXP to further avoid 3D polygons that just happen to align this way after projection bool is3D = ((vertices[0].precise[2] != vertices[1].precise[2]) || (vertices[1].precise[2] != vertices[2].precise[2])); // Shouldn't matter as degenerate primitives will be culled anyways. if ((area != 0.0f) && (texArea != 0.0f) && !is3D) { float inv_area = 1.0f / area; dudx *= inv_area; dudy *= inv_area; dvdx *= inv_area; dvdy *= inv_area; bool neg_dudx = dudx < 0.0f; bool neg_dudy = dudy < 0.0f; bool neg_dvdx = dvdx < 0.0f; bool neg_dvdy = dvdy < 0.0f; bool zero_dudx = dudx == 0.0f; bool zero_dudy = dudy == 0.0f; bool zero_dvdx = dvdx == 0.0f; bool zero_dvdy = dvdy == 0.0f; // Dumb heuristic to check if a polygon may be 2D may_be_2d = may_be_2d || zero_dudy || zero_dudx || zero_dvdy || zero_dvdx; // If we have negative dU or dV in any direction, increment the U or V to work properly with nearest-neighbor in this impl. // If we don't have 1:1 pixel correspondence, this creates a slight "shift" in the sprite, but we guarantee that we don't sample garbage at least. // Overall, this is kinda hacky because there can be legitimate, rare cases where 3D meshes hit this scenario, and a single texel offset can pop in, but // this is way better than having borked 2D overall. // TODO: Try to figure out if this can be generalized. // // TODO: If perf becomes an issue, we can probably SIMD the 8 comparisons above, // create an 8-bit code, and use a LUT to get the offsets. // Case 1: U is decreasing in X, but no change in Y. // Case 2: U is decreasing in Y, but no change in X. // Case 3: V is decreasing in X, but no change in Y. // Case 4: V is decreasing in Y, but no change in X. if (rsx_intf_is_type() != RSX_VULKAN || psx_gpu_upscale_shift_hw) { if (neg_dudx && zero_dudy) off_u = 1; else if (neg_dudy && zero_dudx) off_u = 1; if (neg_dvdx && zero_dvdy) off_v = 1; else if (neg_dvdy && zero_dvdx) off_v = 1; } // HACK fix Wild Arms 2 overworld forest sprite // TODO generalize this perhaps? const float one = float(1 << gpu->upscale_shift); if (zero_dvdx && (aby == one || bcy == one || cay == one) && (aby == 0.0 || bcy == 0.0 || cay == 0.0) && (aby == -one || bcy == -one || cay == -one) ) { if (neg_dvdy) { if (aby == -one) vertices[0].v = vertices[1].v - 1; else if (bcy == -one) vertices[1].v = vertices[2].v - 1; else if (cay == -one) vertices[2].v = vertices[0].v - 1; if (aby == one) vertices[1].v = vertices[0].v - 1; else if (bcy == one) vertices[2].v = vertices[1].v - 1; else if (cay == one) vertices[0].v = vertices[2].v - 1; } } } } gpu->off_u = off_u; gpu->off_v = off_v; gpu->may_be_2d = may_be_2d; } // Reset min/max UVs for primitive void Reset_UVLimits(PS_GPU *gpu) { gpu->min_u = UINT16_MAX; gpu->min_v = UINT16_MAX; gpu->max_u = 0; gpu->max_v = 0; } // Determine min and max UVs sampled for a given primitive void Extend_UVLimits(PS_GPU *gpu, tri_vertex *vertices, unsigned count) { uint8 twx = gpu->SUCV.TWX_AND; uint8 twy = gpu->SUCV.TWY_AND; uint16 min_u = gpu->min_u; uint16 min_v = gpu->min_v; uint16 max_u = gpu->max_u; uint16 max_v = gpu->max_v; if ((twx == (uint8)0xffu) && (twy == (uint8)0xffu)) { // If we're not using texture window, we're likely accessing a small subset of the texture. for (unsigned int i = 0; i < count; i++) { min_u = std::min(min_u, uint16_t(vertices[i].u)); min_v = std::min(min_v, uint16_t(vertices[i].v)); max_u = std::max(max_u, uint16_t(vertices[i].u)); max_v = std::max(max_v, uint16_t(vertices[i].v)); } } else { // texture window so don't clamp texture min_u = 0; min_v = 0; max_u = UINT16_MAX; max_v = UINT16_MAX; } gpu->min_u = min_u; gpu->min_v = min_v; gpu->max_u = max_u; gpu->max_v = max_v; } // Apply offsets to UV limits before returning void Finalise_UVLimits(PS_GPU *gpu) { uint8 twx = gpu->SUCV.TWX_AND; uint8 twy = gpu->SUCV.TWY_AND; uint16 min_u = gpu->min_u; uint16 min_v = gpu->min_v; uint16 max_u = gpu->max_u; uint16 max_v = gpu->max_v; uint16 off_u = gpu->off_u; uint16 off_v = gpu->off_v; if ((twx == (uint8)0xffu) && (twy == (uint8)0xffu)) { // offset output UV Limits min_u += off_u; min_v += off_v; max_u += off_u; max_v += off_v; // In nearest neighbor, we'll get *very* close to this UV, but not close enough to actually sample it. // If du/dx or dv/dx are negative, we probably need to invert this though ... if ((rsx_intf_is_type() != RSX_VULKAN || psx_gpu_upscale_shift_hw) && gpu->may_be_2d) { if (max_u > min_u) max_u--; if (max_v > min_v) max_v--; } // If there's no wrapping, we can prewrap and avoid fallback. if ((max_u & 0xff00) == (min_u & 0xff00)) max_u &= 0xff; if ((max_v & 0xff00) == (min_v & 0xff00)) max_v &= 0xff; } else { // texture window so don't clamp texture min_u = 0; min_v = 0; max_u = UINT16_MAX; max_v = UINT16_MAX; } gpu->min_u = min_u; gpu->min_v = min_v; gpu->max_u = max_u; gpu->max_v = max_v; } // 0 = disabled // 1 = enabled (default mode) // 2 = enabled (aggressive mode) // Hack to deal with PS1 games rendering axis aligned lines using 1 pixel wide triangles with UVs that describe a line // Suitable for games like Soul Blade, Doom and Hexen bool Hack_FindLine(PS_GPU *gpu, tri_vertex* vertices, tri_vertex* outVertices) { int32 pxWidth = 1 << gpu->upscale_shift; // width of a single pixel uint8 cornerIdx, shortIdx, longIdx; // reject 3D elements if ((vertices[0].precise[2] != vertices[1].precise[2]) || (vertices[1].precise[2] != vertices[2].precise[2])) return false; // find short side of triangle / end of line with 2 vertices (guess which vertex is the right angle) if ((vertices[0].u == vertices[1].u) && (vertices[0].v == vertices[1].v)) cornerIdx = 0; else if ((vertices[1].u == vertices[2].u) && (vertices[1].v == vertices[2].v)) cornerIdx = 1; else if ((vertices[2].u == vertices[0].u) && (vertices[2].v == vertices[0].v)) cornerIdx = 2; else return false; // assign other indices to remaining vertices shortIdx = (cornerIdx + 1) % 3; longIdx = (shortIdx + 1) % 3; // determine line orientation and check width if ((vertices[cornerIdx].x == vertices[shortIdx].x) && (abs(vertices[cornerIdx].y - vertices[shortIdx].y) == pxWidth)) { // line is horizontal // determine which is truly the corner by checking against the long side, while making sure it is axis aligned if (vertices[shortIdx].y == vertices[longIdx].y) { uint8 tempIdx = shortIdx; shortIdx = cornerIdx; cornerIdx = tempIdx; } else if (vertices[cornerIdx].y != vertices[longIdx].y) return false; // flip corner index to other side of quad outVertices[cornerIdx] = vertices[longIdx]; outVertices[cornerIdx].y = vertices[shortIdx].y; outVertices[cornerIdx].precise[1] = vertices[shortIdx].precise[1]; } else if ((vertices[cornerIdx].y == vertices[shortIdx].y) && (abs(vertices[cornerIdx].x - vertices[shortIdx].x) == pxWidth)) { // line is vertical // determine which is truly the corner by checking against the long side, while making sure it is axis aligned if (vertices[shortIdx].x == vertices[longIdx].x) { uint8 tempIdx = shortIdx; shortIdx = cornerIdx; cornerIdx = tempIdx; } else if (vertices[cornerIdx].x != vertices[longIdx].x) return false; // flip corner index to other side of quad outVertices[cornerIdx] = vertices[longIdx]; outVertices[cornerIdx].x = vertices[shortIdx].x; outVertices[cornerIdx].precise[0] = vertices[shortIdx].precise[0]; } else return false; outVertices[shortIdx] = vertices[shortIdx]; outVertices[longIdx] = vertices[longIdx]; return true; } // Hack to deal with PS1 games rendering axis aligned lines using 1 pixel wide triangles and force UVs to describe a line // Required for games like Dark Forces and Duke Nukem bool Hack_ForceLine(PS_GPU *gpu, tri_vertex* vertices, tri_vertex* outVertices) { int32 pxWidth = 1 << gpu->upscale_shift; // width of a single pixel uint8 cornerIdx, shortIdx, longIdx; // reject 3D elements if ((vertices[0].precise[2] != vertices[1].precise[2]) || (vertices[1].precise[2] != vertices[2].precise[2])) return false; // find vertical AB uint8 A, B, C; if (vertices[0].x == vertices[1].x) A = 0; else if (vertices[1].x == vertices[2].x) A = 1; else if (vertices[2].x == vertices[0].x) A = 2; else return false; // assign other indices to remaining vertices B = (A + 1) % 3; C = (B + 1) % 3; // find horizontal AC or BC if (vertices[A].y == vertices[C].y) cornerIdx = A; else if (vertices[B].y == vertices[C].y) cornerIdx = B; else return false; // determine lengths of sides if (abs(vertices[A].y - vertices[B].y) == pxWidth) { // is Horizontal shortIdx = (cornerIdx == A) ? B : A; longIdx = C; // flip corner index to other side of quad outVertices[cornerIdx] = vertices[longIdx]; outVertices[cornerIdx].y = vertices[shortIdx].y; outVertices[cornerIdx].precise[1] = vertices[shortIdx].precise[1]; } else if (abs(vertices[A].x - vertices[C].x) == pxWidth) { // is Vertical shortIdx = C; longIdx = (cornerIdx == A) ? B : A; // flip corner index to other side of quad outVertices[cornerIdx] = vertices[longIdx]; outVertices[cornerIdx].x = vertices[shortIdx].x; outVertices[cornerIdx].precise[0] = vertices[shortIdx].precise[0]; } else return false; // force UVs into a line along the upper or left most edge of the triangle // Otherwise the wrong UVs will be sampled on second triangle and by hardware renderers vertices[shortIdx].u = vertices[cornerIdx].u; vertices[shortIdx].v = vertices[cornerIdx].v; // copy other two vertices outVertices[shortIdx] = vertices[shortIdx]; outVertices[longIdx] = vertices[longIdx]; return true; }