17cupsofcoffee / tetra

🎮 A simple 2D game framework written in Rust
MIT License
909 stars 63 forks source link

Add the feature to manually control redraw #245

Closed sumibi-yakitori closed 2 years ago

sumibi-yakitori commented 3 years ago

Using this feature to manually repaint when the need to update arises will reduce the CPU and GPU load on the Desktop Window Manager(Windows) or WindowServer(MacOS). In situations where machine performance is poor, the load on these window managers can be worrisome.

This is a feature needed for a very minor use case that only I need as I am creating tools that are always running.

This is not a perfect retained mode.

sumibi-yakitori commented 3 years ago

I just realized that there is the https://github.com/17cupsofcoffee/tetra/issues/119

17cupsofcoffee commented 3 years ago

You're right that #119 would be the ideal solution, but I'd maybe be open to adding something like this as a stopgap :+1:

That said, do you have any particular data that makes you concerned about the overhead of presenting every frame? I've not run into any issues with it, but I know you're using Tetra for a slightly more niche use-case than I am :)

sumibi-yakitori commented 3 years ago

MacOS (6 physical cores, 6 logical cores, dGPU)

Default

default_macos

disable_auto_redraw

disable_auto_redraw_macos

Windows (2 physical cores, 4 logical cores, iGPU)

Default

default_win

disable_auto_redraw

disable_auto_redraw_win

Here is my file manager in action. This tool, which is built on top of tetra, is now an essential part of my game development. In addition, there are other tools that are always running that were created using tetra, so the actual CPU and GPU usage of Window Manager will be a little higher.

How accurate this measurement is is open to question, though. I'm a little concerned about WindowServer on MacOS. (17.7% -> 5.3%)

I don't think I need to worry about this issue with modern many-core CPUs. I'd rather buy a more expensive CPU than worry about this, but that's not the case with a Mac.

It is my selfishness that I want to give rustc as much CPU power as possible 😆

It's not much use in today's world, but on a poorly performing macbook, it might have some effect on battery life.

17cupsofcoffee commented 3 years ago

My only concern is that I'm not sure if skipping calls to graphics::present is the right approach to avoid redraw - your game loop is still going to be running in the background and taking up CPU if you do that, so I'm wondering if something along the lines of SDL_WaitEvent (i.e. sleep the thread until the next event) is more like what you'd need. I don't really write GUI applications, so I don't know how people generally handle this 😅

I'll give this some thought/ask around about how other people have achieved stuff like this. I've also made some progress with implementing #119 (though I still need to figure out a nice API), so you might potentially be able to use that instead of this PR.

sumibi-yakitori commented 3 years ago

My control of the dirty flag was lax, so I improved it and was able to further reduce the CPU and GPU usage.

Previously, I had not stopped OpenGL commands from being issued.

struct MainState {
  dirty_count: DirtyCount,
}

impl MainState {
  fn new(ctx: &mut Context) -> tetra::Result<Self> {
    let dirty_count: DirtyCount::new(3);
    Self {
      sub_state: SubState::new(ctx, dirty_count.clone())
    }
  }
}

impl State for MainState {
  fn event(&mut self, ctx: &mut Context, event: Event) -> tetra::Result {
    self.dirty_count.set_dirty();

    // ...

    Ok(())
  }

  fn draw(&mut self, ctx: &mut Context) -> tetra::Result {
    std::thread::sleep(
      Duration::from_secs_f32(1.0 / 60.0)
        .saturating_sub(tetra::time::get_delta_time(ctx)),
    );

    // >>>>>> I just added this block <<<<<<
    if !self.dirty_count.is_dirty() {
      return Ok(());
    }

    // ...
    self.sub_state.draw(ctx)?;
    // ...

    // if self.dirty_count.is_dirty() {
      tetra::graphics::present(ctx);
      self.dirty_count.decrement();
    // }

    Ok(())
  }

  fn update(&mut self, ctx: &mut Context) -> tetra::Result {
    if {{update_timing}} {
      self.dirty_count.set_dirty();
    }

    // example
    // let i = self.animation.current_frame_index();
    // self.animation.advance(ctx);
    // if self.animation.current_frame_index() != i {
    //   self.dirty_count.set_dirty();
    // }

    Ok(())
  }
}
#[derive(Clone)]
pub struct DirtyCount {
  current: Arc<AtomicUsize>,
  max: usize,
}

impl DirtyCount {
  pub fn new(max: usize) -> Self {
    let mut x = Self {
      current: Default::default(),
      max,
    };
    x.set_dirty();
    x
  }

  pub fn set_dirty(&mut self) {
    self.current.store(self.max, Ordering::SeqCst);
  }

  pub fn decrement(&mut self) {
    let x = self.current.load(Ordering::Relaxed);
    self.current.store(x.saturating_sub(1), Ordering::SeqCst);
  }

  pub fn is_dirty(&self) -> bool {
    self.current.load(Ordering::Relaxed) > 0
  }
}

Even with this, update and event are still called, so the background operation is not completely stopped (they need to keep running in order to set the dirty flag), but I think the load is low enough to be practical.

Screen Shot 2021-03-02 at 6 34 21
sumibi-yakitori commented 3 years ago

It's not common to make tools with tetra like I did, but I think there are more and more examples of making tools with game frameworks these days.

LDtk, Pixel Fx Designer, etc.

In the worst cases, people are using Unity to create tools.

Tools should be made using UI frameworks with retained mode, but when it comes to something that works cross-platform and can handle images and pixel arts beautifully, using a game framework is probably the quickest way to go.

It is not my desire to take up your time by making you deal with niche use cases.

17cupsofcoffee commented 3 years ago

Tools should be made using UI frameworks with retained mode, but when it comes to something that works cross-platform and can handle images and pixel art beautifully, using a game framework is probably the quickest way to go.

💯

I definitely want to support this use case better in some way, I just want to make sure I'm not adding extra complexity via feature flags if there's a simpler/easier approach.