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

ebiten: deprecate FPSMode, undeprecate SetVsyncEnable, and add more APIs to control TPS/FPS #2338

Closed
hajimehoshi opened this issue Sep 19, 2022 · 37 comments

Comments

@hajimehoshi
Copy link
Owner

hajimehoshi commented Sep 19, 2022

FPSModeVsyncOffMinimum is quite odd since a) this affects TPS and b) this consumes GPU power unnecessarily (e.g. when a mouse is dragged).

To solve these issues, we have discussed at the Discord server and came up with these ideas, instead of introducing a new FPS mode like FPSModeVsyncOnMinimum:

  1. Undeprecate SetVsyncEnabled and deprecate FPSMode to make the API orthogonal with new APIs
  2. Add a new TPS mode SyncWithInput, with which Update is called only when inputting occurs. Draw's timing is also adjusted. When vsync is on, Draw is also called only when inputting occurs, but the maximum is limited by vsync. When vsync is off, Draw is not limited by vsync. This should be the same as FPSModeVsyncOffMinimum. As SetTPS affects Draw, this might be odd, but basically we cannot control the timings of Draw directly in the first place. This solves the problem a) by the clearner APIs and also b) partially by SyncWithInput with vsync-on.
  3. Add a new error value SkipDraw, which skips Draw (and the previous framebuffer is used for swapping framebuffers). This solves the problem b) by suppressing Draw (e.g. when a mouse is dragged).

@tinne26 @Zyko0 Any thoughts?

@hajimehoshi
Copy link
Owner Author

One point I'm not fullly convinced is that whether we can allow SetTSP(SyncWithInput) would affect the timing of Draw.

@tinne26
Copy link

tinne26 commented Sep 19, 2022

  • I assume ScheduleFrame() will also be deprecated? (Edit: oh, we still need it for SyncWithInput.)
  • Returning SkipDraw on Update() can be a problem if input is reworked in the future. In particular, if we ever get support for async input, having SkipDraw returned on Update() is a bit limiting (although not a critical issue). I dislike the error return because it connects update and draw, making them less orthogonal and more interlinked in a way. That said, I understand this simplifies the implementation significantly. The only other reasonable alternative I can come up at the moment would be a new SkipDraw() bool method that the Game type may optionally implement and that would be called once before each Draw(). It's not super clean either, but I like it better than the error return, I think it collides less with other parts of the engine.
  • There are a few edge cases for skipping draws that need to be clarified: possible interference from graphicsDriver.NeedsClearingScreen(), previous draw calls outside Draw(), having SetScreenClearedEveryFrame(true), etc. Basically, whether SkipDraw will always be respected (and Ebitengine handles the edge cases internally) or if the user should be prepared to draw anyway in some edge cases.
  • The addition of SyncWithInput is starting to overload SetTPS's function name. If we take a step back, we can reframe the model by saying that Update() can be invoked based on three types of events:
    • A timer or clock ticking: this is classic fixed TPS.
    • Immediately: this is what we are currently configuring with SetTPS(SyncWithFPS). If we were to use SetTPS(Minimum) or SetTPS(Immediate), though, that wouldn't make sense either because "immediate" refers to calling Update, not to the ticks per second. In fact, the ticks per second don't really exist in this context because ticks refer to a clock, and no clock is used here.
    • On input events: this is what we would add with SetTPS(SyncWithInput). This is again strange because we have no clock being used here, and "sync" also makes it sound like input would be something regular and periodic, when it's not. In fact, under this mode I would call updates "highly variable, unpredictable and unsynced", rather than "synced".

While I think it's ok to use SyncWithInput to avoid breaking compatibility in v2, I think the mechanisms to call Update() are clearly getting outside the scope of what we have been calling TPS until now. I think the timer / immediate / on_input_events model is significantly clearer and we should reconsider the API for v3. Additionally, this model would play better with additional types of events for calling Update(), like manual requests or multiple modes at the same type (e.g. timer + input, though this is admittedly quite strange).

