Skip to content

Use the real partial tick for ClientTickCounter - #1333

Open
AlexProGear wants to merge 1 commit into
FallingColors:1.21from
AlexProGear:fix-clienttickcounter-partial-tick
Open

AlexProGear wants to merge 1 commit into
FallingColors:1.21from
AlexProGear:fix-clienttickcounter-partial-tick

Conversation

@AlexProGear

Copy link
Copy Markdown

ClientTickCounter ends up holding a per-frame delta instead of the partial tick, so every animation that reads it advances at the game tick rate instead of the framerate.

The bug

RenderFrameEvent.Pre does:

ClientTickCounter.renderTickStart(e.getPartialTick().getGameTimeDeltaTicks());

DeltaTracker.getGameTimeDeltaTicks() returns Timer.deltaTicks, which is (now - lastFrame) / msPerTick: the game time that elapsed during this frame. It is roughly 0.333 at 60 fps, and it tracks the framerate rather than the position inside the tick.

ClientTickCounter is built around the other value:

public static float getTotal()  { return (float) ticksInGame + partialTicks; }
public static void renderTickStart(float renderTickTime) { partialTicks = renderTickTime; }
public static void clientTickEnd() { ++ticksInGame; partialTicks = 0.0F; }

clientTickEnd() resets partialTicks to 0 at the end of every tick and renderTickStart() fills it back in for the frames that follow, so the value is clearly meant to be the 0..1 position inside the current tick. Feeding it a per-frame delta keeps ticksInGame + partialTicks on a single value for the whole tick (at 60 fps it stays at N + 0.33 for all three frames of tick N), so it only moves once per tick.

Everything that derives its animation time from getTotal() therefore runs at 20 Hz:

  • RenderLib.zSeed = ClientTickCounter.getTotal() * speed - the pattern wobble,
  • HexAdditionalRenderers.renderSentinel - getTotal() / 2, the sentinel bob and spin,
  • ClientRenderHelper - scrolling patterns,
  • GuiSpellcasting - pattern previews in the GUI,
  • MsgCastParticleS2C - cast particles,
  • IotaHolderItem - the hue of iota tints.

Visually, the pattern wobble and the sentinel step instead of moving smoothly.

The fix

Use the member vanilla itself uses as the render partial tick:

ClientTickCounter.renderTickStart(e.getPartialTick().getGameTimeDeltaPartialTick(false));

DeltaTracker.getGameTimeDeltaPartialTick(boolean) returns deltaTickResidual, the 0..1 fraction of the current tick - exactly what renderTickStart()/clientTickEnd() are written for. For comparison, vanilla passes getGameTimeDeltaPartialTick(false) where it needs a render partial tick (LevelRenderer, GameRenderer) and only calls getGameTimeDeltaTicks() where it wants the elapsed frame time instead (post-effect, entity-effect and overlay processing).

getTotal() now grows smoothly within each tick, so all of the consumers above animate at the framerate. The value at a tick boundary is unchanged, and the paused/frozen handling of DeltaTracker comes along for free.

Testing

Tested in a NeoForge 1.21.1 dev client on the 0.12.0-devel line: before the change the pattern wobble visibly stepped at 20 Hz, after it is smooth at the same framerate, with no other behavioural difference. The compiled bytecode was checked to confirm which method the listener actually invokes.

The same call in the Fabric module

FabricHexClientInitializer.kt:39 (WorldRenderEvents.START) passes it.tickCounter().gameTimeDeltaTicks to the same renderTickStart, and this repository uses official Mojang mappings for both loaders, so it is the same DeltaTracker.getGameTimeDeltaTicks() frame delta. I left it out of this PR because I could not build the Fabric module here, but the equivalent change would be:

WorldRenderEvents.START.register { ClientTickCounter.renderTickStart(it.tickCounter().gameTimeDeltaPartialTick(false)) }

Note

The RenderLevelStageEvent listener above is left alone on purpose: it forwards its partialTick to HexAdditionalRenderers.overlayLevel and on to renderSentinel, where that parameter is never read (the renderer positions itself from ClientTickCounter.getTotal() instead).

RenderFrameEvent.Pre handed ClientTickCounter.getGameTimeDeltaTicks(), which is the
frame-to-frame delta (~0.333 at 60 fps, i.e. nearly constant), not the 0..1 residual
inside the current tick. ClientTickCounter.getTotal() is ticksInGame + partialTicks, so
the value only changed once per game tick: everything derived from it (the zappy-seed
driven pattern wobble, sentinel rotation) advanced at 20 Hz no matter the framerate.

Pass getGameTimeDeltaPartialTick(false) instead - the same partial tick vanilla uses in
LevelRenderer and GameRenderer.
@c-Caelum

Copy link
Copy Markdown

Oh my god. This was one line change. Did you have to use claude???

@AlexProGear

Copy link
Copy Markdown
Author

Sorry, I REALLY didn't want to use my brain for this so I made it do PR for me :)
But basically yeah – it solves wobbly lines fps issue

@object-Object

object-Object commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Can you please:

  1. Make and test the corresponding change on Fabric - our Fabric build is working and tested in CI, so there's no reason you shouldn't be able to build it locally
  2. Include screen recordings in the description showing the before/after so we can see why this change is being made
  3. Rewrite or remove the comment in the code - the current one is quite clearly written by AI, and falls into a trap AI comments often encounter where it describes/explains the change that was made, not the code as it now exists

@Robotgiggle

Robotgiggle commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Also, you mention that you didn't change the code tied to the RenderLevelStageEvent (aka WorldRenderEvents.AFTER_TRANSLUCENT on fabric) because the value there is forwarded to a place where it doesn't get read.

Why not just remove the unused parameter entirely? Broken code shouldn't be left untouched just because its output isn't being used - that just adds confusion for people looking at the codebase in the future.

@IridescentVoid

Copy link
Copy Markdown
Contributor

@AlexProGear Could you rewrite the PR description to bring it in-line with the Contribution Policy/AI Policy and address comments raised?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📋 Backlog

Development

Successfully merging this pull request may close these issues.

5 participants