Files
pcsx2/pcsx2/GS/Renderers/Metal/tfx.metal
T
2026-07-08 14:08:29 +02:00

1861 lines
58 KiB
Metal

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "GSMTLShaderCommon.h"
constant uint FMT_32 = 0;
constant uint FMT_24 = 1;
constant uint FMT_16 = 2;
constant uint SHUFFLE_READ = 1;
[[maybe_unused]] constant uint SHUFFLE_WRITE = 2;
constant uint SHUFFLE_READWRITE = 3;
constant bool HAS_FBFETCH [[function_constant(GSMTLConstantIndex_FRAMEBUFFER_FETCH)]];
constant bool DEPTH_FEEDBACK [[function_constant(GSMTLConstantIndex_DEPTH_FEEDBACK)]];
constant bool ROV_NEEDS_R32 [[function_constant(GSMTLConstantIndex_ROV_NEEDS_R32)]];
constant bool FST [[function_constant(GSMTLConstantIndex_FST)]];
constant bool IIP [[function_constant(GSMTLConstantIndex_IIP)]];
constant bool VS_POINT_SIZE [[function_constant(GSMTLConstantIndex_VS_POINT_SIZE)]];
constant uint VS_EXPAND_TYPE_RAW [[function_constant(GSMTLConstantIndex_VS_EXPAND_TYPE)]];
constant uint PS_AEM_FMT [[function_constant(GSMTLConstantIndex_PS_AEM_FMT)]];
constant uint PS_PAL_FMT [[function_constant(GSMTLConstantIndex_PS_PAL_FMT)]];
constant uint PS_DST_FMT [[function_constant(GSMTLConstantIndex_PS_DST_FMT)]];
constant uint PS_DEPTH_FMT [[function_constant(GSMTLConstantIndex_PS_DEPTH_FMT)]];
constant bool PS_AEM [[function_constant(GSMTLConstantIndex_PS_AEM)]];
constant bool PS_FBA [[function_constant(GSMTLConstantIndex_PS_FBA)]];
constant bool PS_FOG [[function_constant(GSMTLConstantIndex_PS_FOG)]];
constant uint PS_DATE [[function_constant(GSMTLConstantIndex_PS_DATE)]];
constant uint PS_ATST_RAW [[function_constant(GSMTLConstantIndex_PS_ATST)]];
constant uint PS_AFAIL_RAW [[function_constant(GSMTLConstantIndex_PS_AFAIL)]];
constant uint PS_ZTST_RAW [[function_constant(GSMTLConstantIndex_PS_ZTST)]];
constant uint PS_TFX [[function_constant(GSMTLConstantIndex_PS_TFX)]];
constant bool PS_TCC [[function_constant(GSMTLConstantIndex_PS_TCC)]];
constant uint PS_WMS [[function_constant(GSMTLConstantIndex_PS_WMS)]];
constant uint PS_WMT [[function_constant(GSMTLConstantIndex_PS_WMT)]];
constant bool PS_ADJS [[function_constant(GSMTLConstantIndex_PS_ADJS)]];
constant bool PS_ADJT [[function_constant(GSMTLConstantIndex_PS_ADJT)]];
constant bool PS_LTF [[function_constant(GSMTLConstantIndex_PS_LTF)]];
constant bool PS_SHUFFLE [[function_constant(GSMTLConstantIndex_PS_SHUFFLE)]];
constant bool PS_SHUFFLE_SAME [[function_constant(GSMTLConstantIndex_PS_SHUFFLE_SAME)]];
constant uint PS_PROCESS_BA [[function_constant(GSMTLConstantIndex_PS_PROCESS_BA)]];
constant uint PS_PROCESS_RG [[function_constant(GSMTLConstantIndex_PS_PROCESS_RG)]];
constant bool PS_SHUFFLE_ACROSS [[function_constant(GSMTLConstantIndex_PS_SHUFFLE_ACROSS)]];
constant bool PS_READ16_SRC [[function_constant(GSMTLConstantIndex_PS_READ16_SRC)]];
constant bool PS_WRITE_RG [[function_constant(GSMTLConstantIndex_PS_WRITE_RG)]];
constant bool PS_FBMASK [[function_constant(GSMTLConstantIndex_PS_FBMASK)]];
constant uint PS_BLEND_A [[function_constant(GSMTLConstantIndex_PS_BLEND_A)]];
constant uint PS_BLEND_B [[function_constant(GSMTLConstantIndex_PS_BLEND_B)]];
constant uint PS_BLEND_C [[function_constant(GSMTLConstantIndex_PS_BLEND_C)]];
constant uint PS_BLEND_D [[function_constant(GSMTLConstantIndex_PS_BLEND_D)]];
constant uint PS_BLEND_HW [[function_constant(GSMTLConstantIndex_PS_BLEND_HW)]];
constant bool PS_A_MASKED [[function_constant(GSMTLConstantIndex_PS_A_MASKED)]];
constant bool PS_COLCLIP_HW [[function_constant(GSMTLConstantIndex_PS_COLCLIP_HW)]];
constant bool PS_RTA_CORRECTION [[function_constant(GSMTLConstantIndex_PS_RTA_CORRECTION)]];
constant bool PS_RTA_SRC_CORRECTION [[function_constant(GSMTLConstantIndex_PS_RTA_SRC_CORRECTION)]];
constant bool PS_COLCLIP [[function_constant(GSMTLConstantIndex_PS_COLCLIP)]];
constant uint PS_BLEND_MIX [[function_constant(GSMTLConstantIndex_PS_BLEND_MIX)]];
constant bool PS_ROUND_INV [[function_constant(GSMTLConstantIndex_PS_ROUND_INV)]];
constant bool PS_FIXED_ONE_A [[function_constant(GSMTLConstantIndex_PS_FIXED_ONE_A)]];
constant bool PS_PABE [[function_constant(GSMTLConstantIndex_PS_PABE)]];
constant bool PS_NO_COLOR [[function_constant(GSMTLConstantIndex_PS_NO_COLOR)]];
constant bool PS_NO_COLOR1 [[function_constant(GSMTLConstantIndex_PS_NO_COLOR1)]];
constant uint PS_CHANNEL [[function_constant(GSMTLConstantIndex_PS_CHANNEL)]];
constant uint PS_DITHER [[function_constant(GSMTLConstantIndex_PS_DITHER)]];
constant uint PS_DITHER_ADJUST [[function_constant(GSMTLConstantIndex_PS_DITHER_ADJUST)]];
constant bool PS_ZCLAMP [[function_constant(GSMTLConstantIndex_PS_ZCLAMP)]];
constant bool PS_ZFLOOR [[function_constant(GSMTLConstantIndex_PS_ZFLOOR)]];
constant bool PS_TCOFFSETHACK [[function_constant(GSMTLConstantIndex_PS_TCOFFSETHACK)]];
constant bool PS_URBAN_CHAOS_HLE [[function_constant(GSMTLConstantIndex_PS_URBAN_CHAOS_HLE)]];
constant bool PS_TALES_OF_ABYSS_HLE [[function_constant(GSMTLConstantIndex_PS_TALES_OF_ABYSS_HLE)]];
constant bool PS_TEX_IS_FB [[function_constant(GSMTLConstantIndex_PS_TEX_IS_FB)]];
constant bool PS_AUTOMATIC_LOD [[function_constant(GSMTLConstantIndex_PS_AUTOMATIC_LOD)]];
constant bool PS_MANUAL_LOD [[function_constant(GSMTLConstantIndex_PS_MANUAL_LOD)]];
constant bool PS_REGION_RECT [[function_constant(GSMTLConstantIndex_PS_REGION_RECT)]];
constant uint PS_SCANMSK [[function_constant(GSMTLConstantIndex_PS_SCANMSK)]];
constant uint PS_AA1_RAW [[function_constant(GSMTLConstantIndex_PS_AA1)]];
constant bool PS_ABE [[function_constant(GSMTLConstantIndex_PS_ABE)]];
constant uint PS_SW_ANISO [[function_constant(GSMTLConstantIndex_PS_SW_ANISO)]];
constant bool PS_ROV_COLOR [[function_constant(GSMTLConstantIndex_PS_ROV_COLOR)]];
constant uint PS_ROV_DEPTH_RAW [[function_constant(GSMTLConstantIndex_PS_ROV_DEPTH)]];
using GSShader::VSExpand;
using AFAIL = GSShader::PS_AFAIL;
using ATST = GSShader::PS_ATST;
using GSShader::ZTST;
using AA1 = GSShader::PS_AA1;
using ROV_DEPTH = GSShader::PS_ROV_DEPTH;
constant VSExpand VS_EXPAND_TYPE = static_cast<VSExpand>(VS_EXPAND_TYPE_RAW);
constant AFAIL PS_AFAIL = static_cast<AFAIL>(PS_AFAIL_RAW);
constant ATST PS_ATST = static_cast<ATST>(PS_ATST_RAW);
constant ZTST PS_ZTST = static_cast<ZTST>(PS_ZTST_RAW);
constant AA1 PS_AA1 = static_cast<AA1>(PS_AA1_RAW);
constant ROV_DEPTH PS_ROV_DEPTH = static_cast<ROV_DEPTH>(PS_ROV_DEPTH_RAW);
#if defined(__METAL_MACOS__) && __METAL_VERSION__ >= 220
#define PRIMID_SUPPORT 1
#else
#define PRIMID_SUPPORT 0
#endif
#if defined(__METAL_IOS__) || __METAL_VERSION__ >= 230
#define FBFETCH_SUPPORT 1
#else
#define FBFETCH_SUPPORT 0
#endif
constant bool PS_PRIM_CHECKING_INIT = PS_DATE == 1 || PS_DATE == 2;
constant bool PS_PRIM_CHECKING_READ = PS_DATE == 3;
#if PRIMID_SUPPORT
constant bool NEEDS_PRIMID = PS_PRIM_CHECKING_INIT || PS_PRIM_CHECKING_READ;
#endif
constant bool PS_TEX_IS_DEPTH = PS_URBAN_CHAOS_HLE || PS_TALES_OF_ABYSS_HLE || PS_DEPTH_FMT == 1 || PS_DEPTH_FMT == 2;
constant bool PS_TEX_IS_COLOR = !PS_TEX_IS_DEPTH;
constant bool PS_HAS_PALETTE = PS_PAL_FMT != 0 || (PS_CHANNEL >= 1 && PS_CHANNEL <= 5);
constant bool NOT_IIP = !IIP;
constant bool SW_BLEND = (PS_BLEND_A != PS_BLEND_B) || PS_BLEND_D;
constant bool SW_AD_TO_HW = (PS_BLEND_C == 1 && PS_A_MASKED);
constant bool NEEDS_RT_FOR_BLEND = (((PS_BLEND_A != PS_BLEND_B) && (PS_BLEND_A == 1 || PS_BLEND_B == 1 || PS_BLEND_C == 1)) || PS_BLEND_D == 1 || SW_AD_TO_HW);
constant bool NEEDS_RT_EARLY = PS_TEX_IS_FB || PS_DATE >= 5;
constant bool NEEDS_RT_FOR_AFAIL = PS_AFAIL == AFAIL::ZB_ONLY || PS_AFAIL == AFAIL::RGB_ONLY || PS_AFAIL == AFAIL::RGB_ONLY_SW_Z;
constant bool NEEDS_RT = NEEDS_RT_FOR_AFAIL || NEEDS_RT_EARLY || (!PS_PRIM_CHECKING_INIT && (PS_FBMASK || NEEDS_RT_FOR_BLEND));
constant bool NEEDS_DEPTH_FOR_AFAIL = PS_AFAIL == AFAIL::FB_ONLY || PS_AFAIL == AFAIL::RGB_ONLY_SW_Z;
constant bool NEEDS_DEPTH_FOR_ZTST = PS_ZTST == ZTST::GEQUAL || PS_ZTST == ZTST::GREATER;
constant bool NEEDS_DEPTH_FOR_AA1 = PS_AA1 == AA1::TRIANGLE_SW_Z;
constant bool SW_DEPTH = NEEDS_DEPTH_FOR_AFAIL || NEEDS_DEPTH_FOR_ZTST || NEEDS_DEPTH_FOR_AA1;
constant bool PS_OUTPUT_COLOR0 = !PS_NO_COLOR && !PS_ROV_COLOR;
constant bool PS_OUTPUT_COLOR1 = !PS_NO_COLOR1 && !PS_ROV_COLOR;
constant bool PS_ZOUTPUT = (PS_ZCLAMP || PS_ZFLOOR || SW_DEPTH) && PS_ROV_DEPTH == ROV_DEPTH::NONE;
constant bool PS_ZOUTPUT_LESS = PS_ZOUTPUT && !SW_DEPTH;
constant bool PS_ZOUTPUT_ANY = PS_ZOUTPUT && SW_DEPTH;
constant bool PS_ZOUTPUT_COLOR = PS_ZOUTPUT_ANY && !DEPTH_FEEDBACK;
constant bool VS_NEEDS_INDEX_BUFFER = VS_EXPAND_TYPE == VSExpand::TriangleAA1;
constant bool VS_COVERAGE = VS_EXPAND_TYPE == VSExpand::LineAA1 || VS_EXPAND_TYPE == VSExpand::TriangleAA1;
constant bool VS_INTERIOR = VS_EXPAND_TYPE == VSExpand::TriangleAA1;
constant bool PS_COVERAGE = PS_AA1 != AA1::NONE;
constant bool PS_INTERIOR = PS_AA1 == AA1::TRIANGLE_SW_Z;
struct MainVSIn
{
float2 st [[attribute(GSMTLAttributeIndexST)]];
float4 c [[attribute(GSMTLAttributeIndexC)]];
float q [[attribute(GSMTLAttributeIndexQ)]];
uint2 p [[attribute(GSMTLAttributeIndexXY)]];
uint z [[attribute(GSMTLAttributeIndexZ)]];
uint2 uv [[attribute(GSMTLAttributeIndexUV)]];
float4 f [[attribute(GSMTLAttributeIndexF)]];
};
struct MainVSOut
{
float4 p [[position]];
float4 t;
float4 ti;
float4 c [[function_constant(IIP)]];
float4 fc [[flat, function_constant(NOT_IIP)]];
float inv_cov [[function_constant(VS_COVERAGE)]];
uint interior [[function_constant(VS_INTERIOR)]];
float point_size [[point_size, function_constant(VS_POINT_SIZE)]];
};
struct MainPSIn
{
float4 p [[position]];
float4 t;
float4 ti;
float4 c [[function_constant(IIP)]];
float4 fc [[flat, function_constant(NOT_IIP)]];
float inv_cov [[function_constant(PS_COVERAGE)]];
uint interior [[function_constant(PS_INTERIOR)]];
};
struct MainResult
{
float4 c0;
float4 c1;
float depth;
};
struct MainPSOut
{
float4 c0 [[color(0), index(0), function_constant(PS_OUTPUT_COLOR0)]];
float4 c1 [[color(0), index(1), function_constant(PS_OUTPUT_COLOR1)]];
float depthColor [[color(1), function_constant(PS_ZOUTPUT_COLOR)]];
float depthLess [[depth(less), function_constant(PS_ZOUTPUT_LESS)]];
float depthAny [[depth(any), function_constant(PS_ZOUTPUT_ANY)]];
MainPSOut(MainResult res)
{
if (PS_OUTPUT_COLOR0)
c0 = res.c0;
if (PS_OUTPUT_COLOR1)
c1 = res.c1;
if (PS_ZOUTPUT_LESS)
depthLess = res.depth;
if (PS_ZOUTPUT_ANY)
depthAny = res.depth;
if (PS_ZOUTPUT_COLOR)
depthColor = res.depth;
}
};
// MARK: - Vertex functions
static void texture_coord(thread const MainVSIn& v, thread MainVSOut& out, constant GSMTLMainVSUniform& cb)
{
float2 uv = float2(v.uv) - cb.texture_offset;
float2 st = v.st - cb.texture_offset;
// Float coordinate
out.t.xy = st;
out.t.w = v.q;
// Integer coordinate => normalized
out.ti.xy = uv * cb.texture_scale;
if (FST)
{
// Integer coordinate => integral
out.ti.zw = uv;
}
else
{
// Some games uses float coordinate for post-processing effects
out.ti.zw = st / cb.texture_scale;
}
}
static MainVSOut vs_main_run(thread const MainVSIn& v, constant GSMTLMainVSUniform& cb)
{
constexpr float exp_min32 = 0x1p-32;
MainVSOut out;
// Clamp to max depth, gs doesn't wrap
uint z = min(v.z, cb.max_depth);
out.p.xy = float2(v.p) - float2(0.05, 0.05);
out.p.xy = out.p.xy * float2(cb.vertex_scale.x, -cb.vertex_scale.y) - float2(cb.vertex_offset.x, -cb.vertex_offset.y);
out.p.w = 1;
out.p.z = float(z) * exp_min32;
texture_coord(v, out, cb);
if (IIP)
out.c = v.c;
else
out.fc = v.c;
out.t.z = v.f.x; // pack fog with texture
if (VS_POINT_SIZE)
out.point_size = cb.point_size.x;
return out;
}
vertex MainVSOut vs_main(MainVSIn v [[stage_in]], constant GSMTLMainVSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]])
{
return vs_main_run(v, cb);
}
static MainVSIn load_vertex(GSMTLMainVertex base)
{
MainVSIn out;
out.st = base.st;
out.c = float4(base.rgba);
out.q = base.q;
out.p = uint2(base.xy);
out.z = base.z;
out.uv = uint2(base.uv);
out.f = float4(static_cast<float>(base.fog) / 255.f);
return out;
}
// Convert XY from NDC to GS pixel coordinates (i.e. 1.0 = 1 GS pixel).
static float2 get_xy_unscaled(float2 xy, constant GSMTLMainVSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]])
{
return round(xy / cb.vertex_scale) / 16.0f;
}
// Get the XY deltas in GS pixel coordinates, using first vertex as the origin.
static float2x2 get_xy_deltas_unscaled(thread const MainVSOut& v0, thread const MainVSOut& v1, thread const MainVSOut& v2,
constant GSMTLMainVSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]])
{
float2 xy0 = get_xy_unscaled(v0.p.xy, cb);
float2 xy1 = get_xy_unscaled(v1.p.xy, cb);
float2 xy2 = get_xy_unscaled(v2.p.xy, cb);
return float2x2(xy1 - xy0, xy2 - xy0);
}
// Get the AA1 outward expand direction to the edge formed by the first two vertices.
// This is up or down for shallow (X dominant) edges, and right or left for steep (Y dominant) edges.
// Similar expansion to line AA1 except instead of expanding on both sides of the line,
// expand on on the side towards the outside of the triangle.
float2 get_aa1_triangle_expand_dir(thread const MainVSOut& v0, thread const MainVSOut& v1, thread const MainVSOut& v2,
constant GSMTLMainVSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]])
{
float2x2 xy_deltas = get_xy_deltas_unscaled(v0, v1, v2, cb);
float2 line_delta = xy_deltas[0];
float2 line_opposite = xy_deltas[1];
float2 line_normal = float2(line_delta.y, -line_delta.x);
float2 line_expand = abs(line_delta.x) >= abs(line_delta.y) ? float2(0.0f, 1.0f) : float2(1.0f, 0.0f);
if ((dot(line_expand, line_normal) >= 0.0f) == (dot(line_opposite, line_normal) >= 0.0f))
{
// Expand direction point towards the interior so flip it.
line_expand = -line_expand;
}
return line_expand;
}
float2x2 get_inverse(const thread float2x2& mat, float det)
{
return float2x2(mat[1][1], -mat[0][1], -mat[1][0], mat[0][0]) * (1 / det);
}
// Extrapolate triangle attributes from the first vertex along the given direction.
// dp_mat is derived from the input vertices, it is passed in to avoid recomputing.
void extrapolate_aa1_triangle_edge(thread MainVSOut& v0, thread const MainVSOut& v1, thread const MainVSOut& v2,
thread const float2x2& dp_mat, float2 dp, constant GSMTLMainVSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]])
{
// Get texture deltas
float2x2 dt;
if (FST)
{
dt = float2x2(v1.ti.zw - v0.ti.zw, v2.ti.zw - v0.ti.zw);
}
else
{
dt = float2x2(v1.t.xy - v0.t.xy, v2.t.xy - v0.t.xy);
}
// Get color delta if interpolating
float2x4 dc;
if (IIP)
{
dc = float2x4(v1.c - v0.c, v2.c - v0.c);
}
float2 dz = float2(v1.p.z - v0.p.z, v2.p.z - v0.p.z); // Z deltas
float2 df = float2(v1.t.z - v0.t.z, v2.t.z - v0.t.z); // Fog deltas
float2 dq = float2(v1.t.w - v0.t.w, v2.t.w - v0.t.w); // Q deltas
// To prevent unstable extrapolation, do not extrapolate if the
// minimum perpendicular length of the triangle is < 2 pixels.
float dp_det = determinant(dp_mat); // Twice signed triangle area.
float len0 = length(dp_mat[0]);
float len1 = length(dp_mat[1]);
float len2 = length(dp_mat[1] - dp_mat[0]);
float min_perp_length = abs(dp_det) / max(max(len0, len1), len2);
// Get the position -> barycentric weight matrix
float2x2 inv_dp_mat = get_inverse(dp_mat, dp_det);
float2 weights = min_perp_length < 2 ? 0 : inv_dp_mat * dp;
v0.p.xy += dp * cb.point_size; // Extrapolate position
// Extrapolate texture coords
if (FST)
{
v0.ti.zw += dt * weights;
v0.ti.xy = v0.ti.zw * cb.texture_scale;
}
else
{
v0.t.xy += dt * weights;
v0.ti.zw = v0.t.xy / cb.texture_scale;
v0.t.w += dot(dq, weights);
}
// Extrapolate and clamp color
if (IIP)
{
v0.c += dc * weights;
v0.c = clamp(v0.c, 0, 255);
}
v0.p.z += dot(dz, weights); // Extrapolate depth
v0.t.z += dot(df, weights); // Extrapolate fog
}
vertex MainVSOut vs_main_expand(
uint vid [[vertex_id]],
device const GSMTLMainVertex* vertices [[buffer(GSMTLBufferIndexHWVertices)]],
constant GSMTLMainVSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]],
device const ushort* indices [[buffer(GSMTLBufferIndexHWIndices), function_constant(VS_NEEDS_INDEX_BUFFER)]])
{
switch (VS_EXPAND_TYPE)
{
case VSExpand::None:
return vs_main_run(load_vertex(vertices[vid]), cb);
case VSExpand::Point:
{
MainVSOut point = vs_main_run(load_vertex(vertices[vid >> 2]), cb);
if (vid & 1)
point.p.x += cb.point_size.x;
if (vid & 2)
point.p.y += cb.point_size.y;
return point;
}
case VSExpand::Line:
case VSExpand::LineAA1:
{
uint vid_base = vid >> 2;
bool is_bottom = vid & 2;
bool is_right = vid & 1;
uint vid_other = is_bottom ? vid_base - 1 : vid_base + 1;
MainVSOut point = vs_main_run(load_vertex(vertices[vid_base]), cb);
MainVSOut other = vs_main_run(load_vertex(vertices[vid_other]), cb);
// Use bottom minus top for delta regardless of which vertex we are expanding.
float2 line_delta = is_bottom ? point.p.xy - other.p.xy : other.p.xy - point.p.xy;
float2 line_vector = normalize(line_delta / cb.vertex_scale);
float2 line_expand = float2(line_vector.y, -line_vector.x);
if (VS_EXPAND_TYPE == VSExpand::LineAA1)
line_expand *= 2.f * cb.line_aa1_width;
float2 line_width = (line_expand * cb.point_size) / 2;
float2 offset = is_right ? line_width : -line_width;
point.p.xy += offset;
if (VS_EXPAND_TYPE == VSExpand::LineAA1)
point.inv_cov = is_right ? 1.f : -1.f;
// Lines will be run as (0 1 2) (1 2 3)
// This means that both triangles will have a point based off the top line point as their first point
// So we don't have to do anything for !IIP
return point;
}
case VSExpand::Sprite:
{
uint vid_base = vid >> 1;
bool is_bottom = vid & 2;
bool is_right = vid & 1;
// Sprite points are always in pairs
uint vid_lt = vid_base & ~1;
uint vid_rb = vid_base | 1;
MainVSOut lt = vs_main_run(load_vertex(vertices[vid_lt]), cb);
MainVSOut rb = vs_main_run(load_vertex(vertices[vid_rb]), cb);
MainVSOut out = rb;
if (!is_right)
{
out.p.x = lt.p.x;
out.t.x = lt.t.x;
out.ti.xz = lt.ti.xz;
}
if (!is_bottom)
{
out.p.y = lt.p.y;
out.t.y = lt.t.y;
out.ti.yw = lt.ti.yw;
}
return out;
}
case VSExpand::TriangleAA1:
{
// Triangles with AA1 are expanded as follows:
// - Vertices 0-2: Interior of triangle (1 triangle).
// - Vertices 3-8: First edge expanded (2 triangles).
// - Vertices 9-14: Second edge expanded (2 triangles).
// - Vertices 15-20: Third edge expanded (2 triangles).
// - Vertices 21-26: First corner cap (2 triangles).
// - Vertices 27-32: Second corner cap (2 triangles).
// - Vertices 33-38: Third corner cap (2 triangles).
uint prim_id = vid / 39;
uint prim_offset = vid - 39 * prim_id; // range: 0-38
bool interior = prim_offset < 3;
bool edge = 3 <= prim_offset && prim_offset < 21;
MainVSOut out;
if (interior)
{
out = vs_main_run(load_vertex(vertices[indices[3 * prim_id + prim_offset]]), cb);
out.inv_cov = 0.f;
out.interior = 1;
}
else if (edge)
{
// Vertex indices for this edge. We need all 3 for determining exterior/interior.
uint prim_offset_edges = prim_offset - 3; // range: 0-17
uint i0 = prim_offset_edges / 6;
uint i1 = (i0 >= 2) ? i0 - 2 : i0 + 1;
uint i2 = (i0 >= 1) ? i0 - 1 : i0 + 2;
uint edge_offset = prim_offset_edges - 6 * i0; // range: 0-5
// Note: order of top/bottom, inside/outside is arbitrary,
// as long as it assembles into two triangles forming a quad.
bool is_bottom = (2 <= edge_offset) && (edge_offset <= 4);
bool is_outside = edge_offset & 1;
out = vs_main_run(load_vertex(vertices[indices[3 * prim_id + (is_bottom ? i1 : i0)]]), cb);
MainVSOut other = vs_main_run(load_vertex(vertices[indices[3 * prim_id + (is_bottom ? i0 : i1)]]), cb);
MainVSOut opposite = vs_main_run(load_vertex(vertices[indices[3 * prim_id + i2]]), cb);
float2x2 pos_deltas = get_xy_deltas_unscaled(out, other, opposite, cb);
float2 expand_dir = is_outside ? get_aa1_triangle_expand_dir(out, other, opposite, cb) : 0;
// Do actual extrapolation, or no-op if expand_dir == 0.
extrapolate_aa1_triangle_edge(out, other, opposite, pos_deltas, expand_dir, cb);
out.inv_cov = is_outside ? 1.0f : 0.0f; // No coverage on outside, otherwise full.
out.interior = 0;
}
else // Corner cap
{
// Vertex indices for this cap. We need all 3 for determining exterior/interior.
uint prim_offset_cap = prim_offset - 21; // range: 0-8
uint i0 = prim_offset_cap / 6;
uint i1 = (i0 >= 2) ? i0 - 2 : i0 + 1;
uint i2 = (i0 >= 1) ? i0 - 1 : i0 + 2;
uint cap_offset = prim_offset_cap - 6 * i0; // range: 0-5
bool is_near_corner = cap_offset == 0 || cap_offset == 3;
bool is_far_corner = cap_offset == 2 || cap_offset == 5;
bool is_first_tri = cap_offset < 3;
out = vs_main_run(load_vertex(vertices[indices[3 * prim_id + i0]]), cb);
MainVSOut other = vs_main_run(load_vertex(vertices[indices[3 * prim_id + (is_first_tri ? i1 : i2)]]), cb);
MainVSOut opposite = vs_main_run(load_vertex(vertices[indices[3 * prim_id + (is_first_tri ? i2 : i1)]]), cb);
float2x2 pos_deltas = get_xy_deltas_unscaled(out, other, opposite, cb);
// Get the edge expansion directions of both incident edges.
float2 edge_expand_dir_0 = get_aa1_triangle_expand_dir(out, other, opposite, cb);
float2 edge_expand_dir_1 = get_aa1_triangle_expand_dir(out, opposite, other, cb);
// Check if the corner is already filled by the expanded edges.
// This happens if the expand directions are the same.
// If so we output a degenerate triangle at this corner.
bool corner_filled = all(edge_expand_dir_0 == edge_expand_dir_1);
// Nothing if corner is filled, otherwise opposite to the bisector of the corner angle.
float2 far_corner_dir = corner_filled ? 0 : -normalize((pos_deltas[0] + pos_deltas[1]) / 2);
// Determine the expand direction.
float2 expand_dir = is_near_corner ? 0 : // No extrapolation
is_far_corner ? far_corner_dir : // Opposite to the angle bisector of corner
edge_expand_dir_0; // Standard AA1 edge expansion
// Do the actual extrapolation (no-op if expand_dir == 0).
extrapolate_aa1_triangle_edge(out, other, opposite, pos_deltas, expand_dir, cb);
out.inv_cov = is_near_corner ? 0.0f : 1.0f; // Full coverage at near corner, otherwise none.
out.interior = 0;
if (NOT_IIP)
{
// Get the provoking vertex color (first vertex in Metal)
out.fc = i0 == 0 ? out.fc : (i1 == 0 ? other.fc : opposite.fc);
}
}
return out;
}
}
}
// MARK: - Fragment functions
struct PSMain
{
texture2d<float> tex;
depth2d<float> tex_depth;
texture2d<float> palette;
texture2d<float> prim_id_tex;
sampler tex_sampler;
float4 current_color;
float current_depth;
uint prim_id;
bool color_discarded = false;
bool depth_discarded = false;
const thread MainPSIn& in;
constant GSMTLMainPSUniform& cb;
PSMain(const thread MainPSIn& in, constant GSMTLMainPSUniform& cb): in(in), cb(cb) {}
void discard()
{
if (PS_ROV_COLOR || PS_ROV_DEPTH != ROV_DEPTH::NONE)
color_discarded = depth_discarded = true;
else
discard_fragment();
}
void discard_color(thread float4& output)
{
if (PS_ROV_COLOR)
color_discarded = true;
else
output = current_color;
}
void discard_depth(thread float& output)
{
if (PS_ROV_DEPTH == ROV_DEPTH::READ_WRITE)
depth_discarded = true;
else
output = current_depth;
}
template <typename... Args>
float4 sample_tex(Args... args)
{
if (PS_TEX_IS_DEPTH)
return float4(tex_depth.sample(args...));
else
return tex.sample(args...);
}
float4 read_tex(uint2 pos, uint lod = 0)
{
if (PS_TEX_IS_DEPTH)
return float4(tex_depth.read(pos, lod));
else
return tex.read(pos, lod);
}
uint2 get_tex_dims()
{
if (PS_TEX_IS_DEPTH)
return uint2(tex_depth.get_width(), tex_depth.get_height());
else
return uint2(tex.get_width(), tex.get_height());
}
float manual_lod(float uv_w)
{
// FIXME add LOD: K - ( LOG2(Q) * (1 << L))
float K = cb.lod_params.x;
float L = cb.lod_params.y;
float bias = cb.lod_params.z;
float max_lod = cb.lod_params.w;
float gs_lod = K - log2(abs(uv_w)) * L;
// FIXME max useful ?
//return max(min(gs_lod, max_lod) - bias, 0.0f);
return min(gs_lod, max_lod) - bias;
}
float4 sample_c_af(float2 uv, float uv_w)
{
// HW sampler will reject bad UVs, match that here.
uv = any(isnan(uv) | isinf(uv)) ? float2(0.0f, 0.0f) : uv;
// Large floating point values risk NaN/Inf values.
// Above this value floats lose decimal precision, so seems a resonable limit for UVs.
uv = clamp(uv, -8388608.0f, 8388608.0f);
// Below taken from https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm#7.18.11%20LOD%20Calculations
// And https://registry.khronos.org/OpenGL/extensions/EXT/EXT_texture_filter_anisotropic.txt
// With guidance from https://pema.dev/2025/05/09/mipmaps-too-much-detail/
float2 sz = float2(get_tex_dims());
float2 dX = dfdx(uv) * sz;
float2 dY = dfdy(uv) * sz;
float length_x = length(dX);
float length_y = length(dY);
// Calculate Ellipse Transform
bool d_zero = length_x < 0.001f || length_y < 0.001f;
float f = (dX.x * dY.y - dX.y * dY.x);
bool d_par = f < 0.001f;
bool d_per = dot(dX, dY) < 0.001f;
bool d_inf_nan = any(isinf(dX) | isinf(dY) | isnan(dX) | isnan(dY));
if (!(d_zero || d_par || d_per || d_inf_nan))
{
float A = dX.y * dX.y + dY.y * dY.y;
float B = -2 * (dX.x * dX.y + dY.x * dY.y);
float C = dX.x * dX.x + dY.x * dY.x;
float F = f * f;
float p = A - C;
float q = A + C;
float t = sqrt(p * p + B * B);
float signB = sign(B);
float denom_plus = t * (q + t);
float denom_minus = t * (q - t);
float sqrtA = sqrt(F * (t + p));
float sqrtB = sqrt(F * (t - p));
float inv_sqrt_denom_plus = rsqrt(denom_plus);
float inv_sqrt_denom_minus = rsqrt(denom_minus);
float2 new_dX = float2(
sqrtA * inv_sqrt_denom_plus,
sqrtB * inv_sqrt_denom_plus * signB
);
float2 new_dY = float2(
sqrtB * inv_sqrt_denom_minus * -signB,
sqrtA * inv_sqrt_denom_minus
);
d_inf_nan = any(isinf(new_dX) | isinf(new_dY) | isnan(new_dX) | isnan(new_dY));
if (!d_inf_nan)
{
dX = new_dX;
dY = new_dY;
length_x = length(dX);
length_y = length(dY);
}
}
// Compute AF values
bool is_major_x = length_x > length_y;
float length_major = is_major_x ? length_x : length_y;
float length_minor = is_major_x ? length_y : length_x;
float aniso_ratio;
float length_lod;
float2 aniso_line;
if (length_major <= 1.0f)
{
// A zero length_major would result in NaN Lod and break sampling.
// A small length_major would result in aniso_ratio getting clamped to 1.
// Perform isotropic filtering instead.
aniso_ratio = 1.0f;
length_lod = length_major;
aniso_line = float2(0.0f, 0.0f);
}
else
{
float2 aniso_line_dir = is_major_x ? dX : dY;
aniso_ratio = min(length_major / length_minor, float(PS_SW_ANISO));
length_lod = length_major / aniso_ratio;
// clamp to top Lod
if (length_lod < 1.0f)
aniso_ratio = max(1.0f, aniso_ratio * length_lod);
aniso_ratio = round(aniso_ratio);
aniso_line = aniso_line_dir * 0.5f * (1.0f / sz);
}
float lod = PS_AUTOMATIC_LOD ? log2(length_lod) : PS_MANUAL_LOD ? manual_lod(uv_w) : 0.0f;
float4 colour;
if (aniso_ratio == 1.0f)
{
colour = sample_tex(tex_sampler, uv, level(lod));
}
else
{
float4 num = float4(0.0f, 0.0f, 0.0f, 0.0f);
float2 segment = (2.0f * aniso_line) / aniso_ratio;
for (int i = 0; i < aniso_ratio; i++)
{
float2 d = -aniso_line + (0.5f + i) * segment;
float2 uv_sample = uv + d;
float4 sample_colour = sample_tex(tex_sampler, uv_sample, level(lod));
num += sample_colour;
}
colour = num / aniso_ratio;
}
return colour;
}
float4 sample_c(float2 uv)
{
if (PS_TEX_IS_FB)
return current_color;
if (PS_REGION_RECT)
return read_tex(uint2(uv));
if (!PS_ADJS && !PS_ADJT)
{
uv *= cb.st_scale;
}
else
{
if (PS_ADJS)
uv.x = (uv.x - cb.st_range.x) * cb.st_range.z;
else
uv.x = uv.x * cb.st_scale.x;
if (PS_ADJT)
uv.y = (uv.y - cb.st_range.y) * cb.st_range.w;
else
uv.y = uv.y * cb.st_scale.y;
}
if (PS_SW_ANISO > 1)
return sample_c_af(uv, in.t.w);
else if (PS_AUTOMATIC_LOD)
return sample_tex(tex_sampler, uv);
else if (PS_MANUAL_LOD)
return sample_tex(tex_sampler, uv, level(manual_lod(in.t.w)));
else
return sample_tex(tex_sampler, uv, level(0));
}
float4 sample_p(uint idx)
{
return palette.read(uint2(idx, 0));
}
float4 sample_p_norm(float u)
{
return sample_p(uint(u * 255.5f));
}
float4 clamp_wrap_uv(float4 uv)
{
float4 tex_size = cb.wh.xyxy;
if (PS_WMS == PS_WMT)
{
if (PS_REGION_RECT && PS_WMS == 0)
{
uv = fract(uv);
}
else if (PS_REGION_RECT && PS_WMS == 1)
{
uv = saturate(uv);
}
else if (PS_WMS == 2)
{
uv = clamp(uv, cb.uv_min_max.xyxy, cb.uv_min_max.zwzw);
}
else if (PS_WMS == 3)
{
// wrap negative uv coords to avoid an off by one error that shifted
// textures. Fixes Xenosaga's hair issue.
if (!FST)
uv = fract(uv);
uv = float4((ushort4(uv * tex_size) & ushort4(cb.uv_msk_fix.xyxy)) | ushort4(cb.uv_msk_fix.zwzw)) / tex_size;
}
}
else
{
if (PS_REGION_RECT && PS_WMS == 0)
{
uv.xz = fract(uv.xz);
}
else if (PS_REGION_RECT && PS_WMS == 1)
{
uv.xz = saturate(uv.xz);
}
else if (PS_WMS == 2)
{
uv.xz = clamp(uv.xz, cb.uv_min_max.xx, cb.uv_min_max.zz);
}
else if (PS_WMS == 3)
{
if (!FST)
uv.xz = fract(uv.xz);
uv.xz = float2((ushort2(uv.xz * tex_size.xx) & ushort2(cb.uv_msk_fix.xx)) | ushort2(cb.uv_msk_fix.zz)) / tex_size.xx;
}
if (PS_REGION_RECT && PS_WMT == 0)
{
uv.yw = fract(uv.yw);
}
else if (PS_REGION_RECT && PS_WMT == 1)
{
uv.yw = saturate(uv.yw);
}
else if (PS_WMT == 2)
{
uv.yw = clamp(uv.yw, cb.uv_min_max.yy, cb.uv_min_max.ww);
}
else if (PS_WMT == 3)
{
if (!FST)
uv.yw = fract(uv.yw);
uv.yw = float2((ushort2(uv.yw * tex_size.yy) & ushort2(cb.uv_msk_fix.yy)) | ushort2(cb.uv_msk_fix.ww)) / tex_size.yy;
}
}
if (PS_REGION_RECT)
{
// Normalized -> Integer Coordinates.
uv = clamp(uv * cb.wh.zwzw + cb.st_range.xyxy, cb.st_range.xyxy, cb.st_range.zwzw);
}
return uv;
}
float4x4 sample_4c(float4 uv)
{
return {
sample_c(uv.xy),
sample_c(uv.zy),
sample_c(uv.xw),
sample_c(uv.zw),
};
}
uint4 sample_4_index(float4 uv)
{
float4 c;
// Either GS will send a texture that contains a single alpha channel
// Or we have an old RT (ie RGBA8) that contains index (4/8) in the alpha channel
// Note: texture gather can't be used because of special clamping/wrapping
// Also it doesn't support lod
c.x = sample_c(uv.xy).a;
c.y = sample_c(uv.zy).a;
c.z = sample_c(uv.xw).a;
c.w = sample_c(uv.zw).a;
uint4 i;
if (PS_RTA_SRC_CORRECTION)
{
i = uint4(round(c * 128.25f)); // Denormalize value
}
else
{
i = uint4(c * 255.5f); // Denormalize value
}
if (PS_PAL_FMT == 1)
return i & 0xF;
if (PS_PAL_FMT == 2)
return i >> 4;
return i;
}
float4x4 sample_4p(uint4 u)
{
return {
sample_p(u.x),
sample_p(u.y),
sample_p(u.z),
sample_p(u.w),
};
}
uint fetch_raw_depth()
{
return tex_depth.read(ushort2(in.p.xy + cb.channel_shuffle_offset)) * 0x1p32f;
}
float4 fetch_raw_color()
{
if (PS_TEX_IS_FB)
return current_color;
else
return tex.read(ushort2(in.p.xy + cb.channel_shuffle_offset));
}
float4 fetch_c(ushort2 uv)
{
if (PS_TEX_IS_FB)
return current_color;
else if (PS_TEX_IS_DEPTH)
return tex_depth.read(uv);
else
return tex.read(uv);
}
// MARK: Depth sampling
ushort2 clamp_wrap_uv_depth(ushort2 uv)
{
ushort2 uv_out = uv;
// Keep the full precision
// It allow to multiply the ScalingFactor before the 1/16 coeff
ushort4 mask = ushort4(cb.uv_msk_fix) << 4;
if (PS_WMS == PS_WMT)
{
if (PS_WMS == 2)
uv_out = clamp(uv, mask.xy, mask.zw);
else if (PS_WMS == 3)
uv_out = (uv & mask.xy) | mask.zw;
}
else
{
if (PS_WMS == 2)
uv_out.x = clamp(uv.x, mask.x, mask.z);
else if (PS_WMS == 3)
uv_out.x = (uv.x & mask.x) | mask.z;
if (PS_WMT == 2)
uv_out.y = clamp(uv.y, mask.y, mask.w);
else if (PS_WMT == 3)
uv_out.y = (uv.y & mask.y) | mask.w;
}
return uv_out;
}
float4 sample_depth(float2 st)
{
float2 uv_f = float2(clamp_wrap_uv_depth(ushort2(st))) * float2(cb.scale_factor.x);
if (PS_REGION_RECT)
uv_f = clamp(uv_f + cb.st_range.xy, cb.st_range.xy, cb.st_range.zw);
ushort2 uv = ushort2(uv_f);
float4 t = float4(0);
if (PS_TALES_OF_ABYSS_HLE)
{
// Warning: UV can't be used in channel effect
ushort depth = fetch_raw_depth();
// Convert msb based on the palette
t = palette.read(ushort2((depth >> 8) & 0xFF, 0)) * 255.f;
}
else if (PS_URBAN_CHAOS_HLE)
{
// Depth buffer is read as a RGB5A1 texture. The game try to extract the green channel.
// So it will do a first channel trick to extract lsb, value is right-shifted.
// Then a new channel trick to extract msb which will shifted to the left.
// OpenGL uses a FLOAT32 format for the depth so it requires a couple of conversion.
// To be faster both steps (msb&lsb) are done in a single pass.
// Warning: UV can't be used in channel effect
ushort depth = fetch_raw_depth();
// Convert lsb based on the palette
t = palette.read(ushort2(depth & 0xFF, 0)) * 255.f;
// Msb is easier
float green = float((depth >> 8) & 0xFF) * 36.f;
green = min(green, 255.0f);
t.g += green;
}
else if (PS_DEPTH_FMT == 1)
{
t = convert_depth32_rgba8(fetch_c(uv).r);
}
else if (PS_DEPTH_FMT == 2)
{
t = convert_depth16_rgba8(fetch_c(uv).r);
}
else if (PS_DEPTH_FMT == 3)
{
t = fetch_c(uv) * 255.f;
}
// macOS 10.15 ICE's on bool3(t.rgb), so use != 0 instead
if (PS_AEM_FMT == FMT_24)
t.a = (!PS_AEM || any(t.rgb != 0)) ? 255.f * cb.ta.x : 0.f;
else if (PS_AEM_FMT == FMT_16)
t.a = t.a >= 128.f ? 255.f * cb.ta.y : (!PS_AEM || any(t.rgb != 0)) ? 255.f * cb.ta.x : 0.f;
else if (PS_PAL_FMT != 0 && !PS_TALES_OF_ABYSS_HLE && !PS_URBAN_CHAOS_HLE)
t = trunc(sample_4p(uint4(t.aaaa))[0] * 255.0f + 0.05f);
return t;
}
// MARK: Fetch a Single Channel
float4 fetch_red()
{
float rt = PS_TEX_IS_DEPTH ? float(fetch_raw_depth() & 0xFF) / 255.f : fetch_raw_color().r;
return sample_p_norm(rt) * 255.f;
}
float4 fetch_green()
{
float rt = PS_TEX_IS_DEPTH ? float((fetch_raw_depth() >> 8) & 0xFF) / 255.f : fetch_raw_color().g;
return sample_p_norm(rt) * 255.f;
}
float4 fetch_blue()
{
float rt = PS_TEX_IS_DEPTH ? float((fetch_raw_depth() >> 16) & 0xFF) / 255.f : fetch_raw_color().b;
return sample_p_norm(rt) * 255.f;
}
float4 fetch_alpha()
{
return sample_p_norm(fetch_raw_color().a) * 255.f;
}
float4 fetch_rgb()
{
float4 rt = fetch_raw_color();
return float4(sample_p_norm(rt.r).r, sample_p_norm(rt.g).g, sample_p_norm(rt.b).b, 1) * 255.f;
}
float4 fetch_gXbY()
{
if (PS_TEX_IS_DEPTH)
{
uint depth = fetch_raw_depth();
uint bg = (depth >> (8 + cb.channel_shuffle.green_shift)) & 0xFF;
return float4(bg);
}
else
{
uint4 rt = uint4(fetch_raw_color() * 255.5f);
uint green = (rt.g >> cb.channel_shuffle.green_shift) & cb.channel_shuffle.green_mask;
uint blue = (rt.b >> cb.channel_shuffle.blue_shift) & cb.channel_shuffle.blue_mask;
return float4(green | blue);
}
}
float4 sample_color(float2 st)
{
if (PS_TCOFFSETHACK)
st += cb.tc_offset;
float4 t;
float4x4 c;
float2 dd;
if (!PS_LTF && PS_AEM_FMT == FMT_32 && PS_PAL_FMT == 0 && !PS_REGION_RECT && PS_WMS < 2 && PS_WMT < 2)
{
c[0] = sample_c(st);
}
else
{
float4 uv;
if (PS_LTF)
{
uv = st.xyxy + cb.half_texel;
dd = fract(uv.xy * cb.wh.zw);
if (!FST)
{
// Background in Shin Megami Tensei Lucifers
// I suspect that uv isn't a standard number, so fract is outside of the [0;1] range
dd = saturate(dd);
}
}
else
{
uv = st.xyxy;
}
uv = clamp_wrap_uv(uv);
if (PS_PAL_FMT != 0)
c = sample_4p(sample_4_index(uv));
else
c = sample_4c(uv);
}
for (int i = 0; i < 4; i++)
{
// macOS 10.15 ICE's on bool3(c[i].rgb), so use != 0 instead
if (PS_AEM_FMT == FMT_24)
c[i].a = !PS_AEM || any(c[i].rgb != 0) ? cb.ta.x : 0.f;
else if (PS_AEM_FMT == FMT_16)
c[i].a = c[i].a >= 0.5 ? cb.ta.y : !PS_AEM || any((int3(c[i].rgb * 255.0f) & 0xF8) != 0) ? cb.ta.x : 0.f;
}
if (PS_LTF)
t = mix(mix(c[0], c[1], dd.x), mix(c[2], c[3], dd.x), dd.y);
else
t = c[0];
if (PS_AEM_FMT == FMT_32 && PS_PAL_FMT == 0 && PS_RTA_SRC_CORRECTION)
t.a = t.a * (128.5f / 255.0f);
// The 0.05f helps to fix the overbloom of sotc
// I think the issue is related to the rounding of texture coodinate. The linear (from fixed unit)
// interpolation could be slightly below the correct one.
return trunc(t * 255.f + 0.05f);
}
float4 tfx(float4 T, float4 C)
{
float4 C_out;
float4 FxT = trunc((C * T) / 128.f);
if (PS_TFX == 0)
C_out = FxT;
else if (PS_TFX == 1)
C_out = T;
else if (PS_TFX == 2)
C_out = float4(FxT.rgb, T.a) + C.a;
else if (PS_TFX == 3)
C_out = float4(FxT.rgb + C.a, T.a);
else
C_out = C;
if (!PS_TCC)
C_out.a = C.a;
// Clamp only when it is useful
if (PS_TFX == 0 || PS_TFX == 2 || PS_TFX == 3)
C_out = min(C_out, 255.f);
return C_out;
}
bool atst(float4 C)
{
float a = C.a;
switch (PS_ATST)
{
case ATST::NONE:
break; // Nothing to do
case ATST::LEQUAL:
if (a > cb.aref)
return false;
break;
case ATST::GEQUAL:
if (a < cb.aref)
return false;
break;
case ATST::EQUAL:
if (abs(a - cb.aref) > 0.5f)
return false;
break;
case ATST::NOTEQUAL:
if (abs(a - cb.aref) < 0.5f)
return false;
break;
}
return true;
}
void fog(thread float4& C, float f)
{
if (PS_FOG)
C.rgb = trunc(mix(cb.fog_color, C.rgb, (f * 255.0f) / 256.0f));
}
float4 ps_color()
{
float2 st, st_int;
if (!FST)
{
st = in.t.xy / in.t.w;
st_int = in.ti.zw / in.t.w;
}
else
{
// Note: xy are normalized coordinates
st = in.ti.xy;
st_int = in.ti.zw;
}
float4 T;
if (PS_CHANNEL == 1)
T = fetch_red();
else if (PS_CHANNEL == 2)
T = fetch_green();
else if (PS_CHANNEL == 3)
T = fetch_blue();
else if (PS_CHANNEL == 4)
T = fetch_alpha();
else if (PS_CHANNEL == 5)
T = fetch_rgb();
else if (PS_CHANNEL == 6)
T = fetch_gXbY();
else if (PS_DEPTH_FMT != 0)
T = sample_depth(st_int);
else
T = sample_color(st);
if (PS_SHUFFLE && !PS_SHUFFLE_SAME && !PS_READ16_SRC && !(PS_PROCESS_BA == SHUFFLE_READWRITE && PS_PROCESS_RG == SHUFFLE_READWRITE))
{
uint4 denorm_c_before = uint4(T);
if (PS_PROCESS_BA & SHUFFLE_READ)
{
T.r = float((denorm_c_before.b << 3) & 0xF8);
T.g = float(((denorm_c_before.b >> 2) & 0x38) | ((denorm_c_before.a << 6) & 0xC0));
T.b = float((denorm_c_before.a << 1) & 0xF8);
T.a = float(denorm_c_before.a & 0x80);
}
else
{
T.r = float((denorm_c_before.r << 3) & 0xF8);
T.g = float(((denorm_c_before.r >> 2) & 0x38) | ((denorm_c_before.g << 6) & 0xC0));
T.b = float((denorm_c_before.g << 1) & 0xF8);
T.a = float(denorm_c_before.g & 0x80);
}
T.a = (T.a >= 127.5 ? cb.ta.y : !PS_AEM || any((int3(T.rgb) & 0xF8) != 0) ? cb.ta.x : 0.f) * 255.f;
}
float4 C = tfx(T, IIP ? in.c : in.fc);
fog(C, in.t.z);
return C;
}
void ps_fbmask(thread float4& C)
{
if (PS_FBMASK)
{
float multi = PS_COLCLIP_HW ? 65535.0 : 255.5;
C = float4((uint4(int4(C)) & (cb.fbmask ^ 0xff)) | (uint4(current_color * float4(multi, multi, multi, 255)) & cb.fbmask));
}
}
void ps_dither(thread float4& C, float As)
{
if (PS_DITHER == 0 || PS_DITHER == 3)
return;
ushort2 fpos;
if (PS_DITHER == 2)
fpos = ushort2(in.p.xy);
else
fpos = ushort2(in.p.xy * float2(cb.scale_factor.y));
float value = cb.dither_matrix[fpos.y & 3][fpos.x & 3];
// The idea here is we add on the dither amount adjusted by the alpha before it goes to the hw blend
// so after the alpha blend the resulting value should be the same as (Cs - Cd) * As + Cd + Dither.
if (PS_DITHER_ADJUST)
{
float Alpha = PS_BLEND_C == 2 ? cb.alpha_fix : As;
value *= Alpha > 0.f ? min(1.f / Alpha, 1.f) : 1.f;
}
if (PS_ROUND_INV)
C.rgb -= value;
else
C.rgb += value;
}
void ps_color_clamp_wrap(thread float4& C)
{
// When dithering the bottom 3 bits become meaningless and cause lines in the picture
// so we need to limit the color depth on dithered items
if (SW_BLEND || (PS_DITHER > 0 && PS_DITHER < 3) || PS_FBMASK)
{
if (PS_DST_FMT == FMT_16 && PS_BLEND_MIX == 0 && PS_ROUND_INV)
C.rgb += 7.f; // Need to round up, not down since the shader will invert
// Correct the Color value based on the output format
if (PS_COLCLIP == 0 && PS_COLCLIP_HW == 0)
C.rgb = clamp(C.rgb, 0.f, 255.f); // Standard Clamp
// FIXME rouding of negative float?
// compiler uses trunc but it might need floor
// Warning: normally blending equation is mult(A, B) = A * B >> 7. GPU have the full accuracy
// GS: Color = 1, Alpha = 255 => output 1
// GPU: Color = 1/255, Alpha = 255/255 * 255/128 => output 1.9921875
// In 16 bits format, only 5 bits of colors are used. It impacts shadows computation of Castlevania
if (PS_DST_FMT == FMT_16 && PS_DITHER != 3 && (PS_BLEND_MIX == 0 || PS_DITHER))
C.rgb = float3(short3(C.rgb) & 0xF8);
else if (PS_COLCLIP == 1 || PS_COLCLIP_HW == 1)
C.rgb = float3(short3(C.rgb) & 0xFF);
}
else if (PS_DST_FMT == FMT_16 && PS_DITHER != 3 && PS_BLEND_MIX == 0 && PS_BLEND_HW == 0)
C.rgb = float3(short3(C.rgb) & 0xF8);
}
template <typename T>
static T pick(uint selector, T zero, T one, T two)
{
return selector == 0 ? zero : selector == 1 ? one : two;
}
void ps_blend(thread float4& Color, thread float4& As_rgba)
{
float As = As_rgba.a;
if (SW_BLEND)
{
// PABE
if (PS_PABE)
{
// As_rgba needed for accumulation blend to manipulate Cd.
// No blending so early exit
if (As < 1.f)
{
As_rgba.rgb = float3(0.f);
return;
}
As_rgba.rgb = float3(1.f);
}
float Ad = PS_RTA_CORRECTION ? trunc(current_color.a * 128.1f) / 128.f : trunc(current_color.a * 255.1f) / 128.f;
if (PS_SHUFFLE && NEEDS_RT)
{
uint4 denorm_rt = uint4(current_color);
if (PS_PROCESS_BA & SHUFFLE_WRITE)
{
current_color.r = float((denorm_rt.b << 3) & 0xF8);
current_color.g = float(((denorm_rt.b >> 2) & 0x38) | ((denorm_rt.a << 6) & 0xC0));
current_color.b = float((denorm_rt.a << 1) & 0xF8);
current_color.a = float(denorm_rt.a & 0x80);
}
else
{
current_color.r = float((denorm_rt.r << 3) & 0xF8);
current_color.g = float(((denorm_rt.r >> 2) & 0x38) | ((denorm_rt.g << 6) & 0xC0));
current_color.b = float((denorm_rt.g << 1) & 0xF8);
current_color.a = float(denorm_rt.g & 0x80);
}
}
float multi = PS_COLCLIP_HW ? 65535.0 : 255.5;
float3 Cd = trunc(current_color.rgb * multi);
float3 Cs = Color.rgb;
float3 A = pick(PS_BLEND_A, Cs, Cd, float3(0.f));
float3 B = pick(PS_BLEND_B, Cs, Cd, float3(0.f));
float C = pick(PS_BLEND_C, As, Ad, cb.alpha_fix);
float3 D = pick(PS_BLEND_D, Cs, Cd, float3(0.f));
// As/Af clamp alpha for Blend mix
// We shouldn't clamp blend mix with blend hw 1 as we want alpha higher
float C_clamped = C;
if (PS_BLEND_MIX > 0 && PS_BLEND_HW != 1 && PS_BLEND_HW != 2)
C_clamped = saturate(C_clamped);
if (PS_BLEND_A == PS_BLEND_B)
Color.rgb = D;
// In blend_mix, HW adds on some alpha factor * dst.
// Truncating here wouldn't quite get the right result because it prevents the <1 bit here from combining with a <1 bit in dst to form a ≥1 amount that pushes over the truncation.
// Instead, apply an offset to convert HW's round to a floor.
// Since alpha is in 1/128 increments, subtracting (0.5 - 0.5/128 == 127/256) would get us what we want if GPUs blended in full precision.
// But they don't. Details here: https://github.com/PCSX2/pcsx2/pull/6809#issuecomment-1211473399
// Based on the scripts at the above link, the ideal choice for Intel GPUs is 126/256, AMD 120/256. Nvidia is a lost cause.
// 124/256 seems like a reasonable compromise, providing the correct answer 99.3% of the time on Intel (vs 99.6% for 126/256), and 97% of the time on AMD (vs 97.4% for 120/256).
else if (PS_BLEND_MIX == 2)
Color.rgb = ((A - B) * C_clamped + D) + (124.f/256.f);
else if (PS_BLEND_MIX == 1)
Color.rgb = ((A - B) * C_clamped + D) - (124.f/256.f);
else
Color.rgb = trunc((A - B) * C + D);
if (PS_BLEND_HW == 1)
{
// As or Af
As_rgba.rgb = float3(C);
// Subtract 1 for alpha to compensate for the changed equation,
// if c.rgb > 255.0f then we further need to adjust alpha accordingly,
// we pick the lowest overflow from all colors because it's the safest,
// we divide by 255 the color because we don't know Cd value,
// changed alpha should only be done for hw blend.
float3 alpha_compensate = max(float3(1.f), Color.rgb / float3(255.f));
As_rgba.rgb -= alpha_compensate;
}
else if (PS_BLEND_HW == 2)
{
// Since we can't do Cd*(Alpha + 1) - Cs*Alpha in hw blend
// what we can do is adjust the Cs value that will be
// subtracted, this way we can get a better result in hw blend.
// Result is still wrong but less wrong than before.
float division_alpha = 1.f + C;
Color.rgb /= float3(division_alpha);
}
else if (PS_BLEND_HW == 3)
{
// As, Ad or Af clamped.
As_rgba.rgb = float3(C_clamped);
// Cs*(Alpha + 1) might overflow, if it does then adjust alpha value
// that is sent on second output to compensate.
float3 overflow_check = (Color.rgb - float3(255.f)) / 255.f;
float3 alpha_compensate = max(float3(0.f), overflow_check);
As_rgba.rgb -= alpha_compensate;
}
}
else
{
if (PS_BLEND_HW == 1)
{
// Needed for Cd * (As/Ad/F + 1) blending modes
Color.rgb = 255.f;
}
else if (PS_BLEND_HW == 2)
{
// Cd*As,Cd*Ad or Cd*F
float Alpha = PS_BLEND_C == 2 ? cb.alpha_fix : As;
Color.rgb = saturate(Alpha - 1.f) * 255.f;
}
else if (PS_BLEND_HW == 3 && PS_RTA_CORRECTION == 0)
{
// Needed for Cs*Ad, Cs*Ad + Cd, Cd - Cs*Ad
// Multiply Color.rgb by (255/128) to compensate for wrong Ad/255 value when rgb are below 128.
// When any color channel is higher than 128 then adjust the compensation automatically
// to give us more accurate colors, otherwise they will be wrong.
// The higher the value (>128) the lower the compensation will be.
float max_color = max(max(Color.r, Color.g), Color.b);
float color_compensate = 255.f / max(128.f, max_color);
Color.rgb *= float3(color_compensate);
}
}
}
MainResult ps_main()
{
MainResult out = {};
float input_z = in.p.z;
if (PS_ZFLOOR)
input_z = floor(input_z * 0x1p32) * 0x1p-32;
if (PS_ZTST == ZTST::GEQUAL || PS_ZTST == ZTST::GREATER)
{
if (PS_ZTST == ZTST::GEQUAL && input_z < current_depth)
discard();
if (PS_ZTST == ZTST::GREATER && input_z <= current_depth)
discard();
}
if (PS_SCANMSK & 2)
{
if ((uint(in.p.y) & 1) == (PS_SCANMSK & 1))
discard();
}
if (PS_DATE >= 5)
{
// 1 => DATM == 0, 2 => DATM == 1
float rt_a = PS_WRITE_RG ? current_color.g : current_color.a;
bool bad = PS_RTA_CORRECTION ? ((PS_DATE & 3) == 1 ? (rt_a > (254.5f / 255.f)) : (rt_a < (254.5f / 255.f))) : ((PS_DATE & 3) == 1 ? (rt_a > 0.5) : (rt_a < 0.5));
if (bad)
discard();
}
if (PS_DATE == 3)
{
float stencil_ceil = prim_id_tex.read(uint2(in.p.xy)).r;
// Note prim_id == stencil_ceil will be the primitive that will update
// the bad alpha value so we must keep it.
if (float(prim_id) > stencil_ceil)
discard();
}
float4 C = ps_color();
// Must be done before alpha correction
if (PS_AA1 != AA1::NONE)
{
float cov = PS_AA1 == AA1::LINE
? saturate(cb.line_cov_scale * (1.f - abs(in.inv_cov))) // Blur only outer part of the line by scaling coverage.
: saturate(1.f - abs(in.inv_cov));
if (!PS_ABE || floor(C.a) == 128.f) // The coverage is only used if the fragment alpha is 128.
C.a = 128.f * cov;
}
else if (PS_FIXED_ONE_A)
{
// AA (Fixed one) will output a coverage of 1.0 as alpha
C.a = 128.0f;
}
bool atst_pass = atst(C);
if (PS_AFAIL == AFAIL::KEEP && !atst_pass)
discard();
float4 alpha_blend = float4(0.f);
if (SW_AD_TO_HW)
{
alpha_blend = PS_RTA_CORRECTION ? float4(trunc(current_color.a * 128.f) / 128.f) : float4(trunc(current_color.a * 255.5f) / 128.f);
}
else
{
alpha_blend = float4(C.a / 128.f);
}
if (PS_DST_FMT == FMT_16)
{
float A_one = 128.f;
C.a = (PS_FBA) ? A_one : step(128.f, C.a) * A_one;
}
else if (PS_DST_FMT == FMT_32 && PS_FBA)
{
if (C.a < 128.f)
C.a += 128.f;
}
// Get first primitive that will write a failing alpha value
if (PS_DATE == 1)
{
// DATM == 0, Pixel with alpha equal to 1 will failed (128-255)
out.c0 = C.a > 127.5f ? float(prim_id) : FLT_MAX;
return out;
}
else if (PS_DATE == 2)
{
// DATM == 1, Pixel with alpha equal to 0 will failed (0-127)
out.c0 = C.a < 127.5f ? float(prim_id) : FLT_MAX;
return out;
}
ps_blend(C, alpha_blend);
if (PS_SHUFFLE)
{
if (!PS_SHUFFLE_SAME && !PS_READ16_SRC && !(PS_PROCESS_BA == SHUFFLE_READWRITE && PS_PROCESS_RG == SHUFFLE_READWRITE))
{
uint4 denorm_c_after = uint4(C);
if (PS_PROCESS_BA & SHUFFLE_READ)
{
C.b = float(((denorm_c_after.r >> 3) & 0x1F) | ((denorm_c_after.g << 2) & 0xE0));
C.a = float(((denorm_c_after.g >> 6) & 0x3) | ((denorm_c_after.b >> 1) & 0x7C) | (denorm_c_after.a & 0x80));
}
else
{
C.r = float(((denorm_c_after.r >> 3) & 0x1F) | ((denorm_c_after.g << 2) & 0xE0));
C.g = float(((denorm_c_after.g >> 6) & 0x3) | ((denorm_c_after.b >> 1) & 0x7C) | (denorm_c_after.a & 0x80));
}
}
// Special case for 32bit input and 16bit output, shuffle used by The Godfather
if (PS_SHUFFLE_SAME)
{
uint4 denorm_c = uint4(C);
if (PS_PROCESS_BA & SHUFFLE_READ)
C = (denorm_c.b & 0x7F) | (denorm_c.a & 0x80);
else
C.ga = C.rg;
}
// Copy of a 16bit source in to this target
else if (PS_READ16_SRC)
{
uint4 denorm_c = uint4(C);
uint2 denorm_TA = uint2(cb.ta * 255.5f);
C.rb = (denorm_c.r >> 3) | (((denorm_c.g >> 3) & 0x7) << 5);
C.ga = (denorm_c.g >> 6) | ((denorm_c.b >> 3) << 2) | (denorm_TA.x & 0x80);
}
else if (PS_SHUFFLE_ACROSS)
{
if (PS_PROCESS_BA == SHUFFLE_READWRITE && PS_PROCESS_RG == SHUFFLE_READWRITE)
{
C.br = C.rb;
C.ag = C.ga;
}
else if(PS_PROCESS_BA & SHUFFLE_READ)
{
C.rb = C.bb;
C.ga = C.aa;
}
else
{
C.rb = C.rr;
C.ga = C.gg;
}
}
}
ps_dither(C, alpha_blend.a);
// Color clamp/wrap needs to be done after sw blending and dithering
ps_color_clamp_wrap(C);
ps_fbmask(C);
// Use alpha blend factor to determine whether to update A.
if (PS_AFAIL == AFAIL::RGB_ONLY_DSB)
alpha_blend.a = float(atst_pass);
if (!PS_NO_COLOR)
{
out.c0.a = PS_RTA_CORRECTION ? C.a / 128.f : C.a / 255.f;
out.c0.rgb = PS_COLCLIP_HW ? float3(C.rgb / 65535.f) : C.rgb / 255.f;
}
if (!PS_NO_COLOR1)
out.c1 = alpha_blend;
if (PS_ZCLAMP)
input_z = min(input_z, cb.max_depth);
if (PS_AA1 == AA1::TRIANGLE_SW_Z && !in.interior)
discard_depth(input_z); // No depth update for triangle edges.
if (!atst_pass)
{
if (PS_AFAIL == AFAIL::RGB_ONLY_SW_Z || PS_AFAIL == AFAIL::RGB_ONLY)
out.c0.a = current_color.a; // discard alpha
else if (PS_AFAIL == AFAIL::ZB_ONLY)
discard_color(out.c0);
if (PS_AFAIL == AFAIL::RGB_ONLY_SW_Z || PS_AFAIL == AFAIL::FB_ONLY)
discard_depth(input_z);
}
out.depth = input_z;
return out;
}
};
#if FBFETCH_SUPPORT
fragment float4 fbfetch_test(float4 in [[color(0), raster_order_group(0)]])
{
return in * 2;
}
constant bool NEEDS_RT_TEX = NEEDS_RT && !HAS_FBFETCH && !PS_ROV_COLOR;
constant bool NEEDS_RT_FBF = NEEDS_RT && HAS_FBFETCH && !PS_ROV_COLOR;
constant bool NEEDS_DS_FBF = SW_DEPTH && HAS_FBFETCH && !DEPTH_FEEDBACK && PS_ROV_DEPTH == ROV_DEPTH::NONE;
#else
constant bool NEEDS_RT_TEX = NEEDS_RT && !PS_ROV_COLOR;
constant bool NEEDS_DS_FBF = false;
constant float ds_fbf = 0;
#endif
constant bool NEEDS_DS_TEX = SW_DEPTH && !DEPTH_FEEDBACK && !NEEDS_DS_FBF && PS_ROV_DEPTH == ROV_DEPTH::NONE;
constant bool NEEDS_DS_DEPTH = (SW_DEPTH && DEPTH_FEEDBACK || NEEDS_DS_FBF) && PS_ROV_DEPTH == ROV_DEPTH::NONE;
constant bool NEEDS_RT_ROV = PS_ROV_COLOR && !ROV_NEEDS_R32;
constant bool NEEDS_RT_U32 = PS_ROV_COLOR && ROV_NEEDS_R32;
constant bool NEEDS_DS_ROV = PS_ROV_DEPTH != ROV_DEPTH::NONE;
fragment MainPSOut ps_main(
MainPSIn in [[stage_in]],
constant GSMTLMainPSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]],
sampler s [[sampler(0)]],
#if PRIMID_SUPPORT
uint primid [[primitive_id, function_constant(NEEDS_PRIMID)]],
#endif
#if FBFETCH_SUPPORT
float4 rt_fbf [[color(0), raster_order_group(0), function_constant(NEEDS_RT_FBF)]],
float ds_fbf [[color(1), raster_order_group(1), function_constant(NEEDS_DS_FBF)]],
#endif
texture2d<float> tex [[texture(GSMTLTextureIndexTex), function_constant(PS_TEX_IS_COLOR)]],
depth2d<float> depth [[texture(GSMTLTextureIndexTex), function_constant(PS_TEX_IS_DEPTH)]],
texture2d<float> palette [[texture(GSMTLTextureIndexPalette), function_constant(PS_HAS_PALETTE)]],
texture2d<float> rt [[texture(GSMTLTextureIndexRenderTarget), function_constant(NEEDS_RT_TEX)]],
texture2d<float> primidtex [[texture(GSMTLTextureIndexPrimIDs), function_constant(PS_PRIM_CHECKING_READ)]],
texture2d<float> ds_tex [[texture(GSMTLTextureIndexDepthTarget), function_constant(NEEDS_DS_TEX)]],
depth2d<float> ds_depth [[texture(GSMTLTextureIndexDepthTarget), function_constant(NEEDS_DS_DEPTH)]],
texture2d<float, access::read_write> rt_rov [[texture(GSMTLTextureIndexRenderTarget), raster_order_group(0), function_constant(NEEDS_RT_ROV)]],
texture2d<uint, access::read_write> rt_u32 [[texture(GSMTLTextureIndexRenderTarget), raster_order_group(0), function_constant(NEEDS_RT_U32)]],
texture2d<float, access::read_write> ds_rov [[texture(GSMTLTextureIndexDepthTarget), raster_order_group(1), function_constant(NEEDS_DS_ROV)]])
{
PSMain main(in, cb);
main.tex_sampler = s;
if (PS_TEX_IS_COLOR)
main.tex = tex;
else
main.tex_depth = depth;
if (PS_HAS_PALETTE)
main.palette = palette;
if (PS_PRIM_CHECKING_READ)
main.prim_id_tex = primidtex;
#if PRIMID_SUPPORT
if (NEEDS_PRIMID)
main.prim_id = primid;
#endif
uint2 coord = uint2(in.p.xy);
if (SW_DEPTH)
{
if (PS_ROV_DEPTH != ROV_DEPTH::NONE)
main.current_depth = ds_rov.read(coord).x;
else if (DEPTH_FEEDBACK)
main.current_depth = ds_depth.read(coord);
else if (NEEDS_DS_FBF)
main.current_depth = ds_fbf < 0 ? ds_depth.read(coord) : ds_fbf;
else
main.current_depth = ds_tex.read(coord).x;
}
if (NEEDS_RT || (PS_ROV_COLOR && any(cb.fbmask == 0xff)))
{
if (PS_ROV_COLOR)
{
if (ROV_NEEDS_R32)
main.current_color = unpack_unorm4x8_to_float(rt_u32.read(coord).x);
else
main.current_color = rt_rov.read(coord);
}
else
{
#if FBFETCH_SUPPORT
main.current_color = HAS_FBFETCH ? rt_fbf : rt.read(coord);
#else
main.current_color = rt.read(coord);
#endif
}
}
else
{
main.current_color = 0;
}
MainResult out = main.ps_main();
if (PS_ROV_DEPTH == ROV_DEPTH::READ_WRITE && !main.depth_discarded)
ds_rov.write(out.depth, coord);
if (PS_ROV_COLOR && !main.color_discarded)
{
if (!PS_FBMASK)
out.c0 = select(out.c0, main.current_color, cb.fbmask == 0xff);
if (ROV_NEEDS_R32)
rt_u32.write(pack_float_to_unorm4x8(out.c0), coord);
else
rt_rov.write(out.c0, coord);
}
return out;
}
// Metal doesn't let you toggle eft with function constants so we need a separate function for it
[[early_fragment_tests]]
fragment void ps_main_rov_eft(
MainPSIn in [[stage_in]],
constant GSMTLMainPSUniform& cb [[buffer(GSMTLBufferIndexHWUniforms)]],
sampler s [[sampler(0)]],
texture2d<float> tex [[texture(GSMTLTextureIndexTex), function_constant(PS_TEX_IS_COLOR)]],
depth2d<float> depth [[texture(GSMTLTextureIndexTex), function_constant(PS_TEX_IS_DEPTH)]],
texture2d<float> palette [[texture(GSMTLTextureIndexPalette), function_constant(PS_HAS_PALETTE)]],
texture2d<float, access::read_write> rt_rov [[texture(GSMTLTextureIndexRenderTarget), raster_order_group(0), function_constant(NEEDS_RT_ROV)]],
texture2d<uint, access::read_write> rt_u32 [[texture(GSMTLTextureIndexRenderTarget), raster_order_group(0), function_constant(NEEDS_RT_U32)]])
{
PSMain main(in, cb);
main.tex_sampler = s;
if (PS_TEX_IS_COLOR)
main.tex = tex;
else
main.tex_depth = depth;
if (PS_HAS_PALETTE)
main.palette = palette;
uint2 coord = uint2(in.p.xy);
if (ROV_NEEDS_R32)
main.current_color = unpack_unorm4x8_to_float(rt_u32.read(coord).x);
else
main.current_color = rt_rov.read(coord);
MainPSOut out = main.ps_main();
if (!main.color_discarded)
{
if (!PS_FBMASK)
out.c0 = select(out.c0, main.current_color, cb.fbmask == 0xff);
if (ROV_NEEDS_R32)
rt_u32.write(pack_float_to_unorm4x8(out.c0), coord);
else
rt_rov.write(out.c0, coord);
}
}
#if PRIMID_SUPPORT
fragment uint primid_test(uint id [[primitive_id]])
{
return id;
}
#endif
// MARK: Markers for detecting the Metal version a metallib was compiled against
#if __METAL_VERSION__ >= 210
kernel void metal_version_21() {}
#endif
#if __METAL_VERSION__ >= 220
kernel void metal_version_22() {}
#endif
#if __METAL_VERSION__ >= 230
kernel void metal_version_23() {}
#endif