Right now, users are led to think about how Update() is connected to SetTPS(), which may be connected to SyncWithFPS, which may be connected to SetFPSMode(), which may override TPS through VsyncOffMinimum. starts sweating. The suggested model of clock ticks / immediate / on_input seems much more sane. It's still not trivial because both update and draw can have some blocking before they are called, and therefore they mutually affect each other, but... despite that sounding strange to you, I personally think that's the inherent complexity that can't be removed if we want the engine to be flexible enough to cater to all the use-cases we have been discussing. I think that this reframed model would be a big step towards making the main loop easier to understand.

@tinne26
Copy link

tinne26 commented Sep 19, 2022

Hmm. Another behavior and implementation question for SkipDraw: if vsync is active but we skip the draw, we should sleep long enough so that we won't be able to run a new update and actually end up drawing on the same frame we were originally trying to skip, no? I don't even know if this is possible to do precisely, but if we add no sleeps, the SyncWithFPS mode could end up accidentally burning much more CPU than expected. We have to figure out and document whether we can end up drawing on the same frame that we tried to skip or if that's always prevented by the engine.

@hajimehoshi
Copy link
Owner Author

hajimehoshi commented Sep 19, 2022

The suggested model of clock ticks / immediate / on_input seems much more sane.

We can add SetTPSMode or other APIs without breaking the compatibility, right?

@tinne26
Copy link

tinne26 commented Sep 19, 2022

SetTPSMode() should be SetUpdateMode() instead, as immediate and on_input_events are completely unrelated to ticks and TPS. Ticks per second are one of the modes themselves. But then you have to leave SetTPS() only for when the update mode is "ticks per second". I haven't found any ergonomic way to express it in a single function, which is uncomfortable:

SetUpdateMode(ebitengine.UpdateModeTicksPerSecond)
SetUpdateMode(ebitengine.UpdateModeImmediate) // maybe ebitengine.UpdateModeNoWait is better?
SetUpdateMode(ebitengine.UpdateModeWaitForInput)
SetTPS(40) // only applies if mode is ticks per second

This doesn't break compatibility if SyncWithFPS is still allowed on SetTPS(), but it's weird. If update mode is UpdateModeTicksPerSecond and then we call SetTPS(SyncWithFPS), we need to change the mode implicitly and remember the last TPS value in case we later call SetUpdateMode(UpdateModeTicksPerSecond) again. And we only deprecate SyncWithFPS.

@hajimehoshi
Copy link
Owner Author

Thanks,

SetUpdateMode(ebitengine.UpdateModeImmediate) // maybe ebitengine.UpdateModeNoWait is better?

I'm not convinced with the name. Update is followed by Draw, but what is 'immediate'?

@tinne26
Copy link

tinne26 commented Sep 19, 2022

Immediate or no_wait means that when update can be called in the loop, it's called without any previous wait (either due to time or due to waiting for input events). But the loop of update + draw means you still have to wait for draw to be called before being able to call update again (as opposed to an infinite update loop). The fact that in this mode update is called exactly as many times as draw is another particularity that we may want to reflect in the name (this doesn't happen with the ticks_per_second mode). But this also happens with the wait_for_input mode. I simply didn't find any good way to refer to this in the name.

@hajimehoshi
Copy link
Owner Author

Would this be clearer?

SetUpdateMode(ebitengine.UpdateModeTicks)
SetUpdateMode(ebitengine.UpdateModeFrames)
SetUpdateMode(ebitengine.UpdateModeWaitForInput)

@tinne26
Copy link

tinne26 commented Sep 19, 2022

I don't really understand why is it called UpdateModeFrames. What definition would you give? In my opinion, the relevant part is that it doesn't wait for anything in particular before calling update. I don't see what "frames" is trying to indicate here. That it waits for the next frame to be drawn? Then that also happens for all the other modes. That's part of the loop structure, not the update mode.

@hajimehoshi
Copy link
Owner Author

A frame is a time unit to call Draw. (FPS is how many Draw is called per frame)

@hajimehoshi
Copy link
Owner Author

