Skip to content

Commit bb0df01

Browse files
committed
desktop: Use async-task crate to manage Player-created futures
Instead of manually (and unsafely!) fiddling with `Waker` vtables and implementing our own task "allocator" (a.k.a. the `SlotMap` in `AsyncExecutor`), rip everything out and use heap-allocated `PlayerRunnable = async_task::Runnable`s instead. `async-task` takes care of all the nitty-gritty and provides an API we can directly plug into the `winit` event loop. Notes: - This removes the internal queue of to-be-polled tasks; instead, `PlayerRunnable`s get pushed as individual winit events directly on the main event loop. - To prevent polling tasks belonging to stale players, we tag each task with a `PlayerId` uniquely identifying each `Player` instance, and only poll the task if its ID matches the active `Player`.
1 parent 7e7fce6 commit bb0df01

File tree

10 files changed

+107
-381
lines changed

10 files changed

+107
-381
lines changed

Cargo.lock

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ ruffle_render = { path = "../render", features = ["clap"] }
2424
ruffle_render_wgpu = { path = "../render/wgpu", features = ["clap"] }
2525
ruffle_video_software = { path = "../video/software", optional = true }
2626
ruffle_video_external = { path = "../video/external", features = ["openh264"], optional = true }
27-
ruffle_frontend_utils = { path = "../frontend-utils", features = ["cpal", "fs", "navigator", "executor"] }
27+
ruffle_frontend_utils = { path = "../frontend-utils", features = ["cpal", "fs", "navigator"] }
2828
tracing = { workspace = true }
2929
tracing-subscriber = { workspace = true }
3030
tracing-appender = "0.2.3"
@@ -54,6 +54,7 @@ unicode-bidi = "0.3.18"
5454
fontconfig = { version = "0.10.0", optional = true, features = ["dlopen"]}
5555
memmap2.workspace = true
5656
walkdir.workspace = true
57+
async-task = "4.7.1"
5758

5859
[target.'cfg(target_os = "linux")'.dependencies]
5960
ashpd = "0.11.0"

