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
1 change: 1 addition & 0 deletions docs/debug-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ WWHD_STATE_DIR=sdmc:/switch/wwhd/states-eu
| `poke ADDR WORD` | write one big-endian word to MEM2; logs old and new value |
| `raw bench status\|save NAME\|load NAME\|clock [freeze [T]\|run]\|rec N` | the performance test's console side (portable states in `bench/`, the clock held, per-frame recording): use `tools/switch/perf_test.py` ([perf-test.md](perf-test.md)) |
| `raw aniso [1\|2\|4\|8\|16]`, `raw shadowscale [0.5\|1\|2]`, `raw noao [0\|1]`, `raw miiverse [0\|1]` | live and not saved (the Picture tab's aniso level, shadow maps and ambient occlusion, which the menu saves; and the Dev tab's Miiverse switch): anisotropic filtering level, the shadow maps' scale, the ambient-occlusion pass skipped, the Miiverse thread (`0` parks it); with `perf_test.py toggle` |
| `raw fsr [0\|1]` | live and not saved (the Picture tab's FSR 1 upscale and sharpening, which the menu saves); no argument reports the state; with `perf_test.py toggle` |
| `wait`, `quit`, `reload`, `ping`, `help`, `raw CMD ...` | `raw logtext` prints the kept log text |

Button names: `A B X Y L R ZL ZR PLUS MINUS UP DOWN LEFT RIGHT LS RS`. Presses go in where the controller is
Expand Down
23 changes: 19 additions & 4 deletions runtime/src/gfx/deko/backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,13 @@ gfxsw::PictureGrade picture_grade() {
// the picture adjustments and, for an sRGB TV format, sRGB encoding (as gfx/gl present)
struct PresentUbo { // present_fsh.glsl, std140
float grade[4];
int32_t encode[4]; // x: encode as sRGB (sRGB TV format); y: the image is sRGB (sampling decoded it)
int32_t encode[4]; // x: encode as sRGB (sRGB TV format); y: the image is sRGB (sampling decoded it); z: FSR 1 (1 EASU, 2 RCAS)
};
// where the picture went in the window (GamePad touch, gfxsw::gamepad_touch): x, y, w, h; w 0 while the
// GamePad picture is not the one shown
std::atomic<int> g_padRect[4] = {0, 0, 0, 0};
std::atomic<bool> g_gamepadView{true}; // the GamePad picture (the controller acts as the GamePad by default)
std::atomic<bool> g_fsr{false}; // FSR 1's EASU for a picture smaller than the window (gfxsw::fsr)
std::atomic<bool> g_gamepadPip{false}; // the GamePad picture in a corner of the TV's (gfxsw::gamepad_pip)
// `corner`: the picture in the window's bottom right corner, a third of its width (the GamePad picture-in-picture)
bool draw_picture(const PresentSource& src, bool capture, bool corner = false) {
Expand All @@ -528,16 +529,26 @@ bool draw_picture(const PresentSource& src, bool capture, bool corner = false) {
if (!u) return false;
const gfxsw::PictureGrade g = picture_grade();
const bool encode = R.tvSrgb.load(std::memory_order_relaxed);
PresentUbo p{{g.exposure, g.contrast, g.saturation, g.gamma}, {encode ? 1 : 0, src.srgb ? 1 : 0, 0, 0}};
// a picture of the window's size is copied pixel for pixel; others are filtered (or, with FSR, upscaled by EASU
// when the picture is smaller than its place in the window)
const bool exact = src.pw == uint32_t(w) && src.ph == uint32_t(h);
const int fsr = g_fsr.load(std::memory_order_relaxed)
? (src.pw < uint32_t(w) && src.ph < uint32_t(h) ? 1 : exact ? 2 : 0) // EASU, or RCAS alone at the window's size
: 0;
static int fsrLogged = -1;
if (!corner && fsr != fsrLogged) {
fsrLogged = fsr;
LOG("[dk] present: FSR 1 %s (%s picture %ux%u in %dx%d)", fsr == 1 ? "EASU upscale" : fsr == 2 ? "RCAS sharpening" : "not applied",
pad ? "GamePad" : "TV", src.pw, src.ph, w, h);
}
PresentUbo p{{g.exposure, g.contrast, g.saturation, g.gamma}, {encode ? 1 : 0, src.srgb ? 1 : 0, fsr, 0}};
memcpy(u.cpu, &p, sizeof p);
static int encoded = -1;
if (!pad && int(encode) * 2 + int(src.srgb) != encoded) {
encoded = int(encode) * 2 + int(src.srgb);
LOG("[dk] presenting the game's picture (%ux%u, GX2 format %03X%s) with %s", src.width, src.height,
src.surface->format, src.srgb ? ", sRGB" : "", encode ? "sRGB encoding (sRGB TV format)" : "no encoding");
}
// a picture of the window's size is copied pixel for pixel; others are filtered
const bool exact = src.pw == uint32_t(w) && src.ph == uint32_t(h);
const bool integer = src.surface->fmt.kind != FormatInfo::FLOAT;
const DkResHandle tex =
dkMakeTextureHandle(src.imageId, exact || integer ? kPresentNearestSamplerId : kPresentLinearSamplerId);
Expand Down Expand Up @@ -1566,6 +1577,10 @@ bool gamepad_view() { return gfxdk::g_gamepadView.load(std::memory_order_relaxed
void set_gamepad_view(bool on) {
if (gfxdk::g_gamepadView.exchange(on) != on) LOG("[dk] window: %s picture asked for", on ? "GamePad" : "TV");
}
bool fsr() { return gfxdk::g_fsr.load(std::memory_order_relaxed); }
void set_fsr(bool on) {
if (gfxdk::g_fsr.exchange(on) != on) LOG("[dk] present: FSR 1 upscale %s", on ? "on" : "off");
}
bool gamepad_pip() { return gfxdk::g_gamepadPip.load(std::memory_order_relaxed); }
void set_gamepad_pip(bool on) {
if (gfxdk::g_gamepadPip.exchange(on) != on) LOG("[dk] window: GamePad picture in the corner %s", on ? "on" : "off");
Expand Down
129 changes: 123 additions & 6 deletions runtime/src/gfx/deko/shaders/present_fsh.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,132 @@ layout(location = 0) out vec4 color;
layout(binding = 0) uniform sampler2D scan;
layout(std140, binding = 0) uniform Present {
vec4 grade; // exposure, contrast, saturation, gamma
ivec4 encode; // x: encode as sRGB; y: the picture's image has an sRGB format (sampling decoded it)
ivec4 encode; // x: encode as sRGB; y: the picture's image has an sRGB format (sampling decoded it);
// z: FSR 1: 1 EASU upscale (the picture is smaller than the window), 2 RCAS sharpening (same size)
};
vec3 srgb_encode(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), c)); }
vec4 srgb_encode(vec4 c) { return mix(c * 12.92, 1.055 * pow(c, vec4(1.0 / 2.4)) - 0.055, step(vec4(0.0031308), c)); }

// ---- FSR 1 EASU (AMD FidelityFX Super Resolution 1.0, MIT; ffx_fsr1.h FsrEasuF, GLSL port): an edge-adaptive
// upscale from 12 taps. It expects perceptual values: when the picture is still linear (encode.x) the taps are
// taken to gamma 2.0 (sqrt) and back, which is cheaper than the sRGB curve on 12 texels.
// (no clamps: the picture is UNORM, so the taps are in 0..1; the result is clamped at the end)
vec4 easu_in(vec4 v) {
if (encode.y != 0) v = srgb_encode(v);
return encode.x != 0 ? sqrt(v) : v;
}
vec3 rcas_in(vec3 v) {
if (encode.y != 0) v = srgb_encode(v);
return encode.x != 0 ? sqrt(v) : v;
}
void easu_tap(inout vec3 aC, inout float aW, vec2 off, vec2 dir, vec2 len, float lob, float clp, vec3 c) {
vec2 v = vec2(off.x * dir.x + off.y * dir.y, off.x * -dir.y + off.y * dir.x) * len;
float d2 = min(dot(v, v), clp);
float wB = 2.0 / 5.0 * d2 - 1.0, wA = lob * d2 - 1.0;
wB *= wB;
wA *= wA;
wB = 25.0 / 16.0 * wB - (25.0 / 16.0 - 1.0);
float w = wB * wA;
aC += c * w;
aW += w;
}
void easu_set(inout vec2 dir, inout float len, float w, float lA, float lB, float lC, float lD, float lE) {
float lenX = max(abs(lD - lC), abs(lC - lB)), dirX = lD - lB;
dir.x += dirX * w;
lenX = clamp(abs(dirX) / lenX, 0.0, 1.0);
len += lenX * lenX * w;
float lenY = max(abs(lE - lC), abs(lC - lA)), dirY = lE - lA;
dir.y += dirY * w;
lenY = clamp(abs(dirY) / lenY, 0.0, 1.0);
len += lenY * lenY * w;
}
vec3 easu() {
vec2 size = vec2(textureSize(scan, 0)), inv = 1.0 / size;
vec2 pp = uv * size - 0.5; // the output pixel's centre in the picture's texel coordinates
vec2 fp = floor(pp);
pp -= fp;
vec2 p0 = fp * inv + vec2(inv.x, -inv.y);
vec2 p1 = p0 + vec2(-inv.x, 2.0 * inv.y), p2 = p0 + vec2(inv.x, 2.0 * inv.y), p3 = p0 + vec2(0.0, 4.0 * inv.y);
// b c gathers: (x y z w) = (upper left, upper right, lower right, lower left)
// e f g h
// i j k l
// n o
vec4 bcR = easu_in(textureGather(scan, p0, 0)), bcG = easu_in(textureGather(scan, p0, 1)), bcB = easu_in(textureGather(scan, p0, 2));
vec4 ijR = easu_in(textureGather(scan, p1, 0)), ijG = easu_in(textureGather(scan, p1, 1)), ijB = easu_in(textureGather(scan, p1, 2));
vec4 klR = easu_in(textureGather(scan, p2, 0)), klG = easu_in(textureGather(scan, p2, 1)), klB = easu_in(textureGather(scan, p2, 2));
vec4 noR = easu_in(textureGather(scan, p3, 0)), noG = easu_in(textureGather(scan, p3, 1)), noB = easu_in(textureGather(scan, p3, 2));
vec4 bcL = bcB * 0.5 + (bcR * 0.5 + bcG), ijL = ijB * 0.5 + (ijR * 0.5 + ijG);
vec4 klL = klB * 0.5 + (klR * 0.5 + klG), noL = noB * 0.5 + (noR * 0.5 + noG);
float bL = bcL.x, cL = bcL.y, iL = ijL.x, jL = ijL.y, fL = ijL.z, eL = ijL.w;
float kL = klL.x, lL = klL.y, hL = klL.z, gL = klL.w, oL = noL.z, nL = noL.w;
vec2 dir = vec2(0.0);
float len = 0.0;
easu_set(dir, len, (1.0 - pp.x) * (1.0 - pp.y), bL, eL, fL, gL, jL);
easu_set(dir, len, pp.x * (1.0 - pp.y), cL, fL, gL, hL, kL);
easu_set(dir, len, (1.0 - pp.x) * pp.y, fL, iL, jL, kL, nL);
easu_set(dir, len, pp.x * pp.y, gL, jL, kL, lL, oL);
float dirR = dot(dir, dir);
bool zro = dirR < 1.0 / 32768.0;
dirR = zro ? 1.0 : inversesqrt(dirR);
dir.x = zro ? 1.0 : dir.x;
dir *= dirR;
len = len * 0.5;
len *= len;
float stretch = dot(dir, dir) / max(abs(dir.x), abs(dir.y));
vec2 len2 = vec2(1.0 + (stretch - 1.0) * len, 1.0 - 0.5 * len);
float lob = 0.5 + ((1.0 / 4.0 - 0.04) - 0.5) * len, clp = 1.0 / lob;
vec3 fC = vec3(ijR.z, ijG.z, ijB.z), gC = vec3(klR.w, klG.w, klB.w), jC = vec3(ijR.y, ijG.y, ijB.y), kC = vec3(klR.x, klG.x, klB.x);
vec3 min4 = min(min(fC, gC), min(jC, kC)), max4 = max(max(fC, gC), max(jC, kC));
vec3 aC = vec3(0.0);
float aW = 0.0;
easu_tap(aC, aW, vec2(0.0, -1.0) - pp, dir, len2, lob, clp, vec3(bcR.x, bcG.x, bcB.x));
easu_tap(aC, aW, vec2(1.0, -1.0) - pp, dir, len2, lob, clp, vec3(bcR.y, bcG.y, bcB.y));
easu_tap(aC, aW, vec2(-1.0, 1.0) - pp, dir, len2, lob, clp, vec3(ijR.x, ijG.x, ijB.x));
easu_tap(aC, aW, vec2(0.0, 1.0) - pp, dir, len2, lob, clp, jC);
easu_tap(aC, aW, vec2(0.0, 0.0) - pp, dir, len2, lob, clp, fC);
easu_tap(aC, aW, vec2(-1.0, 0.0) - pp, dir, len2, lob, clp, vec3(ijR.w, ijG.w, ijB.w));
easu_tap(aC, aW, vec2(1.0, 1.0) - pp, dir, len2, lob, clp, kC);
easu_tap(aC, aW, vec2(2.0, 1.0) - pp, dir, len2, lob, clp, vec3(klR.y, klG.y, klB.y));
easu_tap(aC, aW, vec2(2.0, 0.0) - pp, dir, len2, lob, clp, vec3(klR.z, klG.z, klB.z));
easu_tap(aC, aW, vec2(1.0, 0.0) - pp, dir, len2, lob, clp, gC);
easu_tap(aC, aW, vec2(1.0, 2.0) - pp, dir, len2, lob, clp, vec3(noR.z, noG.z, noB.z));
easu_tap(aC, aW, vec2(0.0, 2.0) - pp, dir, len2, lob, clp, vec3(noR.w, noG.w, noB.w));
return clamp(min(max4, max(min4, aC / aW)), 0.0, 1.0);
}

// ---- FSR 1 RCAS (ffx_fsr1.h FsrRcasF, GLSL port): contrast-adaptive sharpening from the pixel and its four
// neighbours, on a picture of the window's size (encode.z == 2). 0.25 stops of sharpness (AMD's default is 0.2).
vec3 rcas() {
// the sampler is nearest and clamps to the edge, and the picture is the window's size: the output pixel's
// centre is the texel's centre, and a fixed offset reaches a neighbour (the edge repeats itself)
vec3 b = rcas_in(textureOffset(scan, uv, ivec2(0, -1)).rgb);
vec3 d = rcas_in(textureOffset(scan, uv, ivec2(-1, 0)).rgb);
vec3 e = rcas_in(texture(scan, uv).rgb);
vec3 f = rcas_in(textureOffset(scan, uv, ivec2(1, 0)).rgb);
vec3 h = rcas_in(textureOffset(scan, uv, ivec2(0, 1)).rgb);
vec3 mn4 = min(min(min(b, d), f), h), mx4 = max(max(max(b, d), f), h);
// (a tiny epsilon keeps black and white neighbourhoods from dividing by zero)
vec3 hitMin = min(mn4, e) / (4.0 * mx4 + 1.0e-5);
vec3 hitMax = (1.0 - max(mx4, e)) / (4.0 * mn4 - 4.0 - 1.0e-5);
vec3 lobeC = max(-hitMin, hitMax);
float lobe = max(-(0.25 - 1.0 / 16.0), min(max(max(lobeC.r, lobeC.g), lobeC.b), 0.0)) * exp2(-0.25);
return clamp((lobe * (b + d + h + f) + e) / (4.0 * lobe + 1.0), 0.0, 1.0);
}

void main() {
vec3 c = texture(scan, uv).rgb;
// gfx/gl samples the picture's bytes as stored (GL_SKIP_DECODE_EXT): an sRGB image's decoding is undone
if (encode.y != 0) c = srgb_encode(clamp(c, 0.0, 1.0));
c = clamp(c * grade.x, 0.0, 1.0);
if (encode.x != 0) c = srgb_encode(c);
vec3 c;
if (encode.z != 0) {
// EASU returns the picture as it would be after the encoding steps below (perceptual values)
c = encode.z == 2 ? rcas() : easu();
if (encode.x != 0) c = srgb_encode(clamp(c * c * grade.x, 0.0, 1.0)); // (exposure scales the linear picture)
else c = clamp(c * grade.x, 0.0, 1.0);
} else {
c = texture(scan, uv).rgb;
// gfx/gl samples the picture's bytes as stored (GL_SKIP_DECODE_EXT): an sRGB image's decoding is undone
if (encode.y != 0) c = srgb_encode(clamp(c, 0.0, 1.0));
c = clamp(c * grade.x, 0.0, 1.0);
if (encode.x != 0) c = srgb_encode(c);
}
if (grade.y != 1.0) c = clamp(mix(c, c * c * (3.0 - 2.0 * c), grade.y - 1.0), 0.0, 1.0);
if (grade.z != 1.0) {
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
Expand Down
5 changes: 5 additions & 0 deletions runtime/src/gfx/switch_renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ void set_aniso_level(int level);
// edge smoothing (FXAA, issue #21) on the 3D picture before the HUD (WWHD_FXAA): any thread, from the next frame
bool fxaa();
void set_fxaa(bool on);
// FSR 1 in the present pass when the game's picture is smaller than the window (EASU: docked 720p -> 1080p, or
// dynamic resolution below 1.0) or the same size (RCAS sharpening only): any thread, from the next frame. Test
// option, off by default
bool fsr();
void set_fsr(bool on);
// the bloom and the distance blur (issue #15): the game samples its bloom chain and its half-size picture with more
// levels than the render targets have; gfx/deko mip_chain makes those levels. On by default (WWHD_BLOOM=0,
// WWHD_DISTANCE_BLUR=0 off at start); any thread
Expand Down
6 changes: 6 additions & 0 deletions runtime/src/platform/debug_switch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ debugsrv::Reply bloom(const debugsrv::Args& a) {
if (!a.empty()) gfxsw::set_bloom(a[0] != "0");
return debugsrv::ok(std::string("bloom ") + (gfxsw::bloom() ? "on" : "off"));
}
// "fsr [0|1]": FSR 1 in the present pass (gfxsw::set_fsr), not saved
debugsrv::Reply fsr(const debugsrv::Args& a) {
if (!a.empty()) gfxsw::set_fsr(a[0] != "0");
return debugsrv::ok(std::string("fsr ") + (gfxsw::fsr() ? "on" : "off"));
}
debugsrv::Reply blur(const debugsrv::Args& a) {
if (!a.empty()) gfxsw::set_distance_blur(a[0] != "0");
return debugsrv::ok(std::string("blur ") + (gfxsw::distance_blur() ? "on" : "off"));
Expand Down Expand Up @@ -423,6 +428,7 @@ void start() {
debugsrv::add_command("pacing", "pacing [0|1] frame pacing: 1 swap interval the game's, 0 always 1", pacing);
debugsrv::add_command("bloom", "bloom [0|1] the Switch tab's Bloom (not saved)", bloom);
debugsrv::add_command("blur", "blur [0|1] the Switch tab's Distance blur (not saved)", blur);
debugsrv::add_command("fsr", "fsr [0|1] FSR 1 in the present pass (not saved)", fsr);
debugsrv::add_command("stall", "stall <ms> [core] [n] [gap] spin above the game threads (audio guard tests)", stall);
debugsrv::add_command("evict", "evict [now | idle <n>] texture eviction: now, or after n unused frames", evict);
debugsrv::add_command("bgshaders", "bgshaders [start] background shaders: start / status", bgshaders);
Expand Down
8 changes: 8 additions & 0 deletions runtime/src/platform/overlay_switch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,14 @@ void tab_picture() {
}
help(tx.blurHelp);
}
{
bool v;
if (check(lbl(tx.fsr, "fsr").c_str(), gfxsw::fsr(), &v)) {
gfxsw::set_fsr(v);
hostui::post([v] { hostui::set(kKeyFsr, v ? "1" : "0"); });
}
help(tx.fsrHelp);
}
{
// the shadow maps' size (gfxsw::shadow_scale); saved as switchShadowScale ("0.5", "1" the default, or "2")
const float cur = gfxsw::shadow_scale();
Expand Down
1 change: 1 addition & 0 deletions runtime/src/platform/settings_switch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ void apply_at_start() {
if (hostui::get(kKeyShadowScale, v) && !v.empty()) gfxsw::set_shadow_scale(float(atof(v.c_str())));
if (hostui::get(kKeyAmbientOcclusion, v) && !v.empty()) gfxsw::set_skip_ao(v == "0");
if (hostui::get(kKeyFxaa, v) && !v.empty()) gfxsw::set_fxaa(v == "1");
if (hostui::get(kKeyFsr, v) && !v.empty()) gfxsw::set_fsr(v == "1");
if (hostui::get(kKeyBloom, v) && !v.empty()) gfxsw::set_bloom(v == "1");
if (hostui::get(kKeyDistanceBlur, v) && !v.empty()) gfxsw::set_distance_blur(v == "1");
if (hostui::get(kKeyShaderWait, v) && !v.empty()) shader_wait::set_mode(atoi(v.c_str()));
Expand Down
1 change: 1 addition & 0 deletions runtime/src/platform/settings_switch.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ constexpr const char* kKeyAmbientOcclusion = "switchAmbientOcclusion"; // "1" o
constexpr const char* kKeyAnisoLevel = "switchAnisoLevel"; // anisotropic filtering: 1 (off, the default), 2, 4, 8, 16
constexpr const char* kKeyAniso = "switchAniso"; // the old on/off key: read only when switchAnisoLevel is absent ("1" = 16x), never written
constexpr const char* kKeyFxaa = "switchFxaa"; // gfxsw::fxaa (issue #21), off by default
constexpr const char* kKeyFsr = "switchFsr"; // FSR 1 in the present pass (gfxsw::fsr), a developer option, off by default
constexpr const char* kKeyBloom = "switchBloom"; // gfxsw::bloom
constexpr const char* kKeyDistanceBlur = "switchDistanceBlur"; // gfxsw::distance_blur
// which Wii U controller the Switch controller is (Debug section): single (the GamePad, single screen; the default),
Expand Down
Loading
Loading