Immediately: this is what we are currently configuring with SetTPS(SyncWithFPS)

So I adopted the word Frame for it.

@hajimehoshi
Copy link
Owner Author

For Update,

  • Called based on a timer (Tick)
  • Called based on a refresh rate
  • Called unlimitedly (vsync-off)
  • Called based on user inputs

For Draw,

  • Called based on a refresh rate
  • Called unlimitedly (vsync-off)
  • Called only after Update with vsync limitation
  • Called only after Update without vsync limitation

So there are a lot of pairs, but these are not orthogonal...

@hajimehoshi
Copy link
Owner Author

Frame: Draw timing. This is based on vsync-on, vsync-off, or input (FPSModeOffMinimum)
Tick: Update timing. This is based on timing or frame (SyncWithFPS)

@tinne26 do we agree with these definitions?

@tinne26
Copy link

tinne26 commented Sep 19, 2022

A frame is a time unit to call Draw. (FPS is how many Draw is called per frame)

I assume you meant how many Draw()s are called per second.

So I adopted the word Frame for it.

The substitution makes sense in this context, but my point is that this context on itself, the idea of SetTPS(SyncWithFPS), is already technically dubious. Which is why I didn't use it in the new model.

For Update, [...]
* Called based on a refresh rate
* Called unlimitedly (vsync-off)

I don't agree with separating this as two cases. They are both the same, update doesn't wait. The only part that may force waits is vsync. If vsync is on, buffer swaps on draw will block. If vsync is off, no one will block and the loop will run as fast as possible.

For Draw, [...]

I don't know why you have 4 cases. To me, it's all one case: after the update part of the loop (and doesn't matter whether update has been called zero, one or twenty times), draw is called one time unless we are skipping the draw. Whether the buffer swap blocks or not depends only on the vsync configuration.

Frame: Draw timing. This is based on vsync-on, vsync-off, or input (FPSModeOffMinimum)

This definition is ok, but the problem is that this is something that we can estimate based on the refresh rate and we can measure in practice, but this is not something we can set directly. We do not use this value to wait, we do not create a timer to control it, so we shouldn't use it in contexts where we are setting values.

Tick: Update timing. This is based on timing or frame (SyncWithFPS)

A tick is a time-based "signal" that indicates us that we can call Update() again when we are using the UpdateModeTicks update mode. Talking about SyncWithFPS and frames here does not make sense to me, they shouldn't be directly related like this, so I disagree with this definition.

@hajimehoshi
Copy link
Owner Author

A tick is a time-based "signal" that indicates us that we can call Update() again when we are using the UpdateModeTicks update mode. Talking about SyncWithFPS and frames here does not make sense to me, they shouldn't be directly related like this, so I disagree with this definition.

My opinion is different: Tick is a time unit for one Update, and SyncWithFPS still makes sense. Tick might be or might not be a timer here.

@hajimehoshi
Copy link
Owner Author

Even with the 'immediate' mode, TPS (ticks per second) still represents how many times Update is called per second, whichever this value is useful or not.

@tinne26
Copy link

tinne26 commented Sep 19, 2022

Ok, now we are finally getting somehwere.

So, there are two options here:

  • If you accept that the time unit for updates with SyncWithInput is also a tick, then a tick can be constant and periodic or can be variable and unpredictable and it isn't really helping us classify anything actually.
  • If you don't accept that the time unit for updates with SyncWithInput is also a tick, then the term tick isn't also very helpful, as we still have to talk about update modes to cover the WaitForInput mode.
  • In both cases, we are using at least UpdateModeTicks and UpdateModeFrames despite both of them having "ticks", so we are making it even more confusing.

If you want to call any of those ticks, ok. Let's temporarily rename UpdateModeTicks to UpdateModePeriodic or UpdateModeTime to avoid ambiguity. The problem with ticks as you understand them is that they are not helping you solve the problem. They are not simplifying the solution models, they are only making solutions more complicated. Like, try to understand my proposed model without thinking about ticks at all. Let's assume you have UpdateModePeriodic and SetUpdatePeriodicity(duration) instead. Ticks are completely gone. There aren't a lot of pairs of anything.

@tinne26
Copy link

tinne26 commented Sep 19, 2022

Even with the 'immediate' mode, TPS (ticks per second) still represents how many times Update is called per second, whichever this value is useful or not.

No, in the immediate mode the TPS value is not used for anything.

@hajimehoshi
Copy link
Owner Author

Even with the 'immediate' mode, TPS (ticks per second) still represents how many times Update is called per second, whichever this value is useful or not.

No, in the immediate mode the TPS value is not used for anything.

I'm talking about the current situation (how Tick is used)

@hajimehoshi
Copy link
Owner Author

hajimehoshi commented Sep 19, 2022

If you want to call any of those ticks, ok. Let's temporarily rename UpdateModeTicks to UpdateModePeriodic or UpdateModeTime to avoid ambiguity. The problem with ticks as you understand them is that they are not helping you solve the problem. They are not simplifying the solution models, they are only making solutions more complicated. Like, try to understand my proposed model without thinking about ticks at all. Let's assume you have UpdateModePeriodic and SetUpdatePeriodicity(duration) instead. Ticks are completely gone. There aren't a lot of pairs of anything.

Hmm, ok. I'm fine with changing the definition of tick from Update to time, to avoid confustion. I don't want to deprecate SetTPS so far.

I'll try to read your suggestion more carefully later (as I'm gonna bed soon)

