Eidolon requested to merge ShaderWraith/SRB2:uncappedrevengeance into next

It's finally here. This is a joint effort between Sal and I. I believe this to be almost complete, but some Lua mod and multiplayer testing can't hurt; I have joined and played on some MP servers with mods and it seems that everything works perfectly.

If you are a player: this is not Uncapped Plus and does not include its non-uncapped related changes. This is an evolution of the original uncapped code that was built on.

This MR changes the following:

  • Refactored game loop to support arbitrary framerate
    • A new consvar fpscap is added, which provides three modes: match refresh rate (-1), uncapped (0), and arbitrary caps between 35 and 300.
    • This consvar is available under Video Options.
    • When fpscap is set to 35, the game will fall back to the original sleeping behavior and turn off interpolation.
    • Dedicated mode preserves the original sleep behavior in all cases and will never interpolate.
    • The draw behavior of the framerate counter has changed to accommodate this; it will not be displayed as a ratio if the framerate is uncapped.
  • To produce a smooth image without altering existing game logic, parts of the game state are interpolated in-between tics.
    • Map objects are interpolated by capturing the position and orientation of every object in the level prior to prethink hooks and mobj thinkers, and using the oldstate to interpolate inside the renderer. This guarantees minimal alteration of mobj-related code. The relevant code is categorized as "Mobj Interpolators" and is tracked independently in r_fps.c. (I wanted to make oldstates stored entirely separate from mobjs, but currently there isn't a straightforward hashmap to use for data storage, so old and old2 states exist inside the mobjs)
    • "Level Interpolators", like Mobj Interpolators, capture a slice of level state for level-related thinkers in a separate structure, and are also updated independently from ticking. Unlike Mobj Interpolators, Level Interpolators do alter the level state between tics, as avoiding this would require significant changes to the rendering code. This state is restored before any prethink hooks and thinkers are run, making it transparent to game logic.
  • Menus, finales, and intermissions have been altered to support variable framerates, rather than using interpolation
    • This is because our menus don't consistently distinguish between ticking and drawing, so in places where time-based logic is run during drawing, they have been updated to support variable framerates. The result is quite nice in the level platter!
  • HUD code has seen some adjustments to support variable framerate rendering, for similar reasons to menus
    • HUD hooks will have their draw calls buffered into drawlists on tic draws, and the drawlist will be used to redraw them, instead of calling the hook at full framerate.
    • Drawerlib getDeltaTime function returns the previous frame's time elapsed in tics. Use this for animations now instead of leveltime, as it will be necessary for enabling unbuffered drawing in your hook (when that becomes an option). getDeltaTime has been removed for this MR and another solution will be provided later. HUDs will continue to draw at 35hz as before.
  • To support instant motions without erroneously interpolating to the destination, P_TeleportMove is now DEPRECATED from Lua.
    • Please use P_SetOrigin for non-interpolated instantaneous movement and P_MoveOrigin for interpolated motions.
    • All uses of P_TeleportMove in C code have been adjusted to use one or the other as appropriate.
  • perfstats includes the interpolation fraction of the frame as a number from 0-100.
  • perfstats includes the estimated visual lag added by interpolation
  • timescale cvar added which scales the executed ticrate. When set to a value other than 1.0, interpolation will always be enabled, even at fpscap 35. This is a netvar and is not saved. Have fun. Note that currently this will affect the frequency that SRB2 pumps window and input events, so be careful setting it to low values.
  • Internal engine timing has been rewritten to track time inside the game rather than in system backends.

Some compatibility notes for Lua authors:

  • P_TeleportMove triggers a P_MoveOrigin call as most Lua mods used the former for positioning special effects. This means, if you were using it for an actual teleport, your mod will have interpolation artifacts and you should switch it to use P_SetOrigin.
  • Mobjs spawned from P_SpawnMobjFromMobj will copy the target mobj's interpolation state as well. If the target mobj is a player, the initial interpolated angle will be the old drawangle of the player. This is for compatibility with mods which spawn afterimages and other similar effects with this function (e.g. Adventure Sonic). If you notice interpolation artifacts from mobjs spawned this way, please let us know. This is a tricky problem to solve and we're trying to introduce this without adding a bunch of new Lua functions just to deal with interpolation.
  • Setting scale on a mobj will skip scale interpolation. Setting destscale will not. This is inline with existing behavior, just applied to interpolation state as well.
  • You can use timescale to test your mod for interpolation artifacts.

As a caveat, interpolation necessarily introduces at most 1 tic of visual latency to the game. This latency is not present if fpscap is 35, and will be unnoticeable for most players otherwise. There are also some visual artifacts when mobjs change states, which is most noticeable with rain drop splashes.

Testing build (Updated 2022-05-03)

Edited by Eidolon

Merge request reports