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

Using custom super loop for managing events #2857

Closed
CJBuchel opened this issue Jun 7, 2023 · 7 comments
Closed

Using custom super loop for managing events #2857

CJBuchel opened this issue Jun 7, 2023 · 7 comments

Comments

@CJBuchel
Copy link

CJBuchel commented Jun 7, 2023

I'm trying to build a project that is heavily abstracted, where the window is a trait and it's called into a super loop which would run something like window.on_update()

Primarily I'm doing this so I can switch out the windowing library or the graphics library (currently winit and wgpu) in the future with ease. It's unlikely, but it's possible and I wanted to future proof myself.

pub trait Window: WindowContext + RenderContext {
  fn new(props: WindowProps) -> Self;

  fn on_update(&mut self);

  fn get_width(&self) -> u32;
  fn get_height(&self) -> u32;
  fn get_scale(&self) -> f64;

  // Attributes
  fn set_vsync(&self, enabled: bool);
  fn is_vsync(&self) -> bool;
}

And in an application somewhere my goal is to simply run

fn run(&mut self) {
    self._app_startup();

    
    // Super loop
    while *self.running.lock().unwrap() {

      // Run layers
      for layer in self.layer_stack.iter() {
        layer.lock().unwrap().on_update();
      }
      
      self.window.lock().unwrap().on_update();
    }

    self._app_cleanup();
  }

But I'm having an extreme amount of difficulty working around the run and run_return loops. (Still learning all there is to rust, I come from a c++ background so my code is still reminiscent of that style).

Up til now I've mostly been fine using run_return because I'm currently using my own event system, so technically all the events are processed in the platform dependent window which has the run_return. Then it matches the type of event and uses a callback function which dispatches my own events in my application.

Here is the full implementation used for processing the events

impl WindowContext for LinuxWindow {
  type WindowCtx = winit::window::Window;
  type WindowCtxProps = WindowContextProps;
  type RawEvent = winit::event::Event<'static,()>;
  
  fn new_window_ctx(props: crate::core::window::WindowProps) -> Self::WindowCtxProps {
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new()
      .with_title(props.title.clone())
      .with_inner_size(winit::dpi::LogicalSize::new(props.logical_width, props.logical_height))
      .build(&event_loop)
      .unwrap();

    WindowContextProps {
      event_loop,
      window
    }
  }

  fn get_window_context(&self) -> &Self::WindowCtx {
    &self.window_context_props.window
  }

  fn get_window_ctx_props(&self) -> &Self::WindowCtxProps {
    &self.window_context_props
  }

  fn set_event_callback(&mut self, callback: MutexEventCallback) {
    self.event_callback = Some(callback);
  }

  fn set_event_handler(&mut self, handler: Box<dyn Fn(Self::RawEvent)>) {
    self.event_handler = Some(handler);
  }

