Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Press `U` (sometimes `O`) or, use the mod menu config to control the render scal

## Other features

On 1.21.11 and above, there is experimental support for **FSR 1.0**! No plans to support DLSS or FSR 2.0+.
**FSR 1** support! No plans to support DLSS or FSR 2+.

Also, there is experimental support for **Dynamic Scaling** (1.4.0-alpha.6+), which tries to keep the game at your targeted FPS by changing the multiplier while you play.

Expand Down
113 changes: 105 additions & 8 deletions src/main/java/dev/zelo/renderscale/RenderScale.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,96 @@ public class RenderScale {
private RenderTarget intermediateTarget;

//? >= 1.21.11 {
public static RenderPipeline FSR_EASU_PIPELINE = buildFsrEasuPipeline();
public static final RenderPipeline FSR_EASU_PIPELINE = createFsrPipeline("easu", 0);
public static final RenderPipeline FSR_RCAS_PIPELINE = createFsrPipeline("rcas", 0);

private static RenderPipeline buildFsrEasuPipeline() {
RenderPipeline.Builder builder = fullscreenBuilder("pipeline/fsr_easu", "core/easu");
private Object fsrDevice;
private RenderPipeline fsrEasuPipeline = FSR_EASU_PIPELINE;
private RenderPipeline fsrRcasPipeline = FSR_RCAS_PIPELINE;

private static RenderPipeline createFsrPipeline(String pass, int fp16Extension) {
String suffix = fp16Extension == 0 ? "" : "_fp16";
RenderPipeline.Builder builder = fullscreenBuilder("pipeline/fsr_" + pass + suffix, "core/" + pass + suffix);
// 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");
if ("easu".equals(pass)) {
builder.withShaderDefine("RENDERSCALE_EXPLICIT_GATHER");
}
//?}
if (fp16Extension != 0) {
builder.withShaderDefine("RENDERSCALE_FP16", fp16Extension);
}
return builder.build();
}

public static RenderPipeline FSR_RCAS_PIPELINE =
fullscreenPipeline("pipeline/fsr_rcas", "core/rcas");
private void selectFsrPipelines() {
var device = RenderSystem.getDevice();
if (fsrDevice == device) return;
fsrDevice = device;
fsrEasuPipeline = FSR_EASU_PIPELINE;
fsrRcasPipeline = FSR_RCAS_PIPELINE;

int extension = 0;
//? >=26.2 {
String backend = device.getDeviceInfo().backendName();
//?} else
//String backend = device.getBackendName();
if ("OpenGL".equals(backend)) {
// DeviceInfo lists only extensions used by vanilla, not all supported
// extensions. Query the active context after confirming the backend.
var capabilities = org.lwjgl.opengl.GL.getCapabilities();
// NVIDIA exposes FP16 (and accepts our GLSL 450 shaders) even in
// Minecraft's OpenGL 3.3 context. Check the shader extensions rather
// than the context version, then validate both compiled pipelines.
if (capabilities.GL_AMD_gpu_shader_half_float) extension = 2;
else if (capabilities.GL_NV_gpu_shader5) extension = 3;
else {
int count = org.lwjgl.opengl.GL30C.glGetInteger(org.lwjgl.opengl.GL30C.GL_NUM_EXTENSIONS);
for (int i = 0; i < count; i++) {
String name = org.lwjgl.opengl.GL30C.glGetStringi(org.lwjgl.opengl.GL30C.GL_EXTENSIONS, i);
if ("GL_EXT_shader_explicit_arithmetic_types_float16".equals(name)
|| "GL_EXT_shader_explicit_arithmetic_types".equals(name)) {
extension = 1;
break;
}
}
}
}
//? >=26.2 {
if ("Vulkan".equals(backend) && dev.zelo.renderscale.compat.vulkan.VulkanFsrSupport.isEnabled(device)) {
extension = 1;
}
//?}
if (extension == 0) {
Constants.LOG.info("FSR1: using FP32 (shader FP16 is unavailable)");
return;
}
//? >=26.3 {
// ShaderC consumes EXT syntax; SPIRV-Cross emits the driver's extension.
extension = 1;
//?}
var easu = createFsrPipeline("easu", extension);
var rcas = createFsrPipeline("rcas", extension);
try {
//? >=26.3 {
boolean valid = RenderSystem.getCompiledPipelineNullable(easu) != null
&& RenderSystem.getCompiledPipelineNullable(rcas) != null;
//?} else {
/*boolean valid = device.precompilePipeline(easu).isValid()
&& device.precompilePipeline(rcas).isValid();
*///?}
if (valid) {
fsrEasuPipeline = easu;
fsrRcasPipeline = rcas;
Constants.LOG.info("FSR1: using FP16");
} else {
Constants.LOG.warn("FSR1: FP16 shader compilation failed; using FP32");
}
} catch (RuntimeException exception) {
Constants.LOG.warn("FSR1: FP16 shader compilation failed; using FP32", exception);
}
}

public static RenderPipeline RGSS_PIPELINE =
fullscreenPipeline("pipeline/rgss", "core/rgss");
Expand Down Expand Up @@ -377,6 +453,26 @@ public double getRenderScaleFactor() {
: getConfig().getScale();
}

public String getScalingMode() {
double scale = getRenderScaleFactor();
boolean upsampling = scale < 1.0;
boolean downsampling = scale > 1.0;
if (renderTarget != null && clientRenderTarget != null) {
upsampling = renderTarget.width < clientRenderTarget.width
|| renderTarget.height < clientRenderTarget.height;
downsampling = renderTarget.width > clientRenderTarget.width
|| renderTarget.height > clientRenderTarget.height;
}
String direction = downsampling ? "Downsampling" : upsampling ? "Upsampling" : "Native";
// Match the blit pass: FSR also runs at native resolution, but not when downsampling.
//? >= 1.21.11 {
if (getConfig().fsr && !downsampling) {
return direction + ", FSR1 " + (fsrEasuPipeline == FSR_EASU_PIPELINE ? "FP32" : "FP16");
}
//?}
return direction + (getConfig().getFilter() ? ", Linear" : ", Nearest");
}

public void updateDynamicScale() {
RenderScaleConfig config = getConfig();
if (dynamicScaleLevel != client.level) {
Expand Down Expand Up @@ -491,9 +587,10 @@ public void blitAndBlendToTexture(final RenderTarget input, final RenderTarget o
/*try (RenderPass renderPass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(() -> "Blit render target", output.getColorTextureView(), OptionalInt.empty())) {
*///? } else
if (getConfig().fsr && input.width <= output.width && input.height <= output.height) {
selectFsrPipelines();
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);
fullscreenPass("FSR: EASU", fsrEasuPipeline, intermediate, input, filter);
fullscreenPass("FSR: RCAS", fsrRcasPipeline, 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//? >=26.2 {
package dev.zelo.renderscale.compat.vulkan;

//? >=26.3 {
import com.mojang.renderpearl.api.device.GpuDevice;
import com.mojang.renderpearl.backend.vulkan.VulkanDevice;
import com.mojang.renderpearl.backend.vulkan.VulkanFeatureSets;
import com.mojang.renderpearl.backend.vulkan.init.FeatureSet;
import com.mojang.renderpearl.backend.vulkan.init.VulkanFeature;
//?} else {
/*import com.mojang.blaze3d.systems.GpuDevice;
import com.mojang.blaze3d.vulkan.VulkanDevice;
import com.mojang.blaze3d.vulkan.VulkanBackend;
import com.mojang.blaze3d.vulkan.init.VulkanFeature;
*///?}
import dev.zelo.renderscale.mixin.accessors.MixinGpuDeviceAccessor;
import org.lwjgl.vulkan.VkDevice;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

public final class VulkanFsrSupport {
// Both Vulkan backends require Vulkan 1.2. Reuse their existing feature
// structure; no KHR extension or additional 16-bit storage feature is needed.
public static final VulkanFeature SHADER_FLOAT16 = new VulkanFeature(
//? >=26.3 {
VulkanFeatureSets.VK12_FEATURES_STRUCT,
//?} else
//VulkanBackend.VK12_FEATURES_STRUCT,
"shaderFloat16", org.lwjgl.vulkan.VkPhysicalDeviceVulkan12Features.SHADERFLOAT16);

//? >=26.3 {
public static final FeatureSet FP16 = new FeatureSet("RenderScale FSR1 FP16", Set.of(), Set.of(SHADER_FLOAT16));
//?}

// Track successful logical-device creation, not just physical-device support.
// Weak keys avoid retaining devices after a backend switch or failed startup.
private static final Map<VkDevice, Boolean> ENABLED = Collections.synchronizedMap(new WeakHashMap<>());

private VulkanFsrSupport() {}

public static void recordEnabled(VkDevice device, boolean enabled) {
ENABLED.put(device, enabled);
}

public static boolean isEnabled(GpuDevice device) {
return device instanceof MixinGpuDeviceAccessor accessor
&& accessor.renderScale$getBackend() instanceof VulkanDevice vulkan
&& Boolean.TRUE.equals(ENABLED.get(vulkan.vkDevice()));
}
}
//?}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//? 1.20.1 && forge {
/*package dev.zelo.renderscale.gametest;

import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.pipeline.TextureTarget;
import dev.zelo.renderscale.Constants;
import dev.zelo.renderscale.RenderScale;
import net.irisshaders.iris.Iris;
import net.irisshaders.iris.gl.framebuffer.GlFramebuffer;
import net.irisshaders.iris.pipeline.IrisRenderingPipeline;
import net.irisshaders.iris.targets.Blaze3dRenderTargetExt;
import net.irisshaders.iris.targets.RenderTargets;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.PauseScreen;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraftforge.event.TickEvent;
import org.lwjgl.opengl.GL11C;
import org.lwjgl.opengl.GL30C;
import org.lwjgl.opengl.GL45C;

import java.lang.reflect.Field;
import java.nio.file.Files;

// Run in a disposable game directory with Oculus, a shaderpack, and
// -Drenderscale.shaderCompatibilityTest=true. Render ticks continue while paused.
public final class ForgeShaderCompatibilityTest {
private static int step;
private static int frames;
private static long started;
private static boolean done;

private ForgeShaderCompatibilityTest() {}

public static void frame(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.END || done) return;
Minecraft client = Minecraft.getInstance();
if (started == 0) started = System.nanoTime();
try {
if (System.nanoTime() - started > 180_000_000_000L) throw new AssertionError("Timed out at step " + step);
if (step == 0) {
if (!(client.screen instanceof TitleScreen) || client.getOverlay() != null) return;
client.options.pauseOnLostFocus = false;
// Reproduce modpacks that request stencil before entering a world.
client.getMainRenderTarget().enableStencil();
setScale(0.25f);
RenderScaleAutoTest.INSTANCE.createWorld(client);
step++;
return;
}
if (client.level == null || client.player == null) return;
if (step == 1 && client.screen != null) return;
if (++frames < 30) return;
frames = 0;
verifyTargets();
Constants.LOG.info("Shader compatibility test step {} passed (scale={}, paused={})",
step, RenderScale.getConfig().scale, client.isPaused());
switch (step++) {
case 1 -> setScale(1.0f);
case 2 -> {
// Force the version collision instead of relying on incidental
// window resizes or a particular driver's texture-ID allocation.
RenderScale scale = RenderScale.getInstance();
while (version(scale.clientRenderTarget) < version(scale.renderTarget)) {
resize(scale.clientRenderTarget);
}
while (version(scale.renderTarget) < version(scale.clientRenderTarget)) {
resize(scale.renderTarget);
}
Iris.reload(); // Pipeline is created against the native target.
client.getMainRenderTarget().bindWrite(true);
}
case 3 -> client.setScreen(new PauseScreen(true));
case 4 -> {
require(client.isPaused(), "Pause screen did not pause the game");
client.setScreen(null);
}
case 5 -> setScale(0.25f);
case 6 -> client.setScreen(new PauseScreen(true));
case 7 -> {
require(client.isPaused(), "Pause screen did not pause the game");
client.setScreen(null);
}
case 8 -> finish(client, null);
default -> throw new AssertionError("Unexpected step");
}
} catch (Throwable error) {
finish(client, error);
}
}

private static void setScale(float value) {
RenderScale.getConfig().scale = value;
RenderScale.getConfig().targetFrameRate = 0;
RenderScale.CONFIG.save();
}

private static int version(RenderTarget target) {
return ((Blaze3dRenderTargetExt) target).iris$getDepthBufferVersion();
}

private static void resize(RenderTarget target) {
target.resize(target.width, target.height, Minecraft.ON_OSX);
}

private static void verifyTargets() throws ReflectiveOperationException {
RenderScale scale = RenderScale.getInstance();
require(scale.renderTarget != null && scale.clientRenderTarget != null, "Missing targets");
require(scale.renderTarget.isStencilEnabled() && scale.clientRenderTarget.isStencilEnabled(), "Stencil lost during target swap");
require(Minecraft.getInstance().getMainRenderTarget() == scale.clientRenderTarget, "Main target not restored for UI");
Object pipeline = Iris.getPipelineManager().getPipelineNullable();
require(pipeline instanceof IrisRenderingPipeline, "Shader pipeline not active");
RenderTargets targets = (RenderTargets) field(pipeline, "renderTargets");
int depth = scale.renderTarget.getDepthTextureId();
require(targets.getDepthTexture() == depth, "Oculus retained another target's depth texture");
GlFramebuffer source = (GlFramebuffer) field(targets, "depthSourceFb");
int attachment = GL45C.glGetNamedFramebufferAttachmentParameteri(source.getId(), GL30C.GL_DEPTH_ATTACHMENT,
GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
int stencil = GL45C.glGetNamedFramebufferAttachmentParameteri(source.getId(), GL30C.GL_STENCIL_ATTACHMENT,
GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
require(attachment == depth && stencil == depth, "Oculus depth and stencil are not the scaled texture");
for (GlFramebuffer framebuffer : (Iterable<GlFramebuffer>) field(targets, "ownedFramebuffers")) {
require(GL45C.glCheckNamedFramebufferStatus(framebuffer.getId(), GL30C.GL_FRAMEBUFFER)
== GL30C.GL_FRAMEBUFFER_COMPLETE, "Incomplete Oculus framebuffer " + framebuffer.getId());
}
// Exercise the full-size depth copy used by post-processing/particle mods.
TextureTarget copy = new TextureTarget(scale.clientRenderTarget.width, scale.clientRenderTarget.height, true, Minecraft.ON_OSX);
try {
copy.enableStencil();
copy.copyDepthFrom(scale.renderTarget);
require(GL11C.glGetError() == GL11C.GL_NO_ERROR, "OpenGL error during rendering or depth copy");
} finally {
copy.destroyBuffers();
scale.clientRenderTarget.bindWrite(true);
}
FramebufferPresentationTest.verify();
}

private static Object field(Object owner, String name) throws ReflectiveOperationException {
Field field = owner.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(owner);
}

private static void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}

private static void finish(Minecraft client, Throwable error) {
done = true;
try {
Files.writeString(client.gameDirectory.toPath().resolve("shader-compatibility-result.txt"),
error == null ? "PASS\n" : "FAIL: " + error + "\n");
} catch (java.io.IOException io) {
Constants.LOG.error("Could not write shader compatibility result", io);
}
if (error == null) Constants.LOG.info("Shader compatibility test PASSED");
else Constants.LOG.error("Shader compatibility test FAILED at step " + step, error);
client.stop();
}
}
*///?}
Loading
Loading