@hajimehoshi
Copy link
Owner Author

hajimehoshi commented Sep 19, 2022

My understanding is what you are suggesting is:

  • Change the definition of tick to be just a clock rather than the frequency of Update
  • Add these modes for Update:
    • SetUpdateMode(ebitengine.UpdateModeTicksPerSecond)
    • SetUpdateMode(ebitengine.UpdateModeImmediate)
    • SetUpdateMode(ebitengine.UpdateModeWaitForInput)
  • The current model (Update is called 0-n times and Draw is called 1 time, and this is called as a 'frame') is replaced with a new model (frame is a time unit for Draw. Update is independent from Draw).
  • But, Update should be able to determine whether Draw is called.
    • Calling SkipDraw() function is better than returning SkipDraw error.

Is that correct?

  • If vsync is on, is Update called per frame with UpdateModeImmediate? If so, isn't still UpdateModeFrame or something a better name?
  • If we want to achieve vsyncOffMinimum (Update and Draw are called at the minimum rate), what kind of API combination would work?

@tinne26
Copy link

tinne26 commented Sep 19, 2022

Yeah the first part is fundamentally correct. Only that I don't really say much about a "frame". For me, a frame is the time elapsed between draws or the resulting image created during that time. So, the current model also matches my understanding of frame. It's just that I don't use that value for configuring anything, so it's not really very relevant. Update is independent from Draw in the sense that the configuration of one doesn't affect the configuration of the other. They are not independent in the sense that they are both part of the same loop and are called sequentially, so delays and waits for one can also delay the other. But their configurations are independent, indeed.

For the second part, that's not quite right:

  • As I said, the update configuration is independent from frames or anything. So, vsync being on or off doesn't change anything about the update mode. Any combination is possible, but vsync being on or off only affects the draw part of the loop, and the update mode only affects the update part of the loop, so the configurations don't affect the code of the other part.
  • We would use vsync off and update mode wait_for_input.

@hajimehoshi
Copy link
Owner Author

I feel like your 'loop' is close to the current 'frame'.

We would use vsync off and update mode wait_for_input.

So even if vsync is off, Draw is called only after Update, if the mode is wait_for_input, right? It's because Update and Draw share the same 'loop' and the wait_for_input mode blocks Update. So the next question is, if TPS is very low like 1, is Draw affected?

@tinne26
Copy link

tinne26 commented Sep 19, 2022

In the case of wait_for_input, Draw is only called after Update both for vsync on and vsync off. TPS doesn't affect Draw or anything in wait_for_input mode. TPS only has an effect when the update mode is UpdateModeTicks or however we want to call it.