  fn callback_events(&mut self) {
    let (sender, receiver): (Sender<Event<'static, ()>>, _) = channel();

    self.window_context_props.event_loop.run_return(|event, _, control_flow| {
      *control_flow = winit::event_loop::ControlFlow::Exit;
      sender.send(event.to_static().unwrap()).unwrap();
    });

    self.window_context_props.event_loop.run_return(|event, _, control_flow| {
      *control_flow = winit::event_loop::ControlFlow::Exit;

      // internal event callback
      match event {
        
        winit::event::Event::RedrawRequested(_) => {
          if let Some(callback) = &mut self.event_callback {
            callback(NEW_MUTEX_BOX!(AppRenderEvent::new()));
          }
        },


        winit::event::Event::WindowEvent { window_id: _, event } => {
          match event {
            winit::event::WindowEvent::CloseRequested => {
              if let Some(callback) = &mut self.event_callback {
                callback(NEW_MUTEX_BOX!(WindowCloseEvent::new()));
              }
            },

            winit::event::WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size } => {
              if let Some(callback) = &mut self.event_callback {
                let logical_size: LogicalSize<u32> = new_inner_size.to_logical(scale_factor);

                // Physical size
                self.window_props.physical_width = new_inner_size.width;
                self.window_props.physical_height = new_inner_size.height;

                // Logical size
                self.window_props.logical_width = logical_size.width;
                self.window_props.logical_height = logical_size.height;

                callback(NEW_MUTEX_BOX!(WindowResizeEvent::new(self.window_props.logical_width, self.window_props.logical_height)));
              }
            },

            winit::event::WindowEvent::Resized(physical_size) => {
              // self.window_props.physical_scale = scale;
              if let Some(callback) = &mut self.event_callback {
                let logical_size: LogicalSize<u32> = physical_size.to_logical(self.window_context_props.window.scale_factor());
  
                // Physical size
                self.window_props.physical_width = physical_size.width;
                self.window_props.physical_height = physical_size.height;
  
                // Logical size
                self.window_props.logical_width = logical_size.width;
                self.window_props.logical_height = logical_size.height;
  
                callback(NEW_MUTEX_BOX!(WindowResizeEvent::new(self.window_props.logical_width, self.window_props.logical_height)));
              }
            },

            winit::event::WindowEvent::KeyboardInput { device_id: _, input, is_synthetic: _ } => {
              match input.state {
                winit::event::ElementState::Pressed => {
                  let virtual_key = Some(input.virtual_keycode).unwrap_or_default().unwrap_or(winit::event::VirtualKeyCode::Space);
                  let key = KeyCode::from(virtual_key);
                  if let Some(callback) = &mut self.event_callback {
                    callback(NEW_MUTEX_BOX!(KeyPressedEvent::new(key, false)));
                  }
                },

                winit::event::ElementState::Released => {
                  let virtual_key = Some(input.virtual_keycode).unwrap_or_default().unwrap_or(winit::event::VirtualKeyCode::Space);
                  let key = KeyCode::from(virtual_key);
                  if let Some(callback) = &mut self.event_callback {
                    callback(NEW_MUTEX_BOX!(KeyReleasedEvent::new(key)));
                  }
                }
              }
            },

            winit::event::WindowEvent::MouseInput { device_id: _, state, button, ..} => {
              match state {
                winit::event::ElementState::Pressed => {
                  let mouse_code = MouseCode::from(button);
                  if let Some(callback) = &mut self.event_callback {
                    callback(NEW_MUTEX_BOX!(MouseButtonPressedEvent::new(mouse_code)));
                  }
                },
                winit::event::ElementState::Released => {
                  let mouse_code = MouseCode::from(button);
                  if let Some(callback) = &mut self.event_callback {
                    callback(NEW_MUTEX_BOX!(MouseButtonReleasedEvent::new(mouse_code)));
                  }
                }
              }
            },

            winit::event::WindowEvent::MouseWheel { device_id: _, delta, phase: _, .. } => {
              match delta {
                winit::event::MouseScrollDelta::LineDelta(a, b) => {
                  if let Some(callback) = &mut self.event_callback {
                    callback(NEW_MUTEX_BOX!(MouseScrolledEvent::new(a,b)));
                  }
                },
                _ => panic!("Unknown mouse scroll event")
              }
            },

            winit::event::WindowEvent::CursorMoved { device_id: _, position, .. } => {
              if let Some(callback) = &mut self.event_callback {
                callback(NEW_MUTEX_BOX!(MouseMovedEvent::new(position.x as f32, position.y as f32)));
              }
            },

            _ => {}
          }
        },
        _ => {}
      }
    });
  }
}

But while this has worked great, I'm trying to add a debugging window using egui, and I'm trying really hard not to place it in this implementation. Because I'm wanting to keep to the abstract nature, so I've separated egui into it's own module and now I'm trying to link it up.

My problem becomes apparent when I have to provide egui with the winit events. If I were able to control egui with my own custom events then life would be gravy. But egui is pretty heavily tied to winit, and I'm using the egui_winit_platform and egui_wgpu_backend crates as the contexts. I tried to do the same thing as my own events by creating a callback, but instead calling back the raw winit events instead. But this caused a closure break. Because event can't be moved outside of the run_return or run loops. And as far as I can tell in rust, there is no way to get around that. Which is a tad annoying, just being able to create a static copy of it and send it through the callback is not allowed in rusts rule book. Which is frustrating.

