Jump to content
Search In
  • More options...
Find results that contain...
Find results in...
Linguica

Am I crazy or does [Chocolate] Doom render each tic twice

Recommended Posts

Am I going crazy or does the Doom engine render the entire scene TWICE for every tic?

I will paste in some simplified code showing the important part that I am looking at:

D_DoomLoop()
{
	TryRunTics();
	while(1)
	{
		TryRunTics();
		D_Display();
	}
}

TryRunTics()
{
	NetUpdate();
	counts = maketic - gametic;
	if (counts < 1) counts = 1;
	while (maketic < gametic + counts)
	{
		NetUpdate();
		if (time for new tic)
			return;
		I_Sleep(1);
	}
	while (counts--)
	{
		RunTic(); // game logic
		gametic++;
	}
}
    
NetUpdate()
{
	if(time for new tic)
	{
		BuildNewTic(); // write input data for next tic
		maketic++;
	}
}
Follow along and see for yourself.

Start out with maketic and gametic as 0. DoomLoop() runs TryRunTics() first.

0 < 1, so we get caught in the loop and sleep repeatedly.

Finally enough time has passed and NetUpdate() builds a new tic and increments maketic.

Since it's time for a new tic, we break out of the sleep loop and return to D_DoomLoop().

Now we finally enter the main while(1) loop.

Enter TryRunTics() again.

Now maketic = 1, gametic = 0 so we skip the sleep loop and do RunTic() and increment gametic.

Return to the main loop. gametic = 1, now run D_Display(). First tic finished and rendered!

Enter the main loop again. Enter TryRunTics() again. maketic = 1, gametic = 1, so we enter the sleep loop again until its time for a new tic.

Do BuildNewTic() and increment maketic, return back to the main loop. maketic = 2, gametic = 1.

NOW WE CALL D_DISPLAY() AGAIN EVEN THOUGH GAMETIC IS STILL 1!

And so on and so forth. It looks like the game loops twice through the while(1) loop for every tic. The first time through, TryRunTic() actually processes all the game logic and increments the gametic, and then the game updates the display. Then the second time through the loop, TryRunTic() sleeps until it's time to collect the player input for the next tic, builds the input for the next tic... and then the game updates the display again, even though the next tic hasn't been processed yet.

I put a bunch of fprintfs into Chocolate Doom to see if this was right, and it seems like it is:
Spoiler

doomloop loop
gametic = 18, maketick = 19
tryruntics
netupdate
call to runtic
runtic
g_ticker
run thinkers
gametic++
netupdate
d_display
renderplayerview
netupdate
renderbspnode
i_finishupdate
actual sdl draw

doomloop loop
gametic = 19, maketick = 19
tryruntics
netupdate
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
build new tics?
buildnewtic
d_processevents
m_ticker
g_buildticcmd
returning
d_display
renderplayerview
netupdate
renderbspnode
i_finishupdate
actual sdl draw

doomloop loop
gametic = 19, maketick = 20
tryruntics
netupdate
call to runtic
runtic
g_ticker
run thinkers
gametic++
netupdate
d_display
renderplayerview
netupdate
renderbspnode
i_finishupdate
actual sdl draw

doomloop loop
gametic = 20, maketick = 20
tryruntics
netupdate
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
sleeping
netupdate
build new tics?
buildnewtic
d_processevents
m_ticker
g_buildticcmd
returning
d_display
renderplayerview
netupdate
renderbspnode
i_finishupdate
actual sdl draw

So is this just a Chocolate Doom thing, or did the original Doom literally redraw the whole screen twice every tic?

Share this post


Link to post

1. If so, that's incredible. (...rushes home to double his renderer's speed - wheee!)

2. So, what's up with all these new Doom insights, Linguica? Are we going to see a new thread:

New Source Port: "The Doomling"?

Share this post


Link to post

Vanilla Doom in DOS was locked to the VGA refresh rate via the I_WaitVBL routine. It only ever precisely rendered one frame for each page flip.

This was removed in Linux Doom, so ports based on Linux Doom may render the same tic any number of times - their refresh FPS is not latched. The game loop runs at 35 FPS; the screen is drawn as quickly as possible. Chocolate Doom is modified to try to sleep til the next time it should update but I will note it's very inaccurate because it relies on SDL_GetTicks, which on Windows has a 10 ms *minimum* granularity.

Share this post


Link to post

It might, and might not.

What that logic is doing is keeping the display rate, network rate, and game logic rate independent.

If the network is slow, then maketic is going to be held up waiting for the slow player.

If your display is slow, then two or three tics might pass during a display cycle. But the game logic is correctly preserved.

If your display is very fast, then it could redraw several times before the next tick. The game logic is preserved.

The code could have froze the screen waiting for the next tick, but the choice was to redraw the screen. There can be things on the screen that do not depend upon the gametic (not many, but a few, I am not going to go searching for them).

Share this post


Link to post
Quasar said:

Vanilla Doom in DOS was locked to the VGA refresh rate via the I_WaitVBL routine. It only ever precisely rendered one frame for each page flip.