@hajimehoshi
Copy link
Owner Author

So SetUpdateMode would affect the frequency of Draw, which seemed a little odd.

@tinne26
Copy link

tinne26 commented Sep 19, 2022

It can be weird, I don't know. Here's the pseudo-code structure to make sure we are on the same page, I find it mostly reasonable: https://tinne26.github.io/ebi-main-loop/

@hajimehoshi
Copy link
Owner Author

So there can be a better name, like LoopMode or UpdateAndDrawMode 🤔 This is my current concern, but I might have more later. Let me revisit this tomorrow.

https://tinne26.github.io/ebi-main-loop/

Nice!

@hajimehoshi
Copy link
Owner Author

    vsync = graphics.IsVsyncEnabled() // assume true
    if not game.SkipDraw():
        game.Draw(screen)
        graphics.SwapBuffer(vsync) // vsync on, can block, no tearing
    else if vsync:
        sleepUntilFrameEnd() // prevent busy CPU loop

I think this can be slightly different, since even if Draw is skipped, swapping buffer should happen to wait for vsync correctly. My understanding is this:

    vsync = graphics.IsVsyncEnabled() // assume true
    if not game.SkipDraw():
        screen.Clear()
        game.Draw(screen)
    graphics.SwapBuffer(vsync) // vsync on, can block, no tearing

@tinne26
Copy link

tinne26 commented Sep 19, 2022

Sure, I mean, that part is unclear to me. If the buffer swap won't increase the GPU usage and serves as a wait, sure, that would be better. I don't know how it would be done in DirectX and Metal, but yeah, if that's possible it would be ideal.

Also, SetLoopMode sounds like it could work well, yes. We could even consider passing both the update mode and the vsync value at once, somehow indicating that both are relevant for the loop, while they are still their own thing each. This is not something one changes frequently, so this could make a lot of sense.

@hajimehoshi
Copy link
Owner Author

hajimehoshi commented Sep 20, 2022

What about adding these APIs?

  • ebiten.SetFrameBlockingMode(ebiten.FrameBlockingModeNone)
  • ebiten.SetFrameBlockingMode(ebiten.FrameBlockingModeInput)

We would keep ebiten.SetTPS as it is (and SyncWIthFPS would still be valid, which you might think is odd).

The advantage of my suggestion compared to @tinne26's suggestion is that 1) the blocking behavior is very clear, 2) we don't have to change the notion of 'tick' and 'frame'.

The odd combination would be like ebiten.SetFrameBlocking(ebiten.FrameBlockingModeInput) and ebiten.SetTPS(60) or other integers. In the current implementation, Update is called a certain maximum (5) times in most cases. Or, we can ignore the TPS setting when the blocking mode is ebiten.FrameBlockingModeInput.

@tinne26
Copy link

tinne26 commented Sep 20, 2022

This is interesting, and I basically agree with your general evaluation, but I'm not convinced. As you say, the blocking behavior is clear, but I think it moves us away from the main concept of a loop with Update() and Draw() where any of the two may need to block and the blocking on each one can delay the other. I don't think that focusing on the blocking mode while dropping Update() out of the picture is clearer. And as you say, then setting the TPS is confusing when update is set to block until we receive input (because update is only referenced in regards to a fixed time-based loop / ticks).

Or, we can ignore the TPS setting when the blocking mode is ebiten.FrameBlockingModeInput.

Definitely ignore the TPS. To me this extremely clear with the UpdateMode model.

About the notions of tick and frame, I don't think our notions of frame are very different, as I've already mentioned in other comments. And as for ticks, we don't know what most Ebitengine users have in mind, so I don't think this point is unequivocal.

Right now I'd rather take SetTPS(SyncWithInput), despite the obvious weirdness already discussed, than this proposal. On the positive side, I think we have managed to clearly identify all the main parts of the puzzle. On the negative side, the puzzle is complex and I don't think any of the proposals are ideal, so I'd be wary to introduce these new functions unless we are quite confident that the new API is the best way to go.