More or less I just need to get the events from run_return to a function like this

fn on_event(&mut self, event: crate::events::event::DynamicMutexEvent) {
    if event.lock().unwrap().get_event_type() == EventType::AppRender {
      self.window.winit_event_handler(|e| {
        self.platform.handle_event(e);

        /** Do the rest of the egui code/render */
      });
    }
  }

But, I've given up on that dream using the run_return, so now I'm curious if there is an actual event polling system I can use, to forgo the need to put everything related to rendering in that run_return/run and poll the events in my own way.

TL;DR
I'm wondering if there is a way to poll the events without the event_loop? I have my own super loop which I want to use, I have my own events which I'm already using, and I'd like to pass those raw winit events through a callback which I can use for egui. I'm unsure if this exists or not, I have this funny feeling that it probably doesn't, because this is rust, and it's not rust unless it's beating me with a stick each time I make progress.

@daxpedda
Copy link
Member

daxpedda commented Jun 7, 2023

I believe what you are looking for (just barely glanced at your post yet) is #2767.

@notgull
Copy link
Member

notgull commented Jun 8, 2023

Integrating the winit event loop into another event loop is a fool's errand. The only platforms where it can be done robustly are on non-Android Linux. It's hacky on some of the other platforms and downright impossible on others. run_return and the PR linked above might allow you to do it in a hacky way in some cases, but such code has a habit of breaking in subtle ways that vary from platform to platform.

For your use case, I would expose an EventLoop trait with the run method and anything else you want from our events. Then, use that as the modular interface to a low level event loop in your code, rather than the while loop you have now.

@CJBuchel
Copy link
Author

CJBuchel commented Jun 8, 2023

Yea, I suspected needing to implement the EventLoop as the main super loop. And then everything to be passed along into there to be processed, rather than the opposite of taking things out and doing what I want with it using a callback.

I was trying to follow along with a GUI tutorial, that built up an abstracted game engine. Granted they were doing it in C++ using GLFW and Imgui so the code was different. But I was trying to follow along and keep the core concept the same.

I do have a hacky idea which I want to stubbornly try first before I throw in the towel and do it properly though.
For pretty much all my code I'll be using a different event system with it's own key codes and such, the exception being the debug gui. I already have a map in place which converts the winit events into my own events. But I wonder if doing it in reverse would be any good as well, turning my own events into winit Events for specific parts of my code. Like for instance, the egui section, stupidly hacky because I'm effectively turning winit events into my events, using a callback function for them. Then turning those events back into winit events for the egui part which can be placed in a on_event() function. But I at least want to see if that works first, before switching anyway because it will likely be very inefficient.

@rib
Copy link
Contributor

rib commented Jun 8, 2023

Integrating the winit event loop into another event loop is a fool's errand

Yeah, it definitely comes with a lot of traps and I wouldn't recommend it if you can possibly avoid it.

I only really implemented the pump_events API to allow integration with external event loops for applications that are already strongly tied to that design.

There are numerous platform-specific limitations with trying to write portable graphical applications within an explicit loop like above.

I feel like it's fair to say that it's become an out-dated design trait to build graphical applications based on an explicit loop in the app.

APIs like SDL have also bent over backwards to try and support that view but end up dealing with various special cases because the underlying window systems on some platforms aren't really compatible with this model.

It's probably best to generally think of window systems as being event based, and not expect everything to be pollable within a while loop.

Not sure if you would want to support Web for example but there's no way to run an endless loop in a browser - you instead need to build on top of the event model of the browser since the event loop is an implementation details of the browser.

The tricks required to make MacOS look like it supports polling inside an external event loop also aren't possible on iOS.

Some things should be handled in sync with the operating system (such as lifecycle events on mobile devices) or drawRect callbacks on macOS / iOS but it's more tricky to ensure this with an external loop design.

@CJBuchel
Copy link
Author

CJBuchel commented Jun 8, 2023

There are numerous platform-specific limitations with trying to write portable graphical applications within an explicit loop like above.

I feel like it's fair to say that it's become an out-dated design trait to build graphical applications based on an explicit loop in the app.

