-
-
Notifications
You must be signed in to change notification settings - Fork 154
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
New main loop structure to better allow running PyGame in browser #3187
Comments
I've also started a conversation upstream in SDL about allowing SDL-based programs like PyGame to run inside a web worker, which is a different approach that would allow PyGame programs to work in the browser using the classic PyGame event loop structure. |
Sounds like an interesting idea, but few things to note:
|
Aye. It looked like there was interest in eventually upgrading PyGame to use SDL3, based on a number of issues tagged "sdl3".
Very interesting idea. At least Pyodide packages its own version of PyGame specially, so if there was a PyGame API for supporting an interruptible main loop, Pyodide could patch in its own implementation that would work in a browser context.
Ideally IMHO the API would support Pyodide/pygbag's need to define a main loop function that is interruptible, which could also be targeted at SDL3's "main callbacks API" in the future. A very rough initial API sketch:
I suspect getting a more finalized API will take a couple of passes. |
There is, but it'll take time to migrate over SDL3. SDL3 uses import pygame
pygame.init()
window = pygame.Window()
@pygame.step
def game_step(dt: float): # Maybe provide delta time.
surf = window.get_surface()
surf.fill("black")
pygame.draw.rect(surf, "white", (50, 50, 50, 50))
return True # Or maybe pygame.DO_CONTINUE?
@pygame.process_event
def event(ev: pygame.Event):
if ev.type == pygame.QUIT:
return False # Or maybe pygame.QUIT_APP?
return True # Explicit True to avoid accidental hanging apps (None is falsy value), but can be done in other ways, like not giving the choice to ignore QUIT event and immediately call quit callback (or maybe add an option to abort quitting in the quit callback)
@pygame.on_quit
def quitting():
print("Quitting the game")
pygame.run() Or maybe (here using import pygame
@pygame.setup
def setup():
window = pygame.Window()
return window # State passed to callbacks, can be any python object.
@pygame.step
def game_step(state):
surf = state.get_surface()
surf.fill("black")
pygame.draw.rect(surf, "white", (50, 50, 50, 50))
return True # Or maybe pygame.DO_CONTINUE?
@pygame.process_event
def event(state, ev: pygame.Event):
if ev.type == pygame.QUIT:
return False # Or maybe pygame.QUIT_APP?
return True # Explicit True to avoid accidental hanging apps (None is falsy value), but can be done in other ways, like not giving the choice to ignore QUIT event and immediately call quit callback (or maybe add an option to abort quitting in the quit callback)
@pygame.on_quit
def quitting(state):
print("Quitting the game")
pygame.init(run_callbacks=True) Or maybe even: import pygame
class MyApp(pygame.App):
def __init__(self):
super().__init__()
self.window = pygame.Window()
self.running = True # Or maybe pygame.DO_CONTINUE?
def step(self, dt: float):
surf = self.window.get_surface()
surf.fill("black")
pygame.draw.rect(surf, "white", (50, 50, 50, 50))
return self.running()
def process_event(self, event: pygame.Event):
if event.type == pygame.QUIT:
self.running = False # Or maybe pygame.QUIT_APP?
# If pygame.QUIT events are handled by library instead:
if something_else_that_should_cause_to_close_the_app():
self.quit()
def on_quit(self):
print("Well, closing the app")
# If pygame.QUIT events are handled by library instead:
if i_feel_fancy_to():
self.abort_quitting()
pygame.init()
MyApp().run() |
I'm against this for several reasons, outlined below. Disclaimer: I have a decent amount of experience with pygbag but have never used pyodide, and I haven't touched pygbag in several months, so some of my knowledge there might be out of date. IMHO if you take away the main game loop from the programmer you have just promoted pygame from a framework to a game engine, which is a big no-no. Aside from that, if we adopt a new game loop API, all existing code would be different anyway, and any people who switch to pygame ce from upstream will have different code as well. Maybe if you hone your API enough this won't be the case, but with the current example, switching from an upstream or non-web ce style game loop to this thing will have substantially more code rewriting involved than converting it to the current asyncio style. Aside from that, assuming we do adopt an API like this, the old style loop would have to be maintained for backwards compatibility, on both the pygbag and pygame side. This seems like undue maintenance burden for a feature that might not even have widespread use. Aside from that, how involved do you want this loop to be? Fix Your Timestep has several examples of different loop architectures, and different pygamers can implement any of these at the moment. I'm not going to say that the vast majority of pygamers either use or don't use delta time, for example, but that's something that would be a serious refactoring headache if someone is forced to switch. All of a sudden framerates aren't nearly as configurable as well. Maybe I want my game to cap at 30FPS, while somebody else wants to cap at 120FPS. Maybe both of us want our physics to chug along at 60FPS. Is this something that you are going to support? How? What if somebody comes along and wants to do something really convoluted, like physics, game logic, rendering, and audio all having their own separate framerates? One of thee great things about pygame is that you can actually do that. I am in favor of keeping it that way. I do understand the need to support different platforms though. I don't think that it complicates things too much to make async loops a requirement of truly cross-platform pygame code. You already are supposed to One last argument, even assuming that this API was so simple that barely any refactoring would be needed, and everybody loved the new pygame loop so much that people just unilaterally switched to using it, there still would be things that need to be done for web builds:
|
One other major concern is that we already are not completely thread-safe, and introducing thread wackery behind the scenes here is far, far in the future after we fix thread safety. I honestly cannot see us approving something like this if it dramatically changes how the end user's code functions. Backwards compatibility with old source code is one of pygame-ce's primary tenets (including compat with the original pygame). Yes, we've discussed some breaking changes when we swap to pygame-ce 3.0.0, but I think it'll still be mostly functional changes, not many huge API changes. |
I'd like thoughts from the following individuals though: |
I don't think this is a proposal we can straight out reject, but I also agree that backwards compatibility and support for the classic way of doing things should be the priority. I have thought about adding a |
@davidfstr nb: pygbag web browser async loop use Pygbag is frame based and does not yield for time but instead for VSYNC or custom clocks (eg slow mo / game replay ) . Though pyodide could use same loop as pygbag loop automatically (cf zengl using RequestAnimationFrame on pyodide) that does not mean pyodide is the right runtime for running game engines. @JiffyRob Optimization has costs. Everything else could be probably fixed at pygbag level and would probably be out of scope/maintenance burden for pygame-ce. You maybe got FPS wrong : browsers get it right for you but GPU drivers will break it nevertheless ( insert Linus-Nvidia video here with some tearing ). Subtles differences beetween runtimes make it that loop must remain in user control ( also +1 for JiffyRob's not-a-framework ) There are other backends for pygbag / pygame-ce than web ( there's WASI, and console/mobile C89 retargeting). Async is required for using sub interpreters on web. Async is often required on consoles. The only other sane loop model would be arduino style : setup + loop (which is confusing and should be called step) and which sounds more like SDL3 loop model. But going that way will cut everyone1 from async I/O on main thread (because doing anything I/O related on web browsers has to be over-complicated for some reason). I would advise against using web workers : they are not meant for running games libraries which are expected to live on GUI thread and sync'ed to device VSYNC. Workers are for physics engines, DSP/SFX and heavy computations. Non specialized use of workers encourage bad pratices, the web is async. People also need to learn async and implement it better (and everywhere). My advices would be get CPython a real "platform" module that can at least setup a framebuffer and access "eval" on javascript hosts, stop relying on OS thread for main loop by default and use asyncio console instead (it is already there since 3.8). Pygame is doing it just fine on its own. Footnotes
|
I feel like maybe I'm not seeing something, but I don't get what the issue is? SDL3 has this new loop API optionally. We generally try to port as many as SDL3 APIs as possible, so lets say we port it and have a new optional loop API. Cool. That doesn't threaten backwards compatibility.
@davidfstr I don't think this is true, as far as I know the async main loops work fine locally. You mention students in your SDL issue, if you'd like to teach a class with this and hide that complexity away from the students I think the best way would be to make a single file helper library to import. Could have lots of other goodies as well. From my own experience in school it seems like professors often do things like that.
@pmp-p I assume you mean you would NOT advise, given the second half of your sentence? In terms of asynchronous-ness in the web in general it is very baffling to me as as someone whose experience is all in synchronous programming languages. But I've heard about stack switching from Pyodide and I wonder if this technology will allow web ported programs to behave more normally with regards to sync-ness. https://blog.pyodide.org/posts/0.26-release/#improvements-to-stack-switching-support |
@Starbuck5 indeed, thx edited Pyodide really lives in the future of "maybe that shiny new wasm X.X feature will get implemented on that browser" ( intrinsics / jspi / bulk memory / EH / workers etc) . imho Pygame-ce must stand reference to keep backward compatibility for everyone, including people who don't get updates for their devices's browsers or wasi runtime and are stuck forever on wasm 1.0 (mvp). Leave it to pyodide to patch up all the new features, and of course test them. btw: the more features used, the more bugs. Especially when Emscripten is involved ... |
Some of my takeaways from the discussion above:
Reading the room, I don't see a lot of support for this feature. |
I insist, this a a python C-API problem : not pygame's problem, the game loop belongs to python not to SDL ( and iirc the sdl_main default loop was shipped separately). A good hack example of how python handles async rendering loop ( used for Tk / idle REPL) can be seen here https://github.com/python/cpython/blob/bd4be5e67de5f31e9336ba0fdcd545e88d70b954/Modules/readline.c#L1394
multiple loop models give more power to user requiring more control and less OS specific code. Eg The pygbag default loop is valid anywhere. But it's true it could be enhanced with some c-api support python/cpython#79424, on desktop you may want to add a time.sleep or a sched_yield to avoid cpu burn.
It is not just a pygame-ce loop matter : it is the tip of an iceberg and its base is a python/async/os & non-os/GPU/IO relationship problem. If CPython was more versatile for toplevel loop - by default - we probably could do something nicer. I think the cost to change that is quite high for just some game loops around that indeed only need a few lines adjusted. |
Currently it is possible to run PyGame applications in a web browser using projects like Pyodide and pygbag (see these demos), however both projects require restructuring the PyGame event loop in a non-standard way:
Regular PyGame event loop:
Asyncified PyGame event loop:
This is a problem because PyGame applications cannot be run as-is in the web browser. They must be first edited to use a non-standard structure that only works in the browser.
Imagine if PyGame provided a new event loop API that worked both inside AND outside the web browser. SDL 3 has a new main callbacks API which allows an SDL-based program using it to run normally both instead and outside a browser. Perhaps PyGame could provide a new event loop API that could target this new main callbacks API.
The text was updated successfully, but these errors were encountered: