From 6b888b9590c5ec97e87f202c8ed04b0acd811118 Mon Sep 17 00:00:00 2001 From: PEQHUB Date: Sat, 19 Sep 2026 17:48:29 -0500 Subject: [PATCH] Feat: Add RGSS and SGSS supersampling resolve filters (+ optional RCAS sharpening) Adds a downscale-filter option for scales above 100%: Rotated Grid (4 bilinear taps on the arctan(1/2) grid, best around 2x) and Sparse Grid (8 taps as two half-scale rotated-grid quads, used at 3x and above). Both resolve the already-rendered ordered-grid image; no extra geometry coverage samples. Below 100% the behaviour is unchanged. An optional RCAS sharpening pass (off by default, 35% strength) offsets the resolve softening: it runs at AMD's FSR1 reference tuning over the shared output-sized intermediate and blends towards the unsharpened pixel, so strength is a consistent 0-100% control with a true bypass at 0%. The FSR branch is never sharpened. Strength presets live on a 5% grid; each preset x noise mode gets a lazily built pipeline, since no supported version exposes a float uniform setter on a render pass. Controls sit on the main config page in both the Cloth and Sodium UIs, guarded to 1.21.11+ like the fsr option; older versions are untouched (scale slider ranges revert to 200% there). The dev-only gametest classes are excluded from the shipped jars. All four stonecutter targets build: 1.21.11-fabric, 26.2-fabric, 26.2-neoforge, 26.3-fabric. --- build.fabric.gradle.kts | 8 + build.neoforge.gradle.kts | 8 + .../dev/zelo/renderscale/RenderScale.java | 217 +++++++++++------- .../sodium/RenderScaleSodiumConfig.java | 57 ++++- .../renderscale/config/RenderScaleConfig.java | 66 ++++++ .../assets/renderscale/lang/en_us.json | 12 + .../assets/renderscale/shaders/core/rcas.fsh | 25 +- .../assets/renderscale/shaders/core/rgss.fsh | 33 +++ .../assets/renderscale/shaders/core/sgss.fsh | 43 ++++ 9 files changed, 376 insertions(+), 93 deletions(-) create mode 100644 src/main/resources/assets/renderscale/shaders/core/rgss.fsh create mode 100644 src/main/resources/assets/renderscale/shaders/core/sgss.fsh diff --git a/build.fabric.gradle.kts b/build.fabric.gradle.kts index 4bc1774..5ac332e 100644 --- a/build.fabric.gradle.kts +++ b/build.fabric.gradle.kts @@ -165,3 +165,11 @@ dependencies { modCompileOnly("maven.modrinth:iris:${property("deps.iris")}-fabric") } + +// Game-test classes are dev-only: the fabric-client-gametest entrypoint is +// read by the gametest API in dev runs (which use the source set directly) +// and ignored by the loader in production. Keep the dead classes out of +// the shipped jar. +tasks.jar { + exclude("dev/zelo/renderscale/gametest/**") +} diff --git a/build.neoforge.gradle.kts b/build.neoforge.gradle.kts index 85075b6..5a38336 100644 --- a/build.neoforge.gradle.kts +++ b/build.neoforge.gradle.kts @@ -139,3 +139,11 @@ dependencies { tasks.named("createMinecraftArtifacts") { dependsOn(tasks.named("stonecutterGenerate")) } + +// Game-test classes are dev-only: the fabric-client-gametest entrypoint is +// read by the gametest API in dev runs (which use the source set directly) +// and ignored by the loader in production. Keep the dead classes out of +// the shipped jar. +tasks.jar { + exclude("dev/zelo/renderscale/gametest/**") +} diff --git a/src/main/java/dev/zelo/renderscale/RenderScale.java b/src/main/java/dev/zelo/renderscale/RenderScale.java index 8b757d6..b695a90 100644 --- a/src/main/java/dev/zelo/renderscale/RenderScale.java +++ b/src/main/java/dev/zelo/renderscale/RenderScale.java @@ -93,59 +93,84 @@ public class RenderScale { public RenderTarget clientRenderTarget; @Nullable - private RenderTarget fsrIntermediateTarget; + private RenderTarget intermediateTarget; //? >= 1.21.11 { - public static RenderPipeline FSR_EASU_PIPELINE = - //? >=26.3 { - RenderPipeline.builder().withBindGroupLayout(BindGroupLayouts.GLOBALS) - .withColorTargetState(ColorTargetState.DEFAULT) - .withShaderDefine("RENDERSCALE_EXPLICIT_GATHER") - //?} else - //RenderPipeline.builder(GLOBALS_SNIPPET) - .withLocation(Identifier.fromNamespaceAndPath("renderscale", "pipeline/fsr_easu")) - .withVertexShader("core/screenquad") - .withFragmentShader(Identifier.fromNamespaceAndPath("renderscale", "core/easu")) - //? <= 26.1 { - /*//? 1.21.11 { - /^.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - - ^///?} - .withSampler("InSampler") - .withVertexFormat(DefaultVertexFormat.EMPTY, VertexFormat.Mode.TRIANGLES) - *///?} else { - - .withBindGroupLayout(BindGroupLayouts.IN_SAMPLER) - .withPrimitiveTopology(PrimitiveTopology.TRIANGLES) - //?} - - .build(); + public static RenderPipeline FSR_EASU_PIPELINE = buildFsrEasuPipeline(); + + private static RenderPipeline buildFsrEasuPipeline() { + RenderPipeline.Builder builder = fullscreenBuilder("pipeline/fsr_easu", "core/easu"); + // The explicit-gather workaround only applies to the RenderPearl + // pipeline API; older families keep the native textureGather path. + //? >=26.3 { + builder = builder.withShaderDefine("RENDERSCALE_EXPLICIT_GATHER"); + //?} + return builder.build(); + } public static RenderPipeline FSR_RCAS_PIPELINE = - //? >=26.3 { - RenderPipeline.builder().withBindGroupLayout(BindGroupLayouts.GLOBALS) - .withColorTargetState(ColorTargetState.DEFAULT) - //?} else - //RenderPipeline.builder(GLOBALS_SNIPPET) - .withLocation(Identifier.fromNamespaceAndPath("renderscale", "pipeline/fsr_rcas")) - .withVertexShader("core/screenquad") - .withFragmentShader(Identifier.fromNamespaceAndPath("renderscale", "core/rcas")) - //? <= 26.1 { - /*//? 1.21.11 { - /^.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) - .withDepthWrite(false) - - ^///?} - .withSampler("InSampler") - .withVertexFormat(DefaultVertexFormat.EMPTY, VertexFormat.Mode.TRIANGLES) - *///?} else { - - - .withBindGroupLayout(BindGroupLayouts.IN_SAMPLER) - .withPrimitiveTopology(PrimitiveTopology.TRIANGLES) - //?} - .build(); + fullscreenPipeline("pipeline/fsr_rcas", "core/rcas"); + + public static RenderPipeline RGSS_PIPELINE = + fullscreenPipeline("pipeline/rgss", "core/rgss"); + + public static RenderPipeline SGSS_PIPELINE = + fullscreenPipeline("pipeline/sgss", "core/sgss"); + + // The fullscreen pass chain differs per version family (pipeline lookup, + // bind group layout, vertex format vs topology), so every screen-sized + // blit goes through this one builder. Callers add their own defines with + // the typed withShaderDefine overloads. + private static RenderPipeline.Builder fullscreenBuilder(String location, String fragment) { + //? >=26.3 { + return RenderPipeline.builder().withBindGroupLayout(BindGroupLayouts.GLOBALS) + .withColorTargetState(ColorTargetState.DEFAULT) + //?} else + //return RenderPipeline.builder(GLOBALS_SNIPPET) + .withLocation(Identifier.fromNamespaceAndPath("renderscale", location)) + .withVertexShader("core/screenquad") + .withFragmentShader(Identifier.fromNamespaceAndPath("renderscale", fragment)) + //? <= 26.1 { + /*//? 1.21.11 { + /^.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withDepthWrite(false) + + ^///?} + .withSampler("InSampler") + .withVertexFormat(DefaultVertexFormat.EMPTY, VertexFormat.Mode.TRIANGLES) + *///?} else { + + .withBindGroupLayout(BindGroupLayouts.IN_SAMPLER) + .withPrimitiveTopology(PrimitiveTopology.TRIANGLES) + //?} + ; + } + + private static RenderPipeline fullscreenPipeline(String location, String fragment) { + return fullscreenBuilder(location, fragment).build(); + } + + // Sharpening pipelines: no supported version family exposes a float + // uniform setter on a render pass, so the blend factor is a shader + // define and each (strength preset, noise mode) pair gets its own + // pipeline, built lazily on the render thread. The strength control + // stores the same 5% presets, so this array is the whole domain + // (21 presets x 2 noise modes). + private static final RenderPipeline[] SHARPEN_PIPELINES = new RenderPipeline[42]; + + private static RenderPipeline sharpenPipeline(int blendPercent, boolean denoise) { + int preset = blendPercent / 5 + (denoise ? 21 : 0); + if (SHARPEN_PIPELINES[preset] == null) { + String name = "pipeline/sharpen_rcas" + (denoise ? "_denoise" : "") + "_" + blendPercent; + RenderPipeline.Builder builder = fullscreenBuilder(name, "core/rcas") + .withShaderDefine("RENDERSCALE_SHARPNESS", blendPercent / 100.0f); + if (denoise) { + builder = builder.withShaderDefine("FSR_RCAS_DENOISE"); + } + SHARPEN_PIPELINES[preset] = builder.build(); + } + return SHARPEN_PIPELINES[preset]; + } //?} @@ -394,7 +419,7 @@ public void resizeRenderTarget() { private void resizeRenderTarget(boolean reloadResources) { resize(renderTarget); - // The FSR intermediate target is output-sized and is resized by the blit pass. + // The intermediate target (FSR upscale or supersampling sharpening) is output-sized and is resized by the blit pass. //? <= 1.21.1 { /*resize(client.levelRenderer.entityTarget()); @@ -450,47 +475,38 @@ public void blitAndBlendToTexture(final RenderTarget input, final RenderTarget o //? < 1.21.11 { /*try (RenderPass renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(() -> "Blit render target", output.getColorTextureView(), OptionalInt.empty())) { - *///? } else + *///? } else if (getConfig().fsr && input.width <= output.width && input.height <= output.height) { - if (fsrIntermediateTarget == null) { - // TODO: maybe use TextureTarget, we're wasting like 32MB of VRAM here - fsrIntermediateTarget = new MainTarget(output.width, output.height); -// fsrIntermediateTarget = new TextureTarget("FSR: Intermediate", output.width, output.height); - } else if (fsrIntermediateTarget.width != output.width || fsrIntermediateTarget.height != output.height) { - fsrIntermediateTarget.resize(output.width, output.height); - } - - try (RenderPass renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(() -> "FSR: EASU", fsrIntermediateTarget.getColorTextureView(), /*? > 26.1 {*/ Optional /*?} else {*/ /*OptionalInt *//*?}*/.empty())) { - setPipeline(renderPass, FSR_EASU_PIPELINE); - RenderSystem.bindDefaultUniforms(renderPass); - renderPass.setUniform("InSampler", input.getColorTextureView(), RenderSystem.getSamplerCache().getClampToEdge(filter)); - //? < 26.2 { - /*renderPass.draw(0, 3); - *///?} else - renderPass.draw(3, 1, 0, 0); - } - - try (RenderPass renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(() -> "FSR: RCAS", output.getColorTextureView(), /*? > 26.1 {*/ Optional /*?} else {*/ /*OptionalInt *//*?}*/.empty())) { - setPipeline(renderPass, FSR_RCAS_PIPELINE); - RenderSystem.bindDefaultUniforms(renderPass); - renderPass.setUniform("InSampler", fsrIntermediateTarget.getColorTextureView(), RenderSystem.getSamplerCache().getClampToEdge(filter)); - //? < 26.2 { - /*renderPass.draw(0, 3); - *///?} else - renderPass.draw(3, 1, 0, 0); + RenderTarget intermediate = ensureIntermediateTarget(output.width, output.height); + fullscreenPass("FSR: EASU", FSR_EASU_PIPELINE, intermediate, input, filter); + fullscreenPass("FSR: RCAS", FSR_RCAS_PIPELINE, output, intermediate, filter); + } else if (input.width > output.width && input.height > output.height + && getConfig().getDownscaleFilter() != RenderScaleConfig.DownscaleFilter.BILINEAR) { + // Supersampling: the render target is larger than the output, so downscale + // with a fixed rotated/sparse grid filter instead of a single bilinear fetch. + // RGSS fits any scale but shines at ~2x; SGSS needs >= 3x of headroom to spread. + float ratio = Math.min((float) input.width / output.width, (float) input.height / output.height); + RenderPipeline pipeline = getConfig().getDownscaleFilter() == RenderScaleConfig.DownscaleFilter.SGSS && ratio >= 3.0f + ? SGSS_PIPELINE : RGSS_PIPELINE; + String passName = pipeline == SGSS_PIPELINE ? "SGSS downsample" : "RGSS downsample"; + // Sharpening runs as a second pass over an output-sized intermediate. + // Off (or 0%) resolves straight to the output, keeping the current + // single-pass behaviour and skipping all sharpening work. + float blend = getConfig().getSharpeningBlend(); + boolean sharpen = getConfig().getSharpeningMode() != RenderScaleConfig.SharpeningMode.OFF && blend > 0.0f; + if (sharpen) { + RenderTarget intermediate = ensureIntermediateTarget(output.width, output.height); + fullscreenPass(passName, pipeline, intermediate, input, FilterMode.LINEAR); + boolean denoise = getConfig().getSharpeningMode() == RenderScaleConfig.SharpeningMode.RCAS_DENOISE; + fullscreenPass(denoise ? "RCAS sharpen (noise-protected)" : "RCAS sharpen", + sharpenPipeline(Math.round(blend * 100.0f), denoise), output, intermediate, FilterMode.NEAREST); + } else { + fullscreenPass(passName, pipeline, output, input, FilterMode.LINEAR); } } else { - try (RenderPass renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(() -> "Blit render target", output.getColorTextureView(), /*? > 26.1 {*/ Optional /*?} else {*/ /*OptionalInt *//*?}*/.empty())) { - // Tracy blit is weird because I believe it's technically a debug pass. - // However, it looks exactly the same as vanilla, so I'm assuming it's fine. - setPipeline(renderPass, RenderPipelines.TRACY_BLIT); - RenderSystem.bindDefaultUniforms(renderPass); - renderPass.setUniform("InSampler", input.getColorTextureView(), RenderSystem.getSamplerCache().getClampToEdge(filter)); - //? < 26.2 { - /*renderPass.draw(0, 3); - *///?} else - renderPass.draw(3, 1, 0, 0); - } + // Tracy blit is weird because I believe it's technically a debug pass. + // However, it looks exactly the same as vanilla, so I'm assuming it's fine. + fullscreenPass("Blit render target", RenderPipelines.TRACY_BLIT, output, input, filter); } // copying depth doesn't seem to do anything? @@ -502,6 +518,31 @@ public void blitAndBlendToTexture(final RenderTarget input, final RenderTarget o // } } + // Output-sized lazily created/resize intermediate, shared by the FSR EASU + // upscale and the supersampling sharpening pass. + private RenderTarget ensureIntermediateTarget(int width, int height) { + if (intermediateTarget == null) { + // TODO: maybe use TextureTarget, we're wasting like 32MB of VRAM here + intermediateTarget = new MainTarget(width, height); +// intermediateTarget = new TextureTarget("FSR: Intermediate", width, height); + } else if (intermediateTarget.width != width || intermediateTarget.height != height) { + intermediateTarget.resize(width, height); + } + return intermediateTarget; + } + + private static void fullscreenPass(String name, RenderPipeline pipeline, RenderTarget output, RenderTarget input, FilterMode filter) { + try (RenderPass renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(() -> name, output.getColorTextureView(), /*? > 26.1 {*/ Optional /*?} else {*/ /*OptionalInt *//*?}*/.empty())) { + setPipeline(renderPass, pipeline); + RenderSystem.bindDefaultUniforms(renderPass); + renderPass.setUniform("InSampler", input.getColorTextureView(), RenderSystem.getSamplerCache().getClampToEdge(filter)); + //? < 26.2 { + /*renderPass.draw(0, 3); + *///?} else + renderPass.draw(3, 1, 0, 0); + } + } + private static void setPipeline(RenderPass renderPass, RenderPipeline pipeline) { //? >=26.3 { renderPass.setPipeline(RenderSystem.getCompiledPipeline(pipeline)); diff --git a/src/main/java/dev/zelo/renderscale/compat/sodium/RenderScaleSodiumConfig.java b/src/main/java/dev/zelo/renderscale/compat/sodium/RenderScaleSodiumConfig.java index 57ed874..af26a18 100644 --- a/src/main/java/dev/zelo/renderscale/compat/sodium/RenderScaleSodiumConfig.java +++ b/src/main/java/dev/zelo/renderscale/compat/sodium/RenderScaleSodiumConfig.java @@ -19,6 +19,12 @@ public class RenderScaleSodiumConfig implements ConfigEntryPoint { private static final Identifier MINIMUM_SCALE = id("minimum_scale"); //? >= 1.21.11 private static final Identifier FSR = id("fsr"); + //? >= 1.21.11 + private static final Identifier DOWNSCALE_FILTER = id("downscale_filter"); + //? >= 1.21.11 + private static final Identifier SHARPENING_MODE = id("sharpening_mode"); + //? >= 1.21.11 + private static final Identifier SHARPENING_STRENGTH = id("sharpening_strength"); //? iris private static final Identifier IRIS_SCALE = id("iris_scale"); @@ -43,7 +49,13 @@ public void registerConfigLate(ConfigBuilder builder) { .setStorageHandler(this.storageHandler) .setBinding(this::setScalePercent, this::getScalePercent) .setDefaultValue(100) - .setRange(1, 200, 1) + // Upper end of the slider must include the 300-400% scales + // where the Sparse Grid filter activates. Those filters only + // exist on 1.21.11+, so older versions keep the original range. + //? >= 1.21.11 { + .setRange(1, 400, 1) + //?} else + //.setRange(1, 200, 1) .setValueFormatter(RenderScaleSodiumConfig::formatPercent) // .setImpact(OptionImpact.VARIES) ) @@ -64,6 +76,41 @@ public void registerConfigLate(ConfigBuilder builder) { .setDefaultValue(false) .setImpact(OptionImpact.MEDIUM) ) + .addOption(builder.createIntegerOption(DOWNSCALE_FILTER) + .setName(Component.translatable("text.autoconfig.renderscale.option.downscaleFilter")) + .setTooltip(Component.translatable("text.autoconfig.renderscale.option.downscaleFilter.@Tooltip")) + .setStorageHandler(this.storageHandler) + .setBinding(v -> config().downscaleFilter = RenderScaleConfig.DownscaleFilter.values()[v], + () -> config().getDownscaleFilter().ordinal()) + .setDefaultValue(0) + .setRange(0, RenderScaleConfig.DownscaleFilter.values().length - 1, 1) + .setValueFormatter(value -> Component.translatable( + "text.autoconfig.renderscale.option.downscaleFilter." + + RenderScaleConfig.DownscaleFilter.values()[value].name())) + ) + .addOption(builder.createIntegerOption(SHARPENING_MODE) + .setName(Component.translatable("text.autoconfig.renderscale.option.sharpeningMode")) + .setTooltip(Component.translatable("text.autoconfig.renderscale.option.sharpeningMode.@Tooltip")) + .setStorageHandler(this.storageHandler) + .setBinding(v -> config().sharpeningMode = RenderScaleConfig.SharpeningMode.values()[v], + () -> config().getSharpeningMode().ordinal()) + .setDefaultValue(0) + .setRange(0, RenderScaleConfig.SharpeningMode.values().length - 1, 1) + .setValueFormatter(value -> Component.translatable( + "text.autoconfig.renderscale.option.sharpeningMode." + + RenderScaleConfig.SharpeningMode.values()[value].name())) + ) + .addOption(builder.createIntegerOption(SHARPENING_STRENGTH) + .setName(Component.translatable("text.autoconfig.renderscale.option.sharpeningStrength")) + .setTooltip(Component.translatable("text.autoconfig.renderscale.option.sharpeningStrength.@Tooltip")) + .setStorageHandler(this.storageHandler) + .setBinding(v -> config().sharpeningStrength = RenderScaleConfig.normalizeSharpeningStrength(v), + () -> config().sharpeningStrength) + .setDefaultValue(35) + .setRange(0, 100, 5) + .setValueFormatter(RenderScaleSodiumConfig::formatPercent) + .setImpact(OptionImpact.LOW) + ) //?} ) ) @@ -122,13 +169,17 @@ public void registerConfigLate(ConfigBuilder builder) { .setStorageHandler(this.storageHandler) .setBinding(this::setIrisScalePercent, this::getIrisScalePercent) .setDefaultValue(0) - .setRange(0, 200, 1) + //? >= 1.21.11 { + .setRange(0, 400, 1) + //?} else + //.setRange(0, 200, 1) .setValueFormatter(RenderScaleSodiumConfig::formatIrisScale) // .setImpact(OptionImpact.VARIES) ) ) - ); + ) //?} + ; } private static Identifier id(String path) { diff --git a/src/main/java/dev/zelo/renderscale/config/RenderScaleConfig.java b/src/main/java/dev/zelo/renderscale/config/RenderScaleConfig.java index 37f6c37..1f47105 100644 --- a/src/main/java/dev/zelo/renderscale/config/RenderScaleConfig.java +++ b/src/main/java/dev/zelo/renderscale/config/RenderScaleConfig.java @@ -51,9 +51,33 @@ public String getKey() { } } + //? >= 1.21.11 { + public enum DownscaleFilter implements me.shedaniel.clothconfig2.gui.entries.SelectionListEntry.Translatable { + BILINEAR, RGSS, SGSS; + + @Override + public String getKey() { + return "text.autoconfig.renderscale.option.downscaleFilter." + name(); + } + } + + public enum SharpeningMode implements me.shedaniel.clothconfig2.gui.entries.SelectionListEntry.Translatable { + OFF, RCAS, RCAS_DENOISE; + + @Override + public String getKey() { + return "text.autoconfig.renderscale.option.sharpeningMode." + name(); + } + } + //?} + @Override public void validatePostLoad() { if (aggressionLevel == null) aggressionLevel = Aggression.NORMAL; + //? >= 1.21.11 { + if (sharpeningMode == null) sharpeningMode = SharpeningMode.OFF; + sharpeningStrength = normalizeSharpeningStrength(sharpeningStrength); + //?} } @ConfigEntry.Category("dynamic") @@ -65,6 +89,18 @@ public void validatePostLoad() { //? >= 1.21.11 { @ConfigEntry.Gui.Tooltip() public boolean fsr = false; + + @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON) + @ConfigEntry.Gui.Tooltip() + public DownscaleFilter downscaleFilter = DownscaleFilter.BILINEAR; + + @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON) + @ConfigEntry.Gui.Tooltip() + public SharpeningMode sharpeningMode = SharpeningMode.OFF; + + @ConfigEntry.BoundedDiscrete(min = 0, max = 100) + @ConfigEntry.Gui.Tooltip() + public int sharpeningStrength = 35; //?} // double UltraQuality = 1.3; // (0.77) @@ -105,6 +141,10 @@ public static ConfigHolder init() { // Change resolution upon save! holder.registerSaveListener((manager, data) -> { + //? >= 1.21.11 { + // Persist the same canonical presets the renderer consumes. + data.sharpeningStrength = normalizeSharpeningStrength(data.sharpeningStrength); + //?} RenderScale.getInstance().onResolutionChanged(); IrisCompatibility.reloadShaders(); return null; @@ -164,4 +204,30 @@ public boolean getFilter() { //?} else //return forceLinear || effectiveScale > 1.0; } + + //? >= 1.21.11 { + public DownscaleFilter getDownscaleFilter() { + return downscaleFilter == null ? DownscaleFilter.BILINEAR : downscaleFilter; + } + + public SharpeningMode getSharpeningMode() { + return sharpeningMode == null ? SharpeningMode.OFF : sharpeningMode; + } + + // The stored strength is canonicalized to the 5% presets the renderer + // and the cached sharpening pipelines use, at every write path: config + // load (validatePostLoad), save, and the Sodium slider binding. 0 means + // the pass is skipped entirely (RCAS's fast normalization is not an + // exact passthrough, so the bypass must happen on the CPU side). + public static int normalizeSharpeningStrength(int strength) { + return Math.clamp(Math.round(strength / 5f), 0, 20) * 5; + } + + // Blend factor of the sharpening pass. The shader runs RCAS at its + // reference tuning and mixes towards the unsharpened pixel, so the + // strength behaves as a consistent 0-100% control. + public float getSharpeningBlend() { + return normalizeSharpeningStrength(sharpeningStrength) / 100.0f; + } + //?} } diff --git a/src/main/resources/assets/renderscale/lang/en_us.json b/src/main/resources/assets/renderscale/lang/en_us.json index a341bf3..ae6d846 100644 --- a/src/main/resources/assets/renderscale/lang/en_us.json +++ b/src/main/resources/assets/renderscale/lang/en_us.json @@ -13,6 +13,18 @@ "text.autoconfig.renderscale.option.fsr": "Use FSR 1 (Experimental)", "text.autoconfig.renderscale.option.fsr.@Tooltip": "Results may vary depending on your hardware. Compare your FPS!", "text.autoconfig.renderscale.option.fsr.@Tooltip.sodium": "Results may vary depending on your hardware, compare your FPS!", + "text.autoconfig.renderscale.option.downscaleFilter": "Supersampling filter", + "text.autoconfig.renderscale.option.downscaleFilter.@Tooltip": "How the higher-resolution 3D view is downscaled to the window. Rotated Grid is best around 2x scale. Sparse Grid is a wider filter used at 300% scale and above; at lower scales it falls back to Rotated Grid. Applies when the scale is above 100%.", + "text.autoconfig.renderscale.option.downscaleFilter.BILINEAR": "Bilinear (default)", + "text.autoconfig.renderscale.option.downscaleFilter.RGSS": "Rotated Grid", + "text.autoconfig.renderscale.option.downscaleFilter.SGSS": "Sparse Grid", + "text.autoconfig.renderscale.option.sharpeningMode": "Sharpening", + "text.autoconfig.renderscale.option.sharpeningMode.@Tooltip": "Sharpens the supersampled 3D view (Rotated/Sparse Grid only, never FSR). RCAS is AMD's FidelityFX sharpening pass; Noise-protected trades some sharpness to avoid amplifying grain.", + "text.autoconfig.renderscale.option.sharpeningMode.OFF": "Off", + "text.autoconfig.renderscale.option.sharpeningMode.RCAS": "RCAS", + "text.autoconfig.renderscale.option.sharpeningMode.RCAS_DENOISE": "RCAS (Noise-protected)", + "text.autoconfig.renderscale.option.sharpeningStrength": "Sharpening strength", + "text.autoconfig.renderscale.option.sharpeningStrength.@Tooltip": "How much sharpening to apply. 0% disables the sharpening pass; the stored value is normalized to the nearest 5% step, matching the presets the renderer uses.", "text.autoconfig.renderscale.option.forceLinear": "Force linear scaling", "text.autoconfig.renderscale.option.forceLinear.@Tooltip": "Forces bilinear upscaling on scale factors less than 100%.", "text.autoconfig.renderscale.option.irisScale": "Scale factor for shaders", diff --git a/src/main/resources/assets/renderscale/shaders/core/rcas.fsh b/src/main/resources/assets/renderscale/shaders/core/rcas.fsh index b1f645c..4fe5720 100644 --- a/src/main/resources/assets/renderscale/shaders/core/rcas.fsh +++ b/src/main/resources/assets/renderscale/shaders/core/rcas.fsh @@ -23,17 +23,27 @@ return texelFetch(InSampler, position, 0); void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) { -// Leave empty unless you require an input colour conversion. +// The required input is normalized, display-encoded (sRGB-style +// perceptual) RGB, which is what vanilla is expected to render into the +// main target. This comment is not verification of every attachment +// format or shader-pack colour path: do not add a colour conversion here +// without checking the actual format in game. } #moj_import +// The fixed sharpness tuning is AMD's shipped FSR1 default (stop parameter +// 0.2, i.e. gain 2^-0.2); the FSR pipeline relies on that default. +#ifndef RENDERSCALE_RCAS_GAIN +#define RENDERSCALE_RCAS_GAIN exp2(-0.2) +#endif + void main() { AU2 outputPixel = AU2(gl_FragCoord.xy); // FsrRcasF only uses the 32-bit sharpness in con.x. Avoid generating the unused // half-float constants, which require packing functions absent from GLSL 330. -AU4 fsrConst0 = AU4(floatBitsToUint(exp2(-0.2)), 0u, 0u, 0u); +AU4 fsrConst0 = AU4(floatBitsToUint(RENDERSCALE_RCAS_GAIN), 0u, 0u, 0u); AF3 colour; FsrRcasF( @@ -44,5 +54,16 @@ outputPixel, fsrConst0 ); +// Supersampling pipelines define RENDERSCALE_SHARPNESS (0-1) and blend +// towards the unsharpened centre pixel so the strength control is a +// consistent mix; the 0% setting is skipped CPU-side, since RCAS's fast +// normalization is not an exact passthrough. The Noise-protected variant +// additionally sets FSR_RCAS_DENOISE. The FSR upscale pipeline defines +// neither, so its output path is unchanged. +#ifdef RENDERSCALE_SHARPNESS +AF3 centre = texelFetch(InSampler, ivec2(outputPixel), 0).rgb; +fragColor = vec4(mix(centre, colour, RENDERSCALE_SHARPNESS), 1.0); +#else fragColor = vec4(colour, 1.0); +#endif } diff --git a/src/main/resources/assets/renderscale/shaders/core/rgss.fsh b/src/main/resources/assets/renderscale/shaders/core/rgss.fsh new file mode 100644 index 0000000..97c0f15 --- /dev/null +++ b/src/main/resources/assets/renderscale/shaders/core/rgss.fsh @@ -0,0 +1,33 @@ +#version 420 + +// Rotated Grid resolve filter for ordered-grid supersampling. +// +// The four taps sit on a grid rotated by arctan(1/2) (~26.6 degrees), so +// every tap reads the already-rendered high-resolution image at a +// different horizontal AND vertical offset within the output pixel's +// footprint. Compared to a plain box average of the same 2x-order +// source, angled and axis-crossing edges resolve to more intermediate +// shades instead of a two-step staircase. These taps filter the rendered +// image; they do not add geometry coverage samples. + +#moj_import + +uniform sampler2D InSampler; + +out vec4 fragColor; + +void main() +{ +// ScreenSize must match this pass's destination texture size. +// One output pixel expressed in normalized input UV coordinates. +vec2 outputToInput = 1.0 / ScreenSize; + +vec2 uv = gl_FragCoord.xy / ScreenSize; + +vec3 sum = texture(InSampler, uv + vec2( 0.125, 0.375) * outputToInput).rgb + + texture(InSampler, uv + vec2( 0.375, -0.125) * outputToInput).rgb + + texture(InSampler, uv + vec2(-0.125, -0.375) * outputToInput).rgb + + texture(InSampler, uv + vec2(-0.375, 0.125) * outputToInput).rgb; + +fragColor = vec4(sum * 0.25, 1.0); +} diff --git a/src/main/resources/assets/renderscale/shaders/core/sgss.fsh b/src/main/resources/assets/renderscale/shaders/core/sgss.fsh new file mode 100644 index 0000000..cd49ab8 --- /dev/null +++ b/src/main/resources/assets/renderscale/shaders/core/sgss.fsh @@ -0,0 +1,43 @@ +#version 420 + +// Sparse Grid resolve filter for ordered-grid supersampling. +// +// Eight bilinear taps: two half-scale rotated-grid quads placed in +// opposite quadrants with opposite orientations. Every tap reads the +// already-rendered high-resolution image at a different horizontal AND +// vertical offset within the output pixel's footprint, so angled edges +// resolve to more intermediate shades than a plain 4x-order box average +// across the tested edge angles, at the cost of slight softening of fine +// textures. (The opposite-quadrant layout is not perfectly isotropic on +// reflected diagonals.) Meant for render scales of 3x and above, where the +// pattern has room to spread; below that the single rotated grid filter is +// a better fit. These taps do not add geometry coverage samples. + +#moj_import + +uniform sampler2D InSampler; + +out vec4 fragColor; + +void main() +{ +// ScreenSize must match this pass's destination texture size. +// One output pixel expressed in normalized input UV coordinates. +vec2 outputToInput = 1.0 / ScreenSize; + +vec2 uv = gl_FragCoord.xy / ScreenSize; + +// Two half-scale rotated-grid quads centred at (+0.25, -0.25) and +// (-0.25, +0.25), the second rotated 90 degrees. All coordinates are +// exact multiples of 1/16. +vec3 sum = texture(InSampler, uv + vec2( 0.3125, -0.0625) * outputToInput).rgb + + texture(InSampler, uv + vec2( 0.4375, -0.3125) * outputToInput).rgb + + texture(InSampler, uv + vec2( 0.1875, -0.4375) * outputToInput).rgb + + texture(InSampler, uv + vec2( 0.0625, -0.1875) * outputToInput).rgb + + texture(InSampler, uv + vec2(-0.0625, 0.3125) * outputToInput).rgb + + texture(InSampler, uv + vec2(-0.1875, 0.0625) * outputToInput).rgb + + texture(InSampler, uv + vec2(-0.4375, 0.1875) * outputToInput).rgb + + texture(InSampler, uv + vec2(-0.3125, 0.4375) * outputToInput).rgb; + +fragColor = vec4(sum * 0.125, 1.0); +}