Could you explain a bit more about why that is the case?
I understand that multiple actual "event loops" is a bad idea. Although my impression on the subject was to have a single large loop that also happened to handle events, the only difference being that the main loop wasn't controlled by the event library in question. But rather Incorporated into an existing loop.

For instance at the moment I'm doing a constant cycle of layer updates -> event polling -> event dispatch -> window update. And I'm doing that with run_return which has a bunch of callbacks for each event, which dispatch to each layers on_event() function. The difference I see from having a library control the event system is just changing around the structure so instead everything is called inside of that event loop. Which from what I can tell just changes around the layout of what gets updated first event polling -> event dispatch -> layer update -> window update

But obviously there must be something more to it, because every windowing library or event library I come across has a very similar architecture for rust. Utilising a run loop where you put all your code inside. And more than that having the events inside that loop only exist within the closure. So you can't do anything with them unless it's within the closure.

I'm just curious why this structure is the preferred case for rust and/or modern windowing systems.
You'll have to forgive me for not understanding it, but what actually happens if it was structured differently? and instead of a run loop like this

run(|event, _, control_flow| {
    /* handle events */
});

it was a system like this

let (event, _, control_flow) = run_return();
/* handle events */

What actually breaks when doing this? Other than needing to put it in a loop so it continuously does the poll like so

while running {
  let(event, _, control_flow) = run_return();
  /* handle events */
}

I just don't really know why it's discouraged, because as far as I can tell. I'm more or less doing this anyway, just in a different more round about manner. I'm instantly returning from the run_return() loop, and I'm sending all of the events it got during the loop though a callback. Then i'm continuing past the loop and processing the events I got in the rest of my application.

@rib
Copy link
Contributor

rib commented Jun 8, 2023

It's not really anything special for Rust here; it more a question of how the various OSs and window systems are designed.

Web is the most exaggerated example here where we have a platform that fundamentally doesn't support having a long running loop that an application can run (since that would block the browser).

The other awkward cases generally revolve around events that need to be synchronized with the window system or operating system.

Take for example a "suspend" event on Android. Android has a Java based application programming model and it notifies applications that they are about to be paused via a Java callback on an Activity class.

What's important here is that Android expects the application to handle that event during the Java callback. It's not something that can be added to a queue of events to process later. Conceptually it wouldn't make sense to buffer an application lifecycle event - those need to be handled in sync with the operating system.

Another case is the drawRect callback for macOS and iOS. The window system expects the application to actually do its rendering during that callback - again it's not something that can be added to a queue and handled later.

These things that need to be handled synchronously don't really lend themselves to being processed within a loop that the application has created because the thread that runs the loop then needs to make sure it's in control of when the window system dispatches events (e.g. by calling an api like pump_events).

It's also not possible to control when the window system / OS dispatches those events on all platforms.

On iOS for example you have an NSApplication that contains a RunLoop that is the thing that makes the app wait for events from the operating system and invoke callbacks for events like lifecycle events or drawRect events. There's no way to stop and start an NSApplication so there isn't something you can put inside your own loop that would control exactly when the internal RunLoop dispatches events.

So more specifically to try and highly what goes wrong based on your example.

If you follow a model like this...

while running { // This loop itself would freeze a browser
  let(event, _, control_flow) = run_return(); // There's no way to implement a function like run_return on iOS because NSApplication can't be stopped
  /* 
    Then by the time you get here you have implicitly already missed your deadline for handling any event
    that needs to be synchronized with the window system, such as 'drawRect' on macOS / iOS or mobile life cycle events.

    Some window system / OS events fundamentally can't be buffered.
   */
}

Hope that clarifies a bit some of the challenges

@CJBuchel
Copy link
Author

CJBuchel commented Jun 9, 2023

Ah, I see.
I was more focused on my own implementation where in my case I only care about desktop windowing.
I didn't think about the fundamental cross compatibility of multiple types of windows. Where some types would have blocking or suspending natures which couldn't be driven with the poll like loop.

Thanks,

@CJBuchel CJBuchel closed this as completed Jun 9, 2023
@rib rib mentioned this issue Jul 28, 2023
17 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

4 participants