processing / processing/libprocessing

Windowing & event-loops: who owns what? (Internal vs external event loops)

Đang mở
#28 8 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Ngôn ngữ chính
Rust
Star
69
Fork
14
Merge trung bình
15 ngày 22 giờ
Pull request đã merge (30 ngày)
3

Mô tả

I'd like to discuss a bit our high-level library design. There are basically two different ways that we could have structured our library that comes down to the main event loop that drives the user's application forward: internal or external. Right now, we are structuring things in a way where we expect the external caller/FFI implementer to drive things forward. I think this is the right architecture but it's worth articulating the design trade-offs here.

Internal event loop and the callback handler architecture

In the internal event loop model, the "Processing runtime" owns the window and drives everything forward. What this means concretely from an API design perspective is that rather than having a series of imperative FFI functions, we'd rely on the user to provide us function pointers to callbacks that we fire whenever something happens: e.g. setting up your sketch, drawing each frame, receiving user input, etc.

This is how I built things for Nannou. Basically, we store a bunch of function pointers for the user's sketch and then call their handlers each frame when asked to do so by the engine. More specifically, here we leave frame pacing to the engine and by default it's using Bevy's game oriented "run as fast as possible" pacing.

In other words, the user doesn't decide when to present to a surface, they instead exist inside an event loop that drives all their handlers when the engine decides to.

There are advantages to this design in that it means that all the integration between windowing and the user's application can be contained and doesn't need to be exposed. This is also the most performant way to write a modern application and allows decoupling concerns. For example, we can enable a low-power / reactive event loop where we only re-render on user input quite easily here because the event loop is telling us when to render, not the other way around.

From an FFI perspective, however, this has a big drawback in that it requires expressing everything in terms of function pointers. Typically when codegening bindings in other languages, this makes things a bit more annoying because we have to wrap our functions rather than just having the tooling generate a function we can call. It also means defining a ton more data structures at the FFI layer; you have to have types for all the events, for the arguments to each function handler, etc.

External event loop and the procedural style

For this reason, when scaffolding libprocessing, it made sense to go for an imperative or procedural style with an external event loop. In this case, the implementer calls into our library at each logical step of the event loop. Our functions are exposed in terms of different operations that update some kind of draw/rendering state but are themselves stateless in their sense of time; you can call "start frame" and then wait an hour and call "end frame" and from the perspective of the library that's totally fine. The caller decides when to drive things forward.

Of course, any application that needs a window is still subject to an event loop, but rather than an application handler it can be implemented more via a polling/pumping mechanism like we do in GLFW. That is, GLFW doesn't tell us when to render, we just run in a loop and check in with GLFW to see if there are any new events.

In terms of the FFI boundary, this greatly simplifies things, and means that we can expose the API in terms of procedures. For the most part, we don't even need to define any custom FFI structs, as most things can be simple primitive arguments to functions, or getters for some logical API object's properties. This, in turn, makes it very easy to codegen. The implementer will get a list of a bajillion functions that they need to integrate with, but for the most part, their implementation will just be a pretty small layer over calling these functions. As demonstrated by our examples, setting up a basic GLFW loop is just a handful of lines of code.

The engine-eventing interface

So what would the advantage of integrating with winit in Bevy be? First, it would mean that implementers would also not have to worry about managing a window and setting up a surface. Instead, they would pull events from us each frame in a similar fashion to glfwPollEvents. This would be nice because it would mean that we wouldn't rely on implementers having to figure out what windowing libraries are available in their language and could reliably just expect us to manage the window for them.

But, secondly, it would also mean that events are more easily propagated within the engine itself. Right now, if we want to react to user input in Rust, we have to create an API to feed events into libprocessing. For the core engine, this isn't necessary because outside of window resizes there's no engine code that depends on certain user actions (button presses, etc.). However, there are potentially engine features in the future that may require such events. For example, render picking which requires casting a ray from the pointers position, etc.

The current limit in Bevy

Right now, Bevy uses an internal event loop, and although winit has support for external event loops it's not easy to bridge. I feel confident that we want to continue to maintain an external event loop. The entire Processing API is designed around the assumption that you can call certain methods like beginDraw/endDraw on a graphics object, and this only makes sense in the context where we imperatively drive things forward / decide when to present. Although this is a certain kind of inefficiency with respect to super high performance graphics, for most art and art installations it's conceptually way simpler.

So, for now, I feel confident that external event loop is the right design, but we'll eventually probably want to bring windowing into libprocessing so that users don't have to deal with it!

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu bằng cách đọc phần so sánh giữa các event loop nội bộ và bên ngoài của issue, tài liệu về external loop của winit được liên kết, và các ví dụ hiện có dựa trên GLFW. Không có tệp triển khai hoặc bài kiểm thử nào được nêu tên, vì vậy trước tiên hãy xác định nơi việc tích hợp cửa sổ và event loop hiện đi vào libprocessing. Để được xem là hoàn tất, cần có một kiến trúc đã được thống nhất và một phạm vi triển khai cụ thể.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
rust
Lĩnh vực
api, desktop
Loại issue
Tính năng
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Cần làm rõ
Mức phù hợp với người mới
25/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.