Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ private MixinConfig() {

this.addMixinRule("features.render.particle", true);

this.addMixinRule("features.render.sync", true);

this.addMixinRule("features.render.world", true);
this.addMixinRule("features.render.world.clouds", true);
this.addMixinRule("features.render.world.sky", true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package net.caffeinemc.mods.sodium.mixin.features.render.sync;

import com.mojang.blaze3d.systems.RenderSystem;
import org.lwjgl.glfw.GLFW;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;

@Mixin(value = RenderSystem.class, remap = false)
public class RenderSystemMixin {
/**
* @author theyareonit
* @reason Improve frame synchronization
*/
@Overwrite
public static void limitDisplayFPS(int fps) {
double frameTime = 1.0 / fps;
double now = GLFW.glfwGetTime();
double end = (now - (now % frameTime)) + frameTime; // subtracting (now % frameTime) corrects for desync
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original issue the proposed solution was to use lastDrawTime = target, but in this PR the lastDrawTime field seems entirely unused, and the sleep time is instead calculated from solely the current time. Why is this change? Doesn't this cause frames to be dropped when one frame took too long?

Copy link
Copy Markdown
Author

@theyareonit theyareonit Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivation behind this change was that, with lastDrawTime = target, the game essentially switches to unlimited FPS whenever you lag in an effort to catch up (since lastDrawTime will be far in the past). This creates problems if you have a big stutter, since the game might switch to unlimited FPS for multiple seconds, which somewhat defeats the point of using a capped framerate.

There are a few ways to address this issue. You could, for instance:

  • Not let lastDrawTime get too far behind (e.g. ensure it's only 5 frames behind at most)
  • Always drop frames when you get out of sync instead of trying to catch up
  • Use lastDrawTime = now like the vanilla game does, but only when you get a large stutter

I chose option 2 because option 1 and option 3 both cause temporary or permanent desync from theoretically perfect frame timing, which might cause issues like having your tearline jump around the screen, or less predictability with inputs. And I also wasn't sure that switching to unlimited FPS after a minor stutter really provided a meaningful benefit in terms of smoothness, compared to capped FPS that never gets out of sync. But I might have been wrong about this, and others could do their own testing here.

edit:

I suppose option 2 is really just option 1 but with the max catchup window set to 0 frames. So maybe the number of frames behind it can get could just be an ingame setting with the default at like 2-5? Not sure.

I think on a personal level something like option 1 but with a catchup window of only 1 frame sounds like the best experience to me thinking about it now, but setting it higher by default is probably safe enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think on a personal level something like option 1 but with a catchup window of only 1 frame sounds like the best experience to me thinking about it now, but setting it higher by default is probably safe enough.

I agree this seems the best solution.

douira said on discord adding an in-game option seems ok.


for (; now < end; now = GLFW.glfwGetTime()) {
double waitTime = (end - now) - 0.002; // -2ms to account for sleep imprecision on some operating systems
if (waitTime >= 0.001) { // cant sleep less than 1ms without platform-specific code
Comment on lines +21 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sleep is performed here when sleep time is smaller than 3ms. While I'm not familiar with macOS, this at least shouldn't be necessary on Windows and Linux, both of which have 1ms timer resolution - only subtract 1ms (or something like 1.1ms to be safer) from waitTime to allow the render thread to sleep longer.

GLFW.glfwWaitEventsTimeout(waitTime);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using glfwWaitEventsTimeout is not ideal:

  • It is not a sleep function. It may wake up whenever there is new event from the operating system. In my testing, it woke up around 20 times each frame at 60FPS (with no user input), literally taking away the potential power saving benefits of limiting the FPS
  • It does not, as one might imagine, use a high resolution timer (Win32). As such, it'd be better to prefer Java's built-in sleep methods
    • Thread#sleep, however, should be avoided: it repeats waiting until it believes (as specified by System#nanoTime) the elapsed time is longer than or equal to the requested sleep time. Due to the precision of System#nanoTime. it could frequently cause additional sleeps, requiring extra scheduler periods to finish
    • As an alternative, LockSupport#parkNanos could be used

The spin wait when waitTime <1ms could be (theoretically) improved with Thread#onSpinWait.

}
}

GLFW.glfwPollEvents();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could adapt RenderSystemMixin to skip both polls when FPS limiter is active.

}
}
1 change: 1 addition & 0 deletions src/main/resources/sodium.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"features.render.model.block.ModelBlockRendererMixin",
"features.render.model.item.ItemRendererMixin",
"features.render.particle.SingleQuadParticleMixin",
"features.render.sync.RenderSystemMixin",
"features.render.world.clouds.LevelRendererMixin",
"features.render.world.sky.FogRendererMixin",
"features.render.world.sky.ClientLevelMixin",
Expand Down