mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
iOS: clamp a shader's prescale so upscaling cannot blacken the frame
crt-aperture and sharp-bilinear each derive a whole-number prescale from output height over source height and then divide by it. RetroArch only ever feeds them a small console framebuffer being scaled up, so that ratio never falls below one. PCSX2 renders internally at up to 8x: past roughly 1.5x on a phone the source is taller than the screen, the ratio drops under one, floor() returns zero and the divide yields NaN. The whole frame goes black. Reported on an iPhone SE 2 with a 1334x750 window, where 1.5x rendered and 2x did not. Two hypotheses were wrong first -- push-constant placement, then parameter placement -- and both were refuted by tester data before the reporter supplied the actual trigger, which was the internal resolution and not the preset. Reproduced in the simulator at 3x and fixed there. The clamp is what the sibling sharp-bilinear-simple already carries as max(floor(...), vec2(1.0)) and what crt-geom carries as clamp(floor(...), 1.0, 2.0). Nine of the eleven bundled presets never divide by a derived scale and were unaffected. These files are otherwise byte-verbatim copies of a pinned upstream commit, so the divergence is a reversible patch beside the librashader one and a note in ATTRIBUTION.md. The test fails if either guard is dropped, which is what a re-sync from upstream would otherwise do silently.
This commit is contained in:
@@ -8,8 +8,17 @@ licence, and where it came from.
|
||||
**Pinned commit:** `80372284ea8c00ae5e25e5a6e4f9f49415f85896` (2026-08-15)
|
||||
|
||||
Every file below is copied byte for byte from that commit, licence headers intact. Nothing
|
||||
was renamed, reformatted or edited. To verify any file, clone the upstream repository at the
|
||||
pinned commit and compare it against the copy in this bundle at the upstream path given.
|
||||
was renamed or reformatted. To verify any file, clone the upstream repository at the pinned
|
||||
commit and compare it against the copy in this bundle at the upstream path given.
|
||||
|
||||
**Two files carry a one line change**, recorded in
|
||||
`platforms/ios/patches/slang-shaders-prescale-zero-guard.patch` and reversible with
|
||||
`git apply -R`. `crt/shaders/crt-aperture.slang` and
|
||||
`pixel-art-scaling/shaders/sharp-bilinear.slang` each clamp a derived integer prescale to a
|
||||
minimum of one. Upstream divides by that prescale without checking it, which is safe in
|
||||
RetroArch but not here: PCSX2 renders internally at up to 8x, and once the source is taller
|
||||
than the screen the prescale floors to zero and the frame turns black. Both files remain
|
||||
under their original licences and authorship.
|
||||
|
||||
The upstream collection is mixed licence and has no repository-wide licence file, so each
|
||||
file here was cleared on its own header rather than on a blanket grant. Files whose headers
|
||||
|
||||
@@ -142,7 +142,7 @@ layout(set = 0, binding = 2) uniform sampler2D Source;
|
||||
|
||||
void main()
|
||||
{
|
||||
float scale = floor(params.OutputSize.y * params.SourceSize.w);
|
||||
float scale = max(floor(params.OutputSize.y * params.SourceSize.w), 1.0);
|
||||
float offset = 1.0 / scale * 0.5;
|
||||
|
||||
if (bool(mod(scale, 2.0))) offset = 0.0;
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ void main()
|
||||
vec2 texel = vTexCoord * params.SourceSize.xy;
|
||||
vec2 texel_floored = floor(texel);
|
||||
vec2 s = fract(texel);
|
||||
float scale = (params.AUTO_PRESCALE > 0.5) ? floor(params.OutputSize.y / params.SourceSize.y + 0.01) : params.SHARP_BILINEAR_PRE_SCALE;
|
||||
float scale = max((params.AUTO_PRESCALE > 0.5) ? floor(params.OutputSize.y / params.SourceSize.y + 0.01) : params.SHARP_BILINEAR_PRE_SCALE, 1.0);
|
||||
float region_range = 0.5 - 0.5 / scale;
|
||||
|
||||
// Figure out where in the texel to sample to get correct pre-scaled bilinear.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
Guard the integer prescale in crt-aperture and sharp-bilinear against zero.
|
||||
|
||||
Both shaders derive a whole-number prescale from output height over source height,
|
||||
then divide by it. RetroArch only ever feeds them a small console framebuffer being
|
||||
scaled up, so the ratio is never below one. PCSX2 renders internally at up to 8x, and
|
||||
above roughly 1.5x on a phone the source is taller than the screen: the ratio drops
|
||||
under one, floor() returns zero, and the divide yields NaN. The whole frame goes black.
|
||||
|
||||
Reported on an iPhone SE 2 (1334x750) where 1.5x worked and 2x did not, then reproduced
|
||||
in the simulator at 3x. Fixed by clamping the prescale to one, which is what the sibling
|
||||
sharp-bilinear-simple already does with max(floor(...), vec2(1.0)) and what crt-geom does
|
||||
with clamp(floor(...), 1.0, 2.0).
|
||||
|
||||
This diverges from the pinned upstream commit, so it is recorded here and in
|
||||
shaders/ATTRIBUTION.md. Reverse it to recover the upstream bytes:
|
||||
|
||||
git apply -R platforms/ios/patches/slang-shaders-prescale-zero-guard.patch
|
||||
|
||||
test_ios_shader_prescale_guard.py fails if either guard is dropped, which is what a
|
||||
re-sync from upstream would otherwise do silently.
|
||||
|
||||
diff --git a/platforms/ios/app/src/main/assets/shaders/presets/crt/shaders/crt-aperture.slang b/platforms/ios/app/src/main/assets/shaders/presets/crt/shaders/crt-aperture.slang
|
||||
index 8ca5192e40..508c8fa2eb 100644
|
||||
--- a/platforms/ios/app/src/main/assets/shaders/presets/crt/shaders/crt-aperture.slang
|
||||
+++ b/platforms/ios/app/src/main/assets/shaders/presets/crt/shaders/crt-aperture.slang
|
||||
@@ -142,7 +142,7 @@ layout(set = 0, binding = 2) uniform sampler2D Source;
|
||||
|
||||
void main()
|
||||
{
|
||||
- float scale = floor(params.OutputSize.y * params.SourceSize.w);
|
||||
+ float scale = max(floor(params.OutputSize.y * params.SourceSize.w), 1.0);
|
||||
float offset = 1.0 / scale * 0.5;
|
||||
|
||||
if (bool(mod(scale, 2.0))) offset = 0.0;
|
||||
diff --git a/platforms/ios/app/src/main/assets/shaders/presets/pixel-art-scaling/shaders/sharp-bilinear.slang b/platforms/ios/app/src/main/assets/shaders/presets/pixel-art-scaling/shaders/sharp-bilinear.slang
|
||||
index 90508aa907..90865cc715 100644
|
||||
--- a/platforms/ios/app/src/main/assets/shaders/presets/pixel-art-scaling/shaders/sharp-bilinear.slang
|
||||
+++ b/platforms/ios/app/src/main/assets/shaders/presets/pixel-art-scaling/shaders/sharp-bilinear.slang
|
||||
@@ -47,7 +47,7 @@ void main()
|
||||
vec2 texel = vTexCoord * params.SourceSize.xy;
|
||||
vec2 texel_floored = floor(texel);
|
||||
vec2 s = fract(texel);
|
||||
- float scale = (params.AUTO_PRESCALE > 0.5) ? floor(params.OutputSize.y / params.SourceSize.y + 0.01) : params.SHARP_BILINEAR_PRE_SCALE;
|
||||
+ float scale = max((params.AUTO_PRESCALE > 0.5) ? floor(params.OutputSize.y / params.SourceSize.y + 0.01) : params.SHARP_BILINEAR_PRE_SCALE, 1.0);
|
||||
float region_range = 0.5 - 0.5 / scale;
|
||||
|
||||
// Figure out where in the texel to sample to get correct pre-scaled bilinear.
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A bundled shader must not divide by an unguarded integer prescale.
|
||||
|
||||
Several slang shaders derive a whole-number prescale from output height over source
|
||||
height and then divide by it. That is safe in RetroArch, where the source is always a
|
||||
small console framebuffer being scaled up. PCSX2 renders internally at up to 8x, so
|
||||
above roughly 1.5x the source is taller than the screen, the ratio falls under one,
|
||||
floor() returns zero and the frame turns black. Reported on an iPhone SE 2 at 2x,
|
||||
reproduced in the simulator at 3x.
|
||||
|
||||
The guard is a max(..., 1.0) around the prescale, which sharp-bilinear-simple and
|
||||
crt-geom already carry upstream. This test exists because re-syncing a preset from
|
||||
upstream would silently drop it again.
|
||||
"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PRESETS = ROOT / "platforms/ios/app/src/main/assets/shaders/presets"
|
||||
|
||||
# A prescale is a floor() over the output/source height ratio, in either the divide or
|
||||
# the reciprocal-multiply spelling. SourceSize.w and .zw are 1/height and 1/size.
|
||||
PRESCALE = re.compile(
|
||||
r"floor\s*\([^;]*?OutputSize\.[xy]{1,2}[^;]*?"
|
||||
r"(?:/\s*[a-zA-Z_.]*SourceSize\.[xy]{1,2}|\*\s*[a-zA-Z_.]*SourceSize\.[zw]{1,2})",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def shaders():
|
||||
return sorted(PRESETS.rglob("*.slang"))
|
||||
|
||||
|
||||
class PrescaleGuard(unittest.TestCase):
|
||||
def test_presets_exist(self):
|
||||
self.assertTrue(shaders(), f"no .slang files under {PRESETS}")
|
||||
|
||||
def test_every_prescale_is_clamped(self):
|
||||
offenders = []
|
||||
for path in shaders():
|
||||
for number, line in enumerate(path.read_text().splitlines(), 1):
|
||||
code = line.split("//")[0]
|
||||
if not PRESCALE.search(code):
|
||||
continue
|
||||
# clamp() carries its own lower bound; max() is the direct guard.
|
||||
if "max(" in code or "clamp(" in code:
|
||||
continue
|
||||
offenders.append(f" {path.relative_to(PRESETS)}:{number}: {code.strip()}")
|
||||
self.assertEqual(
|
||||
offenders,
|
||||
[],
|
||||
"a derived integer prescale is not clamped to at least 1. Above ~1.5x internal "
|
||||
"resolution it floors to zero and the frame goes black. Wrap it in max(..., 1.0) "
|
||||
"and record the change in patches/slang-shaders-prescale-zero-guard.patch:\n"
|
||||
+ "\n".join(offenders),
|
||||
)
|
||||
|
||||
def test_the_two_known_shaders_still_carry_their_guard(self):
|
||||
"""Named directly, so a rename or a re-sync cannot quietly drop the fix."""
|
||||
for relative in (
|
||||
"crt/shaders/crt-aperture.slang",
|
||||
"pixel-art-scaling/shaders/sharp-bilinear.slang",
|
||||
):
|
||||
path = PRESETS / relative
|
||||
self.assertTrue(path.is_file(), f"missing {relative}")
|
||||
self.assertIn(
|
||||
"max(",
|
||||
path.read_text(),
|
||||
f"{relative} lost its prescale guard; see "
|
||||
"patches/slang-shaders-prescale-zero-guard.patch",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user