Summarizing all the main proposals up to this point:

  • The UpdateMode proposal is explicit about the blocking and sync for update, but it does not reflect that the blocking on update can also impact Draw() and frame rate. This could still be clarified in the documentation of UpdateMode itself, same as that small page showing the main loop structures.
  • The SetFrameBlockingMode proposal is explicit about the blocking and the fact that it can affect Draw() and frames, but the combination of SetTPS with input blocking is less explicit than UpdateMode and the connection between blocking and update is also blurrier, prioritizing the connection between blocking and Draw() instead. And SyncWithFPS stays separate too. Again, documentation could help.
  • The SetTPS(SyncWithInput) proposal mixes up concepts that are technically not quite right and it really isn't clearer or more explicit than the other two proposals on any part of the main loop. But the API changes are minimal, and even if we need to deprecate SyncWithInput later, we already had to do that for SyncWithFPS.

Given that there's still considerable work to do to get SkipDraw working and see if buffer swaps are GPU-friendly, maybe that can be prioritized first and we can see if anyone else chimes in or if we come up with anything better with regards to the FPSMode / main loop control discussion. Maybe SkipDraw would be better tracked on a separate issue, as it is orthogonal to loop modes.

@tinne26
Copy link

tinne26 commented Sep 20, 2022

A third approach is this one, which I mentioned ealier:

SetLoopMode(ebiten.UpdateTPS, ebiten.DrawVsyncOff) // or UpdateFixedTimestep, etc.
SetLoopMode(ebiten.UpdateOnce, ebiten.DrawVsyncOn) // or Immediate, NoBlock, something about frames, etc.
SetLoopMode(ebiten.UpdateWaitInput, ebiten.DrawVsyncOn) // or UpdateOnInput, etc.
// SetLoopMode(ebiten.UpdateOnRequest, ebiten.DrawVsyncOff) // example, hypothetical addition for async input

I like it conceptually (if we improve the constant names, and maybe SetGameLoopMode() is better) and I think it's the clearest option (very explicit loop = update + draw), but it messes with the API badly. SetTPS remains, but SetVsyncEnable stays deprecated, FPSMode is also deprecated and vsync constants are added.

@hajimehoshi
Copy link
Owner Author

What I'm not convined is mixing UpdateTPS/UpdateOnce and UpdateWaitInput. The latter's behavior is completely different from the former in terms of blocking Draw. So I thought these should be a different flag and that's why I suggested SetFrameBlockingMode (and SetTPS).

The SetFrameBlockingMode proposal is explicit about the blocking and the fact that it can affect Draw() and frames, but the combination of SetTPS with input blocking is less explicit than UpdateMode and the connection between blocking and update is also blurrier, prioritizing the connection between blocking and Draw() instead. And SyncWithFPS stays separate too. Again, documentation could help.

Yeah I separate SyncWithFPS by keeping the existing API on purpose, as I described above.

@tinne26
Copy link

tinne26 commented Sep 21, 2022

The latter's behavior is completely different from the former [...]

I totally agree with this point, but I do not believe that highlighting the blocking at the expense of being less explicit about Update() is an improvement.

What about focusing on intended usage instead of update / draw / blocking / ticks / frames?

SetLoopMode(ebitengine.LoopModeFixedTimestep)
SetLoopMode(ebitengine.LoopModeDeltaTime) // or VariableTimestep
SetLoopMode(ebitengine.LoopModeInputBlock)

It's true that modes behave very differently (in fact, fixed timestep is also very different because it's the only that has 0-N updates), but they are different loop modes anyway, and the loop involves both update and draw, so it seems fair to me.

@Zyko0
Copy link
Contributor

Zyko0 commented Sep 21, 2022

FPSModeVsyncOffMinimum is quite odd since a) this affects TPS and b) this consumes GPU power unnecessarily (e.g. when a mouse is dragged).

I am late to the discussion, @tinne26 suggestions make sense but look heavy for a single issue I believe.
If we were to split or try to resolve a) and b) from original post the shortest way, would the following API solve it?

