diff --git a/Cargo.lock b/Cargo.lock index 85769e0034e6..8ad7e1704591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,22 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.9.0" +source = "git+https://github.com/bilelmoussaoui/ashpd.git?rev=34c0ab8f83cd16c3190ecc4cc51021daa531d89b#34c0ab8f83cd16c3190ecc4cc51021daa531d89b" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + [[package]] name = "ashpd" version = "0.9.1" @@ -4233,7 +4249,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8af382a047821a08aa6bfc09ab0d80ff48d45d8726f7cd8e44891f7cb4a4278e" dependencies = [ - "ashpd", + "ashpd 0.9.1", "block2", "js-sys", "log", @@ -4342,6 +4358,7 @@ name = "ruffle_desktop" version = "0.1.0" dependencies = [ "anyhow", + "ashpd 0.9.0", "bytemuck", "chrono", "clap", @@ -5459,8 +5476,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -6842,6 +6861,7 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", diff --git a/deny.toml b/deny.toml index 4f2349eddcfe..c3cf2527c50a 100644 --- a/deny.toml +++ b/deny.toml @@ -77,6 +77,8 @@ unknown-git = "deny" # github.com organizations to allow git sources for github = [ "ruffle-rs", + # TODO: Remove once a release with https://github.com/bilelmoussaoui/ashpd/pull/234 in it is out. + "bilelmoussaoui", ] [advisories] diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 3c4eae939fe5..760996f8f25c 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -53,6 +53,7 @@ thiserror.workspace = true [target.'cfg(target_os = "linux")'.dependencies] zbus = "4.4.0" +ashpd = { git = "https://github.com/bilelmoussaoui/ashpd.git", rev = "34c0ab8f83cd16c3190ecc4cc51021daa531d89b" } [target.'cfg(windows)'.dependencies] winapi = "0.3.9" diff --git a/desktop/assets/texts/en-US/common.ftl b/desktop/assets/texts/en-US/common.ftl index ffb29bafd1f3..32788d43b451 100644 --- a/desktop/assets/texts/en-US/common.ftl +++ b/desktop/assets/texts/en-US/common.ftl @@ -7,4 +7,4 @@ cancel = Cancel remove = Remove enable = Enable -disable = Disable \ No newline at end of file +disable = Disable diff --git a/desktop/assets/texts/en-US/preferences_dialog.ftl b/desktop/assets/texts/en-US/preferences_dialog.ftl index 4ddd478e665a..6b3882505f81 100644 --- a/desktop/assets/texts/en-US/preferences_dialog.ftl +++ b/desktop/assets/texts/en-US/preferences_dialog.ftl @@ -33,3 +33,11 @@ theme = Theme theme-system = System Default theme-light = Light theme-dark = Dark + +# See for context https://github.com/FeralInteractive/gamemode +gamemode = GameMode +gamemode-tooltip = + GameMode temporarily applies a set of optimizations to your computer and/or Ruffle. + Ruffle requests GameMode only when a movie is being played. +gamemode-default = Default +gamemode-default-tooltip = GameMode will be enabled only when power preference is set to high. diff --git a/desktop/src/cli.rs b/desktop/src/cli.rs index d540150add7c..4a81581589b8 100644 --- a/desktop/src/cli.rs +++ b/desktop/src/cli.rs @@ -9,6 +9,7 @@ use ruffle_core::{LoadBehavior, PlayerRuntime, StageAlign, StageScaleMode}; use ruffle_render::quality::StageQuality; use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference}; use std::path::Path; +use std::str::FromStr; use std::time::Duration; use url::Url; @@ -62,6 +63,19 @@ pub struct Opt { #[clap(long, short)] pub power: Option, + /// GameMode preference. + /// + /// This allows enabling or disabling GameMode manually. + /// When enabled, GameMode will be requested only when a movie is loaded. + /// + /// The default preference enables GameMode when power preference is set to high. + /// This option temporarily overrides any stored preference. + /// + /// See . + #[clap(long)] + #[cfg_attr(not(target_os = "linux"), clap(hide = true))] + pub gamemode: Option, + /// Type of storage backend to use. This determines where local storage data is saved (e.g. shared objects). /// /// This option temporarily overrides any stored preference. @@ -298,6 +312,36 @@ impl Opt { } } +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, clap::ValueEnum)] +pub enum GameModePreference { + #[default] + Default, + On, + Off, +} + +impl GameModePreference { + pub fn as_str(&self) -> Option<&'static str> { + match self { + GameModePreference::Default => None, + GameModePreference::On => Some("on"), + GameModePreference::Off => Some("off"), + } + } +} + +impl FromStr for GameModePreference { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "on" => Ok(GameModePreference::On), + "off" => Ok(GameModePreference::Off), + _ => Err(()), + } + } +} + // TODO The following enum exists in order to preserve // the behavior of mapping gamepad buttons, // We should probably do something smarter here. diff --git a/desktop/src/dbus.rs b/desktop/src/dbus.rs index 7280d3be3778..34267378ee7a 100644 --- a/desktop/src/dbus.rs +++ b/desktop/src/dbus.rs @@ -1,6 +1,9 @@ #![cfg(target_os = "linux")] //! Types and methods utilized for communicating with D-Bus +use std::mem; +use std::sync::{Arc, Mutex}; + use futures::StreamExt; use zbus::export::futures_core::Stream; use zbus::zvariant::{OwnedValue, Value}; @@ -80,3 +83,56 @@ impl<'p> FreedesktopSettings<'p> { } } } + +pub struct GameModeSession { + _guard: Arc>>, +} + +impl GameModeSession { + pub fn new(enabled: bool) -> Self { + let guard = Arc::new(Mutex::new(None)); + let guard2 = guard.clone(); + tokio::spawn(async move { + let game_mode_guard = GameModeGuard::new(enabled).await; + *guard2.lock().expect("Non-poisoned gamemode guard") = Some(game_mode_guard); + }); + Self { _guard: guard } + } +} + +struct GameModeGuard { + gamemode: Option>, +} + +impl GameModeGuard { + async fn new(enabled: bool) -> Self { + if !enabled { + return Self { gamemode: None }; + } + + let gamemode = ashpd::desktop::game_mode::GameMode::new() + .await + .inspect_err(|err| tracing::warn!("Failed to initialize gamemode controller: {}", err)) + .ok(); + + if let Some(gamemode) = &gamemode { + if let Err(err) = gamemode.register(std::process::id()).await { + tracing::warn!("Failed to register a game with gamemode: {}", err) + } + } + + Self { gamemode } + } +} + +impl Drop for GameModeGuard { + fn drop(&mut self) { + if let Some(gamemode) = mem::take(&mut self.gamemode) { + tokio::spawn(async move { + if let Err(err) = gamemode.unregister(std::process::id()).await { + tracing::warn!("Failed to unregister a game with gamemode: {}", err) + } + }); + } + } +} diff --git a/desktop/src/gui/dialogs/preferences_dialog.rs b/desktop/src/gui/dialogs/preferences_dialog.rs index e82eaba82728..e1da9105a7b6 100644 --- a/desktop/src/gui/dialogs/preferences_dialog.rs +++ b/desktop/src/gui/dialogs/preferences_dialog.rs @@ -1,3 +1,4 @@ +use crate::cli::GameModePreference; use crate::gui::{available_languages, optional_text, text, ThemePreference}; use crate::log::FilenamePattern; use crate::preferences::{storage::StorageBackend, GlobalPreferences}; @@ -19,6 +20,10 @@ pub struct PreferencesDialog { power_preference_readonly: bool, power_preference_changed: bool, + gamemode_preference: GameModePreference, + gamemode_preference_readonly: bool, + gamemode_preference_changed: bool, + language: LanguageIdentifier, language_changed: bool, @@ -68,6 +73,10 @@ impl PreferencesDialog { power_preference_readonly: preferences.cli.power.is_some(), power_preference_changed: false, + gamemode_preference: preferences.gamemode_preference(), + gamemode_preference_readonly: preferences.cli.gamemode.is_some(), + gamemode_preference_changed: false, + language: preferences.language(), language_changed: false, @@ -114,6 +123,10 @@ impl PreferencesDialog { .show(ui, |ui| { self.show_graphics_preferences(locale, &locked_text, ui); + if cfg!(target_os = "linux") { + self.show_gamemode_preferences(locale, &locked_text, ui); + } + self.show_language_preferences(locale, ui); self.show_theme_preferences(locale, ui); @@ -289,6 +302,46 @@ impl PreferencesDialog { ui.end_row(); } + fn show_gamemode_preferences( + &mut self, + locale: &LanguageIdentifier, + locked_text: &str, + ui: &mut Ui, + ) { + ui.label(text(locale, "gamemode")) + .on_hover_text_at_pointer(text(locale, "gamemode-tooltip")); + if self.gamemode_preference_readonly { + ui.label(gamemode_preference_name(locale, self.gamemode_preference)) + .on_hover_text(locked_text); + } else { + let previous = self.gamemode_preference; + ComboBox::from_id_salt("gamemode") + .selected_text(gamemode_preference_name(locale, self.gamemode_preference)) + .show_ui(ui, |ui| { + let values = [ + GameModePreference::Default, + GameModePreference::On, + GameModePreference::Off, + ]; + for value in values { + let response = ui.selectable_value( + &mut self.gamemode_preference, + value, + gamemode_preference_name(locale, value), + ); + + if let Some(tooltip) = gamemode_preference_tooltip(locale, value) { + response.on_hover_text_at_pointer(tooltip); + } + } + }); + if self.gamemode_preference != previous { + self.gamemode_preference_changed = true; + } + } + ui.end_row(); + } + fn show_audio_preferences(&mut self, locale: &LanguageIdentifier, ui: &mut Ui) { ui.label(text(locale, "audio-output-device")); @@ -459,6 +512,9 @@ impl PreferencesDialog { if self.theme_preference_changed { preferences.set_theme_preference(self.theme_preference); } + if self.gamemode_preference_changed { + preferences.set_gamemode_preference(self.gamemode_preference); + } }) { // [NA] TODO: Better error handling... everywhere in desktop, really tracing::error!("Could not save preferences: {e}"); @@ -500,6 +556,27 @@ fn theme_preference_name( } } +fn gamemode_preference_name( + locale: &LanguageIdentifier, + gamemode_preference: GameModePreference, +) -> Cow { + match gamemode_preference { + GameModePreference::Default => text(locale, "gamemode-default"), + GameModePreference::On => text(locale, "enable"), + GameModePreference::Off => text(locale, "disable"), + } +} + +fn gamemode_preference_tooltip( + locale: &LanguageIdentifier, + gamemode_preference: GameModePreference, +) -> Option> { + Some(match gamemode_preference { + GameModePreference::Default => text(locale, "gamemode-default-tooltip"), + _ => return None, + }) +} + fn filename_pattern_name(locale: &LanguageIdentifier, pattern: FilenamePattern) -> Cow { match pattern { FilenamePattern::SingleFile => text(locale, "log-filename-pattern-single-file"), diff --git a/desktop/src/player.rs b/desktop/src/player.rs index 848440809e01..ecc3f4ba7f56 100644 --- a/desktop/src/player.rs +++ b/desktop/src/player.rs @@ -3,6 +3,7 @@ use crate::backends::{ DesktopUiBackend, }; use crate::cli::FilesystemAccessMode; +use crate::cli::GameModePreference; use crate::custom_event::RuffleEvent; use crate::gui::{FilePicker, MovieView}; use crate::preferences::GlobalPreferences; @@ -23,6 +24,7 @@ use ruffle_frontend_utils::recents::Recent; use ruffle_render::backend::RenderBackend; use ruffle_render::quality::StageQuality; use ruffle_render_wgpu::backend::WgpuRenderBackend; +use ruffle_render_wgpu::clap::PowerPreference; use ruffle_render_wgpu::descriptors::Descriptors; use std::borrow::Cow; use std::collections::{HashMap, HashSet}; @@ -121,6 +123,9 @@ impl PollRequester for WinitWaker { struct ActivePlayer { player: Arc>, executor: Arc>, + + #[cfg(target_os = "linux")] + _gamemode_session: crate::dbus::GameModeSession, } impl ActivePlayer { @@ -259,6 +264,18 @@ impl ActivePlayer { } } + let gamemode_enable = match preferences.gamemode_preference() { + GameModePreference::Default => { + preferences.graphics_power_preference() == PowerPreference::High + } + GameModePreference::On => true, + GameModePreference::Off => false, + }; + + if cfg!(not(target_os = "linux")) && gamemode_enable { + tracing::warn!("Cannot enable GameMode, as it is supported only on Linux"); + } + let renderer = WgpuRenderBackend::new(descriptors, movie_view) .map_err(|e| anyhow!(e.to_string())) .expect("Couldn't create wgpu rendering backend"); @@ -389,7 +406,12 @@ impl ActivePlayer { ); } - Self { player, executor } + Self { + player, + executor, + #[cfg(target_os = "linux")] + _gamemode_session: crate::dbus::GameModeSession::new(gamemode_enable), + } } } diff --git a/desktop/src/preferences.rs b/desktop/src/preferences.rs index 7553b907eab6..a43e1e1390fa 100644 --- a/desktop/src/preferences.rs +++ b/desktop/src/preferences.rs @@ -3,7 +3,7 @@ mod write; pub mod storage; -use crate::cli::Opt; +use crate::cli::{GameModePreference, Opt}; use crate::gui::ThemePreference; use crate::log::FilenamePattern; use crate::preferences::read::read_preferences; @@ -119,6 +119,15 @@ impl GlobalPreferences { }) } + pub fn gamemode_preference(&self) -> GameModePreference { + self.cli.gamemode.unwrap_or_else(|| { + self.preferences + .lock() + .expect("Non-poisoned preferences") + .gamemode_preference + }) + } + pub fn language(&self) -> LanguageIdentifier { self.preferences .lock() @@ -250,6 +259,7 @@ impl GlobalPreferences { pub struct SavedGlobalPreferences { pub graphics_backend: GraphicsBackend, pub graphics_power_preference: PowerPreference, + pub gamemode_preference: GameModePreference, pub language: LanguageIdentifier, pub output_device: Option, pub mute: bool, @@ -271,6 +281,7 @@ impl Default for SavedGlobalPreferences { Self { graphics_backend: Default::default(), graphics_power_preference: Default::default(), + gamemode_preference: Default::default(), language: locale, output_device: None, mute: false, diff --git a/desktop/src/preferences/read.rs b/desktop/src/preferences/read.rs index 21430f885d95..8a766686a9ed 100644 --- a/desktop/src/preferences/read.rs +++ b/desktop/src/preferences/read.rs @@ -63,6 +63,10 @@ pub fn read_preferences(input: &str) -> ParseDetails { result.theme_preference = value; } + if let Some(value) = document.parse_from_str(&mut cx, "gamemode") { + result.gamemode_preference = value; + } + document.get_table_like(&mut cx, "log", |cx, log| { if let Some(value) = log.parse_from_str(cx, "filename_pattern") { result.log.filename_pattern = value; @@ -84,6 +88,7 @@ pub fn read_preferences(input: &str) -> ParseDetails { #[cfg(test)] mod tests { use super::*; + use crate::cli::GameModePreference; use crate::gui::ThemePreference; use crate::log::FilenamePattern; use crate::preferences::{storage::StorageBackend, LogPreferences, StoragePreferences}; @@ -586,4 +591,60 @@ mod tests { result.warnings ); } + + #[test] + fn gamemode() { + let result = read_preferences("gamemode = \"on\""); + assert_eq!( + &SavedGlobalPreferences { + gamemode_preference: GameModePreference::On, + ..Default::default() + }, + result.values() + ); + assert_eq!(Vec::::new(), result.warnings); + + let result = read_preferences("gamemode = \"off\""); + assert_eq!( + &SavedGlobalPreferences { + gamemode_preference: GameModePreference::Off, + ..Default::default() + }, + result.values() + ); + assert_eq!(Vec::::new(), result.warnings); + + let result = read_preferences("gamemode = \"default\""); + assert_eq!( + &SavedGlobalPreferences { + gamemode_preference: GameModePreference::Default, + ..Default::default() + }, + result.values() + ); + assert_eq!( + vec![ParseWarning::UnsupportedValue { + value: "default".to_string(), + path: "gamemode".to_string(), + }], + result.warnings + ); + + let result = read_preferences("gamemode = 1"); + assert_eq!( + &SavedGlobalPreferences { + gamemode_preference: GameModePreference::Default, + ..Default::default() + }, + result.values() + ); + assert_eq!( + vec![ParseWarning::UnexpectedType { + expected: "string", + actual: "integer", + path: "gamemode".to_string(), + }], + result.warnings + ); + } } diff --git a/desktop/src/preferences/write.rs b/desktop/src/preferences/write.rs index 77effbaed397..bfcb3da6173e 100644 --- a/desktop/src/preferences/write.rs +++ b/desktop/src/preferences/write.rs @@ -1,3 +1,4 @@ +use crate::cli::GameModePreference; use crate::gui::ThemePreference; use crate::log::FilenamePattern; use crate::preferences::storage::StorageBackend; @@ -108,6 +109,17 @@ impl<'a> PreferencesWriter<'a> { let _ = watcher.send(theme_preference); } } + + pub fn set_gamemode_preference(&mut self, gamemode_preference: GameModePreference) { + self.0.edit(|values, toml_document| { + if let Some(gamemode_preference) = gamemode_preference.as_str() { + toml_document["gamemode"] = value(gamemode_preference); + } else { + toml_document.remove("gamemode"); + } + values.gamemode_preference = gamemode_preference; + }); + } } #[cfg(test)] @@ -276,4 +288,18 @@ mod tests { "", ); } + + #[test] + fn set_gamemode() { + test( + "gamemode = 6\n", + |writer| writer.set_gamemode_preference(GameModePreference::Off), + "gamemode = \"off\"\n", + ); + test( + "gamemode = \"on\"", + |writer| writer.set_gamemode_preference(GameModePreference::Default), + "", + ); + } }