This was removed in Linux Doom, so ports based on Linux Doom may render the same tic any number of times - their refresh FPS is not latched. The game loop runs at 35 FPS; the screen is drawn as quickly as possible. Chocolate Doom is modified to try to sleep til the next time it should update but I will note it's very inaccurate because it relies on SDL_GetTicks, which on Windows has a 10 ms *minimum* granularity.

So it's (probably) a Linux Doom bug accidentally preserved in Chocolate Doom, then? Noted.

FWIW, I tried making a short demo in DOSBox doom2.exe and then adjusting the CPU cycles so that a -timedemo played back in almost real time. (The issue only appears to happen in Chocolate Doom during normal play / normal demo playback, not during a timedemo.) If the engine was in fact rendering each tic twice during normal play, then there should have been noticeable chop playing back the demo with -playdemo. However, it played fine. I also ran it with -debugfile and the resulting text file showed it wasn't dropping frames. False alarm I guess.

Share this post


Link to post

Funny! I have inserted a "printf("tic %d\n", gametic);" into R_RenderPlayerView() and indeed it renders twice per tic. However, wesley's explanations makes sense.

Share this post


Link to post

I looked at TryRunTics, but had to look in heretic and Boom.
The logic is not quite the same as described.
In the version I saw it only takes a Return stmt for a specific condition.
Approximately (from memory). I cannot remember (or find) how to make those CODE markers work!

while( ... )
{
 ...
   // Don't get stuck here, let the menus work.
   if( I_GetTime()/TICRATE + exptics - gametics > 20 )
   {
      ...
       Return
   }
}
So it only uses that return stmt if it is going to be stuck in the
loop long enough to interfere with menu usage. To make the menu's
display work it requires redrawing the screen often.

Share this post


Link to post
wesleyjohnson said:

...I cannot remember (or find) how to make those CODE markers work!

Use square brackets instead of GT LT:

 __        __
|            |
|    CODE    |
|            |
 --        --

    printf("ASCII art sucks...\n");

 __        __
|            |
|    /CODE   |
|            |
 --        --

Share this post


Link to post
wesleyjohnson said:

If your display is very fast, then it could redraw several times before the next tick. The game logic is preserved.


Yeah but why waste the time? The only reason to run the renderer again is that something changed. If the menu & game world are tied to TICRATE (which I'm pretty sure they are, because they both tick), there won't be any changes.

wesleyjohnson said:

There can be things on the screen that do not depend upon the gametic (not many, but a few, I am not going to go searching for them).


I'm willing to eat my words, but I bet you wouldn't find any if you did.

I'll even go further and say that if you do, it doesn't have an impact on gameplay or compat, and it could be locked to TICRATE.

To be clear, if you're talking about network code or non-gameplay/display stuff, then yeah sure it can run between TICs (and probably should). But there's no reason to run menu or game code when a TIC has not elapsed, unless you're interpolating frames.

wesleyjohnson said:

If the network is slow, then maketic is going to be held up waiting for the slow player.


Yeah and here I think it would make sense to at least tick the menu and HUD.

---

Fundamentally, the problem is just some complicated logic gone wrong (the Doom event loop is hard). When TryRunTics builds a command, it bails and D_DoomLoop renders a frame. You could either hack in a check for a changed gametic (ugh static variables) or you can make TryRunTics a little smarter and have it I_Sleep/NetUpdate until a TIC elapses and a command is built; then it can enter the while(counts--) block.

I don't really understand why there's a TryRunTics call outside of while(1) in D_DoomLoop, and I also don't understand why TryRunTics bails when it builds a TIC. It doesn't mess up the math to change this stuff.

Share this post


Link to post

Trying to analyze what id did is like trying to criticize congress, easy but fruitless. Hindsight.

Likely that this escape was thrown in when they discovered something ...
like hitting the game pause key froze up the user interface
(menus, skull animation, etc..), or to keep their twirling disk activity icon going during game load, and maybe to
see error messages (depending on where they were displayed).

I have been rewriting network code the last week. Sure you can change things, but every time you run the risk of re-discovering a hack that depends on it being the strange way it was.
I spend considerable time finding and documenting such things in DoomLegacy.

Share this post


Link to post
Ladna said:

I don't really understand why there's a TryRunTics call outside of while(1) in D_DoomLoop

I do not know how that got there either, as it's not a part of most other source ports like BOOM but, I can tell you this - removing it causes Chocolate (anything - Doom, Heretic, Hexen, Strife) to crash.

I know because I tried to get rid of it in SVE while adding in frame interpolation.

Share this post


Link to post
Quasar said:

removing it causes Chocolate (anything - Doom, Heretic, Hexen, Strife) to crash.

I cannot confirm this. The only difference I see is that it wipes into the title screen.

Share this post


Link to post

It makes a little sense - TryRunTics() has to be entered twice each tic, once to wait for the next tic, and then again to actually update all the game logic to the next tic. If you tried to run D_Display() before ever having updated the game logic a single time, it's not hard to believe it could crash the game or otherwise do bad things.

Share this post


Link to post

See if your exit button glows before the end level wipe. I had to add code to do another frame paint on level exit to be able to see the exit switch light up. Might be related.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×