// FPS capped to refresh rate or not, FPS as the number of calls to Draw() per second
SetVsyncEnabled(bool) // Undeprecate as suggested

// Sets the number of times Update() will be called
const (
	TPSOnInputEvent = -2 // Whatever the name
	TPSUncapped = -1
)
SetTPS(int) // Special values as UncappedTPS / OnInputEvent

var SkipDraw error // Whatever the name
Update() error // Special error to skip the next few Draw() before the next update, by re-using same buffer

These are the simplest changes I can come up with, as a first step not to complicate our dear "dead simple library"
I can't see a weird combination that could be done inadvertently by a user, and it allows:

  • Vsync off or on any TPS setup
  • TPS either uncapped / fixed to a value / calling update on input only
  • Returning a special error in Update() is quite explicit and would be the only way to skip the next (few) Draw().
    • There's no confusion possible compared to SkipDraw() that could be called at any point in time (even in Draw() itself).
    • This would still allow a user to fine-tune when he wants to Draw or not (after evaluating the only inputs are mouse dragging for example)

I think this welcomes most (I might have missed some) use cases without changing the API too much.

The special error returned from Update (which I'm also not a fan of) could also be solved by a combination of extra API like this one:

// This would not be the SetFPSMode we know, but an extra addition that can synergize with previously suggested SetVsyncEnabled
const (
	FPSModeAuto mode = 0
	FPSModeManual = 1
)
SetFPSMode(mode) // Mode being FPSModeManual as on-request and would default to FPSModeAuto (no specific behaviour)

// Probably known previously as ScheduleFrame, but allows for scheduling next few frames (not a single one, but those between the next call to Update) in a lower TPS + high FPS setup for example
// If this function is called in FPSModeAuto, it has no effect
// If this function is not called in FPSModeManual, the previous buffer is re-used
ScheduleFrame() // with a 's' or "NextFrames" whatever name we can come up with. But this would create the same thing I pointed out with SkipDraw as it can be called anytime but should only be called in Update

SetScreenClearedEveryFrame still exists, but if a user chose to use it in a setup that doesn't mix well, then it can be fixed by choosing the appropriate one.

@tinne26
Copy link

tinne26 commented Sep 21, 2022

What you explain is the SetTPS(SyncWithInput) initial proposal, but renaming SyncWithFPS (already existing) and SyncWithInput to TPSUncapped and TPSOnInputEvent. We agree on the advantages:

Right now I'd rather take SetTPS(SyncWithInput) [...]

Because...

[...] the API changes are minimal

But it is sus, both semantically and technically. Even under Hajime's understanding of ticks, SyncWithInput doesn't really make sense in the context of SetTPS, and under my understanding of ticks, neither SyncWithInput nor SyncWithFPS make sense. And it's not nice that an update-related configuration affects frame rate without being more explicit about it (my initial SetUpdateMode also suffers from this to some degree, but at least it separates the update mode from the tps value).

We haven't mentioned it explicitly, but I think we all agree that if the changes are considered too messy, we can always leave it for v3. But both SetFrameBlockingMode() and SetLoopMode() (version 2) "only" add one function and reuse SetTPS() and SetVsyncEnabled(), which is not that bad.

On the topic of SkipDraw:

There's no confusion possible compared to SkipDraw() that could be called at any point in time (even in Draw() itself).

Maybe there was some confusion, but at least I didn't suggest this in this issue (though we did mention it on Discord). My preference is to detect if the Game interface implements SkipDraw() bool and use that. See the pseudo-code on https://tinne26.github.io/ebi-main-loop/. My main point right now would be that under the default loop mode (TPS / fixed timestep), this is the only method that gives enough control over skipping or not skipping draws, as update may not be called between draws (and for some reason you may want to draw the first time but not the second, or skip both), or update may be called multiple times in a row and the initial error returns are unused.

@hajimehoshi
Copy link
Owner Author

hajimehoshi commented Sep 21, 2022

We discussed on the Discord server and concluded that:

Let's focus on the first two items.

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

No branches or pull requests

3 participants