desktop/src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ impl ApplicationHandler<RuffleEvent> for App {
534534

535535
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RuffleEvent) {
536536
match (&mut self.main_window, event) {
537-
(Some(main_window), RuffleEvent::TaskPoll) => main_window.player.poll(),
537+
(Some(main_window), RuffleEvent::TaskPoll(task)) => main_window.player.poll(task),
538538

539539
(Some(main_window), RuffleEvent::OnMetadata(swf_header)) => {
540540
main_window.on_metadata(swf_header)

desktop/src/custom_event.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
33
use ruffle_core::events::PlayerNotification;
44

5-
use crate::{gui::DialogDescriptor, player::LaunchOptions};
5+
use crate::gui::DialogDescriptor;
6+
use crate::player::{LaunchOptions, PlayerRunnable};
67

78
/// User-defined events.
89
pub enum RuffleEvent {
9-
/// Indicates that one or more tasks are ready to poll on our executor.
10-
TaskPoll,
10+
/// Indicates that a task is ready to be polled.
11+
TaskPoll(PlayerRunnable),
1112

1213
/// Indicates that an asynchronous SWF metadata load has been completed.
1314
OnMetadata(ruffle_core::swf::HeaderExt),

desktop/src/player.rs

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ use crate::gui::{FilePicker, MovieView};
99
use crate::preferences::GlobalPreferences;
1010
use crate::{CALLSTACK, RENDER_INFO, SWF_INFO};
1111
use anyhow::anyhow;
12-
use ruffle_core::backend::navigator::SocketMode;
12+
use ruffle_core::backend::navigator::{OwnedFuture, SocketMode};
1313
use ruffle_core::config::Letterbox;
1414
use ruffle_core::events::{GamepadButton, KeyCode};
1515
use ruffle_core::{DefaultFont, LoadBehavior, Player, PlayerBuilder, PlayerEvent};
1616
use ruffle_frontend_utils::backends::audio::CpalAudioBackend;
17-
use ruffle_frontend_utils::backends::executor::{AsyncExecutor, PollRequester};
18-
use ruffle_frontend_utils::backends::navigator::ExternalNavigatorBackend;
17+
use ruffle_frontend_utils::backends::navigator::{ExternalNavigatorBackend, FutureSpawner};
1918
use ruffle_frontend_utils::bundle::source::BundleSourceError;
2019
use ruffle_frontend_utils::bundle::{Bundle, BundleError};
2120
use ruffle_frontend_utils::content::PlayingContent;
@@ -105,22 +104,11 @@ impl From<&GlobalPreferences> for LaunchOptions {
105104
}
106105
}
107106

108-
#[derive(Clone)]
109-
struct WinitWaker(EventLoopProxy<RuffleEvent>);
110-
111-
impl PollRequester for WinitWaker {
112-
fn request_poll(&self) {
113-
if self.0.send_event(RuffleEvent::TaskPoll).is_err() {
114-
tracing::error!("Couldn't request poll - event loop is closed");
115-
}
116-
}
117-
}
118-
119107
/// Represents a current Player and any associated state with that player,
120108
/// which may be lost when this Player is closed (dropped)
121109
struct ActivePlayer {
110+
id: PlayerId,
122111
player: Arc<Mutex<Player>>,
123-
executor: Arc<AsyncExecutor<WinitWaker>>,
124112

125113
#[cfg(target_os = "linux")]
126114
_gamemode_session: crate::dbus::GameModeSession,
@@ -139,6 +127,7 @@ impl ActivePlayer {
139127
preferences: GlobalPreferences,
140128
file_picker: FilePicker,
141129
) -> Self {
130+
let player_id = PlayerId::new();
142131
let mut builder = PlayerBuilder::new();
143132

144133
match CpalAudioBackend::new(preferences.output_device_name().as_deref()) {
@@ -211,7 +200,10 @@ impl ActivePlayer {
211200
}
212201
};
213202

214-
let (executor, future_spawner) = AsyncExecutor::new(WinitWaker(event_loop.clone()));
203+
let future_spawner = WinitExecutor {
204+
event_loop: event_loop.clone(),
205+
player_id,
206+
};
215207
let movie_url = content.initial_swf_url().clone();
216208
let readable_name = content.name();
217209
let navigator = ExternalNavigatorBackend::new(
@@ -419,8 +411,8 @@ impl ActivePlayer {
419411
}
420412

421413
Self {
414+
id: player_id,
422415
player,
423-
executor,
424416
#[cfg(target_os = "linux")]
425417
_gamemode_session: crate::dbus::GameModeSession::new(gamemode_enable),
426418
}
@@ -500,9 +492,67 @@ impl PlayerController {
500492
false
501493
}
502494

503-
pub fn poll(&self) {
504-
if let Some(player) = &self.player {
505-
player.executor.poll_all()
495+
pub fn poll(&self, task: PlayerRunnable) {
496+
// Only run the task if it matches our current player;
497+
// otherwise it is stale, and should be cancelled (which
498+
// happens implicitly on drop).
499+
if let Some(player) = &self.player
500+
&& *task.0.metadata() == player.id
501+
{
502+
task.0.run();
506503
}
507504
}
508505
}
506+
507+
/// A unique identifier for a given `Player` instance.
508+
/// Used to track which player any currently executing future is bound to.
509+
#[derive(Copy, Clone, Eq, PartialEq)]
510+
struct PlayerId(i64);
511+
512+
impl PlayerId {
513+
fn new() -> Self {
514+
use std::sync::atomic::{AtomicI64, Ordering};
515+
516+
static NEXT: AtomicI64 = AtomicI64::new(0);
517+
let id = NEXT.fetch_add(1, Ordering::Relaxed);
518+
assert!(id >= 0, "PlayerId overflowed!");
519+
Self(id)
520+
}
521+
}
522+
523+
/// A `Player`-bound future that is currently running.
524+
pub struct PlayerRunnable(async_task::Runnable<PlayerId>);
525+
526+
/// A bare-bones executor that schedules tasks on the winit event loop.
527+
struct WinitExecutor {
528+
event_loop: EventLoopProxy<RuffleEvent>,
529+
player_id: PlayerId,
530+
}
531+
532+
impl<E: std::error::Error + 'static> FutureSpawner<E> for WinitExecutor {
533+
fn spawn(&self, future: OwnedFuture<(), E>) {
534+
// Discard any errors.
535+
let future = async {
536+
if let Err(e) = future.await {
537+
tracing::error!("Async error: {}", e);
538+
}
539+
};
540+
541+
let event_loop = self.event_loop.clone();
542+
let scheduler = move |task| {
543+
let event = RuffleEvent::TaskPoll(PlayerRunnable(task));
544+
if event_loop.send_event(event).is_err() {
545+
tracing::error!("Couldn't schedule task - event loop is closed");
546+
}
547+
};
548+
549+
let (runnable, task) = async_task::Builder::new()
550+
.metadata(self.player_id)
551+
.spawn_local(|_| future, scheduler);
552+
553+
// The future should run in the background.
554+
task.detach();
555+
// Immediately schedule the future to be polled for the first time.
556+
runnable.schedule();
557+
}
558+
}

frontend-utils/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ workspace = true
1414
cpal = ["dep:cpal", "dep:bytemuck", "ruffle_core/audio"]
1515
fs = []
1616
navigator = ["fs", "dep:async-io", "dep:tokio"]
17-
executor = ["dep:tokio"]
1817

1918
[dependencies]
2019
toml_edit = { version = "0.23.6", features = ["parse"] }
@@ -26,7 +25,6 @@ urlencoding = "2.1.3"
2625
ruffle_core = { path = "../core", default-features = false }
2726
ruffle_render = { path = "../render", default-features = false }
2827
async-channel = { workspace = true }
29-
slotmap = { workspace = true }
3028
async-io = { version = "2.6.0", optional = true }
3129
futures-lite = "2.6.1"
3230
reqwest = { version = "0.12.24", default-features = false, features = [

frontend-utils/src/backends.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#[cfg(feature = "cpal")]
22
pub mod audio;
3-
#[cfg(feature = "executor")]
4-
pub mod executor;
53
#[cfg(feature = "navigator")]
64
pub mod navigator;
75
#[cfg(feature = "fs")]

0 commit comments

Comments
 (0)