Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support >60Hz (2nd try) #585

Draft
wants to merge 34 commits into
base: master
Choose a base branch
from

Conversation

DanielGibson
Copy link
Member

@DanielGibson DanielGibson commented Jun 27, 2024

Based on @dezo2's patch (see also), but adjusted to make the framerate configurable with the com_gameHz CVar + my own fixes, also some ideas from Stradex' branch.

Very much WIP, and no guarantees this will work out


TODO

Potential bugs that have been observed with various attempts of running dhewm3 with >60Hz and that are worth testing:

  • Crashes with crane in Alpha Labs Sector 3 (should be fixed by patching GAME_FPS and GAME_FRAMETIME in the scripts)
    • Should really be fixed now, was actually something with SetSpeed() in C++ code that I first fixed incorrectly
  • Chaingun shooting too fast (should be fixed by patching CHAINGUN_FIRE_SKIPFRAMES in script)
  • Flickering particles (happened with com_fixedTic -1, might not be relevant here?)
    • Haven't seen this issue so far (and no one else has reported it either)
  • Switching Mods/game modes can cause black menu for multiple seconds to minutes
    • I can still reproduce this bug. I think it's related to counting tics (com_ticNumber?) and then multiplying it with USERCMD_MSEC and comparing the result to Sys_Milliseconds() or something, combined with one mod having a different com_gameHz value than the other, so tics are longer/shorter. Needs more investigation..
      Should be fixed
  • RoE gravity gun only works after 2 minutes or so (haven't tried this yet, might be similar to the previous one)
    • I couldn't reproduce this with my branch, if anyone runs into this problem please report!
  • I found if I tweak the fps higher than the monitor refresh rate, NPCs and mobs in-game will no longer interact with stairs. I think this is not a bug related to this fork, cause I found it in BFG Edition too. (again from same bugreport on simonedibilio's branch - this sounds extremely weird)
    • Update: I don't think this has anything to do with monitor refresh rate, but it seems to happen at >= 250fps (and at >= 200fps or so it already looks quite twitchy). I can reproduce it, see first level T. Washington and Mars Sec Anthony, in section between overhearing Betruger and the restroom door
    • According to dezo2's comment this still happens, esp. at framerates where the frametime is no integer (e.g. with 240fps it happens, with 200 not)
      • It also happens with 333fps (3ms frames) and 250fps (4ms frames), so I don't think this is directly related to integer frametimes, though it could be that at <= 5ms it generally starts to get real glitchy
    • Fixed!
  • Savegames don't work, "Script checksum didn't match"
    • Fixed as far as possible: This update will break savegames, i.e. loading old savegames will make you restart the level the savegames is from. This is due to changes I had to hack into the scripts, can't be avoided.
      But at least (unlike in Stradex/simonedibilio's branch) they don't break every time you change com_gameHz, so from now on they should be stable again.
  • the console cursor (and probably other cursors?) also changes the flashing frequency with different values for com_gameHz
    • fixed
  • The d3xp grabber feels worse at high FPS somehow, for example in that bossfight in erebus2. the grabbed things slow down way too fast and sometimes fall down before being in front of the weapon where they should be held for some time
    • fixed
  • "I saw maybe 2 or 3 physics related glitches where imp couldn't jump out of his hole due to a moving part in the path." "IIRC the glitches were in map recycling1/2 somewhere (can't remember) and in RoE map erebus4 in the beginning where that new imp couldn't leap down from the ceiling."
    • Can be reproduced, see comments below. Not sure if the physics are generally buggy now, or just behave a bit differently (so for example stuff falling to the floor lands at different places and possibly blocks monsters it didn't block before)
    • This seems to be rare enough, and usually the affected monsters turn up eventually, so I could live with this bug
  • Oxygen drainage too slow or fast (should be fixed by modifying the "air" code in idPlayer)
    • Mostly fixed, still TODO: changing com_gameHz while Oxygen is active is buggy
  • Make sure that game frames aren't longer than 17ms, because apparently not only too short frametimes, but also too long frametimes can screw up physics. Just call idGame::RunFrame() multiple times if com_gameHz is too low
  • It would probably make sense to check at runtime if the configured com_gameHz can be reached (and is not slowed down by vsync or simply a slow computer), and show a warning or something if it can't, telling the user to configure a lower value or otherwise fix their settings
  • Getting stuck in delta4 at a fence in the gateway at the very beginning, at >= 173fps (see this comment)
  • Player sometimes getting stuck at stairs (the higher the FPS the worse it is), for example at beginning of hell1, see this comment
  • The Mancubus in hell1 behaves weirdly at 480fps, and "jumps a lot after killing it" (same comment as previous item)
  • Multiplayer must be tested (do clients and server all running at different com_gameHz work?)

@DanielGibson
Copy link
Member Author

This time I was lazy and kept USERCMD_MSEC and USERCMD_HZ around, just turning them into #defines that alias global variables storing the current values.
Not sure how great that really is, but at least it reduces the amount of changed code significantly, as most (but unfortunately not all) code using USERCMD_MSEC/HZ just continues to work without modification.

@DanielGibson
Copy link
Member Author

Ok, I think I have fixed the "Switching game modes can trigger a fake black screen" issue, by fixing how com_frameTime is calculated.
I couldn't reproduce the issue with the grabber either (after the fix, didn't try before).

At least on my (Linux) PC, the framerate is extremely stable now (with vsync disabled), but not at the rate configured with com_gameHz, but the next multiple of int(1000/com_gameHz), e.g. 63fps for 60hz, 125fps for 120hz, 143fps for 144Hz, 250fps for 240Hz.

I guess I should try using floating point (double) times at least for the async tics, but probably I should also try how that all works on Windows and how it interacts with vsync.


However, all this somehow feels wrong: I can't find it right now, but I think there are comments in the code stating that the tics are supposed to be incremented at 60Hz, and that this is supposed to be independent of the render framerate.
But OTOH, they obviously didn't implement (or at least ship) that in Doom3, so maybe they discarded that idea and just forgot to remove some stray comments.

On the other other hand, I don't see the point of that whole async thread for incrementing tics, no matter if the renderer is running in sync with the game or not.
If one wanted to use threads there, it would probably make more sense to run the gamecode and the renderer in different threads, than having an additional thread just for incrementing a counter every 16ms...

@j4reporting
Copy link

the console cursor also changes the flashing frequency with different values for com_gameHz

@DanielGibson
Copy link
Member Author

the console cursor also changes the flashing frequency with different values for com_gameHz

true, I've also noticed this already, will add it to the list (I also know why it happens, just doing other changes first)

@DanielGibson
Copy link
Member Author

DanielGibson commented Jul 2, 2024

I've pushed lots of changes in the last hours.
One thing is that I rebased this on the soft-particle branch, so both features can be tested together (I'll post Windows binaries later).

Another is that the framerates should now be very close to what's configured with com_gameHz (unless VSync or a slow computer slow it down), because the timing of frame starts is now done with much more precision than full milliseconds.

Most times in the game and engine still use full (integer) milliseconds, like before, so I wonder if this causes any issues or if starting the frames at the correct time is good enough for things to run smoothly.

@j4reporting
Copy link

looks goot so far, played up to 1st airlock in mars city underground.

There is an issue with the air supply though. It can be manipulated by changing com_gameHz while being outside

com_gameHz 500 -> cycle airlock then set com_gameHz to a lower value
com_gameHz 20, then again to 10 > 4000

changing com_gameHz to a higher value than configured has ofc the opposide effectl

image

@DanielGibson
Copy link
Member Author

(rebased this branch again, to current master, so it includes the fix for #587)

I'll look at the oxygen issue.
I don't think it's critical though, changing com_gameHz while outside is quite an edge-case ;)

@DanielGibson
Copy link
Member Author

Thanks for testing by the way, I appreciate that you're doing this, and how thorough you are! :)

By the way, does dhewm3 at higher framerates feel smooth?
(Or does your display only support 60Hz anyway so there's not much visual difference, unless a bug introduced additional stuttering?)

@j4reporting
Copy link

It was a coincidence, I changed the setting while i was still in the airlock, but after I already initiated the airlock cycle.
I wonder what other scripted/timed events could be prolonged in this manner?
I guess in the end you'll need a fps independent internal value/timer.
THe same happens with loading a savegame craeated with a different com_gameHz setting.
maybe keep a 60 FPS value around and save only this value in a savegame.

Desktop is set to 144Hz. I would not want to go back to 60Hz.
Higher FPS feel ok. No stutter, nothing feels very good so far. Tested on WIndows with 144Hz and tied 250 for a while as well. Ofc not much is happening at this stage of the game.
Unexpectedly, I had some time to spare, because the Austria - Turkey game was only on a strangly colored pay-TV channel. Sometimes it's an advantage to follow a game via audio stream only.

@DanielGibson
Copy link
Member Author

I guess in the end you'll need a fps independent internal value/timer.

I do, it's just that some code (incl. some scripts) uses the game tics that are incremented each frame, and at a higher framerate they increment faster..
The fixes so far have been to at least scale the amount of tics a function waits (or sets as a limit, for example for "then I'll be out of oxygen") by com_gameHz/60 (=> for 120fps wait for twice as many tics), but as you noticed if com_gameHz are modified after such a "target tic number" has been set, you get glitches.

But maybe I could change the logic to use a floating point tic number and then scale the added value each frame (at 120Hz add 0.5 instead of 1 tic), so the target tic would remain the same.. I just hope this isn't used by too much code besides the oxygen.

Or I'd have to re-evaluate going back to 60Hz tics for the game code and only render at a higher framerate, but I'm not sure if this is feasible (and if there is even a point to that: what use are 120fps if nothing changes every second frame? but maybe something can be done without increasing the tic, I'll have to look at how the prediction code for multiplayer works, it might be related to this..)

nothing feels very good so far

why not?

@j4reporting
Copy link

nothing feels very good so far

why not?

aaaaah. damn.

No stutter, nothing. Feels very good so far.

@DanielGibson
Copy link
Member Author

Here is a build for Windows: dhewm3-1.5.4pre-highfps_win32.zip
Note that it does not support any mods for now, only the base game and RoE.

This contains both the soft particles changes and this high FPS stuff.

Other new features:

  • r_displayRefresh allows setting the display refresh rate for real fullscreen mode (unless you're using Wayland, it doesn't support that)
  • com_showFPS can now be set to 2 to also show avg/min/max frame times in ms

@DanielGibson
Copy link
Member Author

DanielGibson commented Jul 3, 2024

Based on the comment from @dezo2 at #250 (comment) I tested d3xp Erebus4 and yes, that new Imp near the start of the level definitely doesn't spawn (or drop?) like it should when running at 120 or 144 or 240 fps (it still seems to work fine at 60).

It should come out of the black hole that arrow is pointing to:
d3noimp

It eventually is there, at least if I stand directly below that hole, but at 60fps it drops down that hole pretty soon after that panel from the ceiling falls down after entering the room.

Something else I noticed is that things completely fall apart when I change com_gameHz while the game is running (loading a savegame again fixes it).
No matter if I increase or decrease it, and if it's only by a little bit (like 120 -> 121), I get weird moving double images (note that despite the muzzle flash I'm not currently shooting):
d3double

No idea what's going on there, or if the issue is in the scripts or C++ code or whatever.
UPD: That double vision problem seems to be related to me messing with the slowmo state in idGameLocal::SetGameHz().
Still no idea about the Imp (only thing I noticed is that it most probably does spawn, as I heard it hissing).

@dezo2
Copy link

dezo2 commented Jul 3, 2024

I got finally some time before going back to work so I just want to thank you Daniel for working on this. I tested the base game briefly and the game movement seems fine. What I found so far using com_gameHz 200 and 240 with VSync on a 240Hz VRR monitor:

The crane at the start of map alphalabs3 doesn' t come up when I press the button and crashes the game (something about binding an object to itself). Which is strange because the chaingun firing rate is OK, so the scripts should be adjusted.

There is a slight problem with 240Hz using VSync in cutscenes, where the audio sync (lipsync) drifts after a while even with default com_fixedTic 0. This is minor and can be fixed by running the engine at 200Hz (5ms int) - you can test it with map mars_city2 at the start, the survivor up the ladder there speaks relativelly long.

I also noticed very brief frame skips when the Hz does not match the integer framerate like 240Hz and again they dissapear when using 200Hz or by setting com_fixedTic 1. The frequency of the skips seems to match the difference between int and float frametimes so at 240Hz 4.1666ms they are realtivelly far between.

The imp in RoE should come down if you kick the panel under the hole - seems like the spawn is somehow tied to the panel position as it lands differently when it falls down before the imp at FPS >60.

I will continue testing when I have some time.

@dezo2
Copy link

dezo2 commented Jul 3, 2024

The crane crash is not caused by the scripts after all, I tried it with my old code and it needs the SetSteerSpeed adjustment in Physics_AF.h, it was this line in the code:
void SetSteerSpeed(const float speed) { steerSpeed = speed * 60.0f / USERCMD_HZ; }

But I thought you did something similar it in your new code, so not sure what's going on.

@DanielGibson
Copy link
Member Author

DanielGibson commented Jul 3, 2024

Thanks for testing!

seems like the spawn is somehow tied to the panel position as it lands differently when it falls down before the imp at FPS >60

Hmm interesting theory - maybe the panel blocks the spawn position or something?

SetSteerSpeed(const float speed) { steerSpeed = speed * 60.0f / USERCMD_HZ; }

I just realized: I replaced that code with
SetSteerSpeed( const float speed ) { steerSpeed = speed * gameLocal.gameTicScale; }
but gameTicScale is gameHz/60 (the factor for "how many more tics do I need to wait for the same amount of time") - so it must be steerSpeed = speed / gameLocal.gameTicScale; instead.
Same for STOP_SPEED

@DanielGibson
Copy link
Member Author

DanielGibson commented Jul 3, 2024

Just pushed a commit that fixes the crane (I tested the crane and didn't have crashes with 120, 240 or 60fps)

@DanielGibson
Copy link
Member Author

DanielGibson commented Jul 4, 2024

Updated Windows build: dhewm3-1.5.4pre-highfps2_win32.zip
Most important fixes since last build: Crane works now, so does changing framerate in d3xp, cursor blinking speed is normal at all framerates, com_showFPS 2 shows avg/min/max frame times of all frames in last 0.5 seconds.


By the way, some suggestions for testing:

  • Don't forget to increase the framerate ;)
    • In the new menu, which can usually be opened with F10, it's under Game Options -> Framerate
    • Enable com_showFPS to see if the game actually runs at that framerate (VSync might prevent it)
    • You don't necessarily need a display that supports >60Hz. You should not see much of a difference then (compared to how dhewm3 looked/ran before these changes), but most bugs should also be visible when running (without VSync) with 120 (or 144 or 240 or whatever) FPS on a 60Hz screen.
    • And of course testing if 60fps still behave like before is also valuable
  • Save frequently (maybe even increase com_numQuicksaves) and if you run into a bug, make backups of the last one or two quicksaves, which can hopefully help to reproduce it
  • Please report the map an issue happens in, ideally take a screenshot of the place, and run getviewpos or where in the console (and report the output) - it will print at what coordinate in the level you are.

@dezo2
Copy link

dezo2 commented Jul 5, 2024

Tested RoE yesterday with com_gameHz 200 and was able to run from start to finish without problems (apart from that one blocked imp in erebus4). No crashes or game breaking bugs, enemies went up/down the stairs to find me, grabber/slowmo/oxygen/enviro suit/machinery/elevators/bridges all worked as expected, so to me the code is already very usable. I will also try the base game but don't really expect any major problems there. BTW thanks for the F10 hidden menu tip, didn't even know about it.

As for the second blocked imp, it's in the base game, map recycling2 (-494.73 1410.72 -11.75) 190.2, right after interaction with the left screen. Imp should jump out of his closet, but with FPS >= 85 (11ms) he will be blocked with a panel he tried to kick out. So similar situation to than one in RoE. Not sure if this can be fixed without breaking something else in the physics code.
QuickSave.zip
shot00002

@Terepin
Copy link

Terepin commented Jul 6, 2024

I just wanna point out that the original Doom 3 can now run at higher fps than BFG edition. What a time to be alive in.

@dezo2
Copy link

dezo2 commented Jul 6, 2024

I may have found a fix for the blocked imps and potentially other enemies hidden behind wals and panels. This was caused by the STOP_SPEED in physics being too low for higher FPS, so by replacing a line in the source files Physics_RigidBody.cpp (base and d3xp):
const float STOP_SPEED = 10.0f / gameLocal.gameTicScale;
by this line:
const float STOP_SPEED = 10.0f * gameLocal.gameTicScale;
all the blockages seem to be fixed including the imp in RoE erebus4 and the one I posted yesterday. This should also fix the wild enemy ragdols and panels movements after explosions etc. Now at 200 FPS they all look similar to 60 FPS. Hopefully this won't break anything else. I will continue testing with this but I think this can be it.

@dezo2
Copy link

dezo2 commented Jul 6, 2024

Crap, celebrated too soon. It works with 200 FPS but not with 144 or 120. The physics code is seriously weird. The STOP_SPEED has to be the culprit for these problems.

This fixes the issue of scaled frametimes in d3xp.

As for some reason the events defined in C++ code must be defined again
in scripts (script/doom_events.script), I had to add a little hack to
the script compiler to inject the definition for getRawFrameTime()
(it's done when the getFrameTime definition is parsed)
also do that when loading a savegame, so those values are correct
even if com_gameHz has a different value than it had when saving
It used to be set with just
  com_frameTime = com_ticNumber * USERCMD_MSEC;
where com_ticNumber is incremented in each
idCommonLocal::SingleAsyncTic().
That worked well enough when USERCMD_MSEC was a fixed constant,
but now changing com_gameHz could cause com_frameTime to decrease, which
leads to all kinds of funny glitches like fading into the main menu
when switching mods taking way too long, resulting in a black screen
(when the mod has higher com_gameHz => lower USERCMD_MSEC).
So now this is done in a function with slightly more logic that
adds to com_frameTime instead of recalculating it with a multiplication.

I also changed how SingleAsyncTic() (or idCommonLocal::Async()) is
called, by using a proper thread instead of an SDL_Timer.
This gives me more control over it so it can be more precise (and, as
far as I can tell, it is)
So far dhewm3 ignored that CVar, now it tries to set that refreshrate

Also display the current refreshrate in the Dhewm3SettingsMenu, but
no way to configure it there yet.
Sys_MillisecondsPrecise() returns the milliseconds since start of the
engine as a double, so it can also represent micro- or even nanoseconds.
Sys_Milliseconds() now just returns that value casted to unsigned int,
for backwards compatibility.

Sys_SleepUntilPrecise( double targetTimeMS ) sleeps until
Sys_MillisecondsPrecise() returns a value >= targetTimeMS.
It aims to have a precision of about 0.01ms (10usec), and achieves that
by using a busy loop (with pause instructions) for the last 1-2 ms
by using Sys_MillisecondsPrecise() and Sys_SleepUntilPrecise()
and by adding com_preciseFrameLengthMS (as float instead of int).

Renamed com_gameFrameTime to com_gameFrameLengthMS, so it's less
confusing with com_frameTime (which is the timestamp of the current
frame, not its length)
for the nsec part of struct timespec a simple long is enough
instead of the ones from parms, as they can be different
also made it more precise by using floats with Sys_MillisecondsPrecise()
instead of ints with Sys_Milliseconds()
it uses com_ticNumber to decide when to (not) draw - that must be scaled
for shorter tics (at higher FPS)
turns out ResetSlowTimeVars() also does things one absolutely does
*not* want at that point, like resetting frame counters and times
that are *always* used, no matter if slowmo is enabled or not
Smoothes things out a bit, but also makes deviations more visible
in the min/max frametimes of com_showFPS 2
in dezo2's code it was
SetSteerSpeed(const float speed)
{ steerSpeed = speed * 60.0f / USERCMD_HZ; }

I replaced it with `steerSpeed = speed * gameLocal.gameTicScale;`
but gameTicScale is gameHz/60, not 60/gameHz, so it must be
`steerSpeed = speed / gameLocal.gameTicScale;`

Same for STOP_TIME.
turns out it should be `10.0f * gameLocal.gameTicScale` instead of
`10.0f / gameLocal.gameTicScale` after all, probably, at least it seems
to work better according to
dhewm#585 (comment)
(Doesn't fix the issues with all framerates though, apparently
 200fps work better now, but 144 or 200 don't)

Moved the calculation into a function so if further adjustments are
needed it's easier to do
in preparation of trying out TDM's fixes
they have *lots* of changes to physics code, partly to support mantling
and movement in water, so it's not that easy to tell which are relevant
for us. These are in things already identified as unstable here, so
I'm hopeful that they help..
many thanks to dezo2 for suggesting these fixes!

I think in the StepMove() case it's more of a hack than a fix - *maybe*
the correct thing would be to accumulate the deltas over multiple frames
when you got blocked, or something like this? - but as long as it
doesn't break anything else I don't care..
at high framerates, grabbed stuff often fell down before being in front
of the Grabber (where it could be thrown away again)

Thanks to @dezo2 (again!) for pushing me in the right direction!
based on a patch from @dezo2, who took the general idea from D3BFG

this works around the problem that for most framerates, with integer
frametimes, gameHz * frametime doesn't add up to 1000ms
(e.g. 120Hz * 8ms = 960ms), by making some frames a millisecond longer
it's still possible, but a warning is shown and the setting in the
menu only goes up to 250 now.
Apart from physics getting wonkier the higher the framerate is, >250Hz
breaks slow motion effects in RoE (d3xp), because it divides frametimes
by four, and as the frametimes are integers, for >250Hz they're <= 3,
so divided by four that's 0, which isn't good.

Furthermore, I moved that setting to the Video settings, so it's closer
to related settings like VSync and display refreshrate.
While at it, I made sure that `com_showFPS 2` can be configured in the
menu as well (it still assumed it's a bool).

Last but not least I fixed a misleading variable name in Win_InitTime()
to prevent them from jumping so much at high framerates
The changes in this branch breaks the Game API,
so it won't be in a 1.5.x release.
it already mostly worked, but picking up oxygen bottles gave too little
oxygen at >60fps and changing com_gameHz while being "outside" would
screw up the remaining oxygen and the implementation was needlessly
invasive anyway.
I reverted most changes, turned idPlayer::airTics into a float (so they
are now virtual 60Hz tics instead of actual number of tics) and adjusted
the code that increases or decreases the airTics (idPlayer::UpdateAir())
to scale the values added/subtracted accordingly.
Apart from that it's only a few changes to accomodate the fact that
it's a float now.
it only returns Sys_MillisecondsPrecise() casted to unsigned int anyway
@DanielGibson
Copy link
Member Author

FFS why do those expire :-/

I rebased to the current master code to trigger a new build, so downloading artifacts from those builds at https://github.com/dhewm/dhewm3/actions?query=branch%3Akickstart-my-hertz should work again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.