/** * @file src/process.h * @brief Declarations for the startup and shutdown of the apps started by a streaming Session. */ #pragma once #ifndef __kernel_entry #define __kernel_entry #endif // standard includes #include #include #include #include #include #include #include #include #include // lib includes #include "boost_process_shim.h" #include // local includes #include "config.h" #include "platform/common.h" #include "rtsp.h" #include "steam_process_tracker.h" #include "utility.h" #ifdef _WIN32 #include "platform/windows/virtual_display.h" #include "tools/playnite_launcher/lossless_scaling.h" namespace VDISPLAY { enum class DRIVER_STATUS; enum class DRIVER_SELECTION; } #endif #define VIRTUAL_DISPLAY_UUID "8902CB19-674A-403D-A587-41B092E900BA" #define FALLBACK_DESKTOP_UUID "EAAC6159-089A-46A9-9E24-6436885F6610" #define REMOTE_INPUT_UUID "8CB5C136-DA67-4F99-B4A1-F9CD35005CF4" #define TERMINATE_APP_UUID "E16CBE1B-295D-4632-9A76-EC4180C857D3" namespace bp = boost_process_shim; #define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png" namespace proc { using file_t = util::safe_ptr_v2; #ifdef _WIN32 extern std::atomic vDisplayDriverStatus; extern std::atomic vDisplayDriverSelection; struct vdisplay_driver_status_snapshot_t { VDISPLAY::DRIVER_STATUS status; VDISPLAY::DRIVER_SELECTION selection; }; vdisplay_driver_status_snapshot_t vDisplayDriverStatusSnapshot(); void setVDisplayDriverStatus(VDISPLAY::DRIVER_STATUS status); void setVDisplayDriverStatus(VDISPLAY::DRIVER_STATUS status, VDISPLAY::DRIVER_SELECTION selection); void initVDisplayDriver(); #endif typedef config::prep_cmd_t cmd_t; inline constexpr int kLosslessScalingDefaultLaunchDelaySeconds = 8; struct active_session_guard_t { bool has_active_app {false}; bool uses_playnite {false}; std::string playnite_id; std::string client_uuid; std::uint64_t normal_vdd_identity_token {0}; std::chrono::steady_clock::time_point launch_started_at {}; }; #ifdef _WIN32 struct running_app_state_t { bool has_active_app {false}; bool trackable {false}; bool uses_playnite {false}; std::string playnite_id; std::string name; std::string command; std::string working_dir; uint32_t root_pid {0}; }; #endif /** * pre_cmds -- guaranteed to be executed unless any of the commands fail. * detached -- commands detached from Sunshine * cmd -- Runs indefinitely until: * No session is running and a different set of commands it to be executed * Command exits * working_dir -- the process working directory. This is required for some games to run properly. * cmd_output -- * empty -- The output of the commands are appended to the output of sunshine * "null" -- The output of the commands are discarded * filename -- The output of the commands are appended to filename */ struct lossless_scaling_profile_overrides_t { std::optional performance_mode; std::optional flow_scale; std::optional resolution_scale; std::optional scaling_type; std::optional sharpening; std::optional anime4k_size; std::optional anime4k_vrs; }; struct ctx_t { std::vector prep_cmds; std::vector state_cmds; /** * Some applications, such as Steam, either exit quickly, or keep running indefinitely. * * Apps that launch normal child processes and terminate will be handled by the process * grouping logic (wait_all). However, apps that launch child processes indirectly or * into another process group (such as UWP apps) can only be handled by the auto-detach * heuristic which catches processes that exit 0 very quickly, but we won't have proper * process tracking for those. * * For cases where users just want to kick off a background process and never manage the * lifetime of that process, they can use detached commands for that. */ std::vector detached; std::string idx; std::string uuid; std::string name; std::string cmd; std::string working_dir; std::string output; std::optional output_name_override; std::string image_path; std::string id; std::string gamepad; std::string art_version; std::vector id_aliases; // Provider metadata used by the Steam-owned process lifecycle boundary. // These remain strings because the catalog/API stores Steam IDs and paths // as JSON strings for compatibility with older app entries. std::string steam_id; std::string steam_install_dir; // Lutris also launches through a short-lived broker. Its game directory // lets the shared process tracker own the real Wine/native process. std::string lutris_id; std::string lutris_directory; // When present, this app should be launched via Playnite instead of direct cmd. std::string playnite_id; // When true, launch Playnite in fullscreen mode via the helper. bool playnite_fullscreen; bool frame_gen_limiter_fix; bool elevated; bool virtual_screen {false}; // Unset inherits the device preference; false allows normal client HDR requests. std::optional prefer_10bit_sdr; std::optional virtual_display_mode_override; std::optional virtual_display_layout_override; bool auto_detach; bool wait_all; bool virtual_display; bool virtual_display_primary; bool use_app_identity; bool per_client_app_identity; bool allow_client_commands; bool terminate_on_pause; int scale_factor; std::chrono::seconds exit_timeout; bool gen1_framegen_fix; bool gen2_framegen_fix; bool frame_generation_enabled {false}; bool lossless_scaling_enabled {false}; bool lossless_scaling_framegen {false}; std::string frame_generation_provider {"lossless-scaling"}; std::optional lossless_scaling_target_fps; std::optional lossless_scaling_rtss_limit; std::string lossless_scaling_profile {"custom"}; lossless_scaling_profile_overrides_t lossless_scaling_recommended; lossless_scaling_profile_overrides_t lossless_scaling_custom; int lossless_scaling_launch_delay_seconds {kLosslessScalingDefaultLaunchDelaySeconds}; bool lossless_scaling_legacy_auto_detect {false}; std::optional dd_config_option_override; // Per-application overrides for global config keys (raw config-file value representation). // These are applied at runtime and are not persisted to the global config file. std::unordered_map config_overrides; }; class proc_t { public: proc_t() = default; proc_t(proc_t &&other) noexcept; proc_t &operator=(proc_t &&other) noexcept; std::string display_name; std::string initial_display; bool virtual_display = false; bool allow_client_commands = false; proc_t( bp::environment &&env, std::vector &&apps ): _env(std::move(env)), _apps(std::move(apps)) { } void launch_input_only(); int execute(const ctx_t &_app, std::shared_ptr launch_session); /** * @return `_app_id` if a process is running, otherwise returns `0` */ int running(); /** * @return A side-effect-free snapshot of the current application ID. */ int current_app_id() const; ~proc_t(); // Return a snapshot copy to avoid concurrent access races active_session_guard_t active_session_guard() const; std::vector get_apps() const; std::string get_app_image(int app_id); std::optional resolve_app(const std::string &appid, const std::string &appuuid = "") const; std::optional resolve_app(int app_id) const; std::string get_last_run_app_name(); std::string get_running_app_uuid(); bp::environment get_env(); void resume(); void pause(bool stream_lifecycle_lock_held = false); void terminate( bool immediate = false, bool needs_refresh = true, bool skip_display_revert = false, bool stream_lifecycle_lock_held = false ); bool last_run_app_frame_gen_limiter_fix() const; bool is_launch_deferred() const; bool has_trackable_running_app() const; bool foreground_window_matches_running_app(); #ifdef _WIN32 running_app_state_t running_app_state() const; bool running_app_contains_pid(uint32_t pid); #endif // Hot-update app list and environment without disrupting a running app void update_apps(std::vector &&apps, bp::environment &&env); #ifdef _WIN32 bool update_active_app_live_rtx_hdr_overrides(const std::string &app_uuid); bool update_active_app_live_rtx_hdr_overrides( const std::string &app_uuid, const std::unordered_map &rtx_hdr_overrides ); #endif // Helpers for parse/refresh to extract newly parsed state without exposing internals std::vector release_apps(); bp::environment release_env(); private: int launch_app_commands(bool stream_lifecycle_lock_held); std::atomic _app_id {0}; std::string _app_name; bp::environment _env; std::shared_ptr _launch_session; std::shared_ptr _saved_input_config; // Entries inserted by a prior stream must not leak into a later launch, // while values loaded from apps.json remain explicit user configuration. std::unordered_set _stream_owned_environment_keys; std::vector _apps; ctx_t _app; std::chrono::steady_clock::time_point _app_launch_time; std::string _active_client_uuid; std::uint64_t _active_client_vdd_identity_token {0}; mutable std::mutex _apps_mutex; // If no command associated with _app_id, yet it's still running bool placebo {}; platf::steam::lifecycle::tracker _steam_tracker; std::shared_ptr _steam_process_controller; bool _steam_tracking_active {false}; bool _steam_tracking_associated {false}; std::chrono::steady_clock::time_point _steam_tracking_deadline {}; std::chrono::steady_clock::time_point _steam_last_tracking_poll {}; #ifdef _WIN32 bool _deferred_launch {false}; bool _lossless_should_start_support {false}; playnite_launcher::lossless::lossless_scaling_app_metadata _lossless_metadata {}; #endif bp::child _process; bp::group _process_group; #ifdef _WIN32 GUID _virtual_display_guid {}; bool _virtual_display_active {false}; std::optional _runtime_output_override_lease; #endif file_t _pipe; std::vector::const_iterator _app_prep_it; std::vector::const_iterator _app_prep_begin; #ifdef _WIN32 void start_lossless_scaling_support(std::unordered_set baseline_pids, const playnite_launcher::lossless::lossless_scaling_app_metadata &metadata, std::string install_dir_hint_utf8, DWORD root_pid); void stop_lossless_scaling_support(); std::thread _lossless_thread; std::atomic_bool _lossless_stop_requested {false}; std::mutex _lossless_mutex; bool _lossless_profile_applied {false}; playnite_launcher::lossless::lossless_scaling_profile_backup _lossless_backup {}; std::string _lossless_last_install_dir; std::string _lossless_last_exe_path; #endif }; boost::filesystem::path find_working_directory(const std::string &cmd, const bp::environment &env); /** * @brief Calculate a stable id based on UUID when present, otherwise legacy name and image data. * @return Tuple of id calculated without index (for use if no collision) and one with. */ std::tuple calculate_app_id(const std::string &app_name, const std::string &app_uuid, std::string app_image_path, int index); bool check_valid_png(const std::filesystem::path &path); std::string validate_app_image_path(std::string app_image_path); std::optional read_validated_app_image(const std::string &validated_path); std::string calculate_app_cover_fingerprint(std::string app_image_path); void refresh(const std::string &file_name, bool needs_terminate = true); void migrate_apps(nlohmann::json *fileTree_p, nlohmann::json *inputTree_p); std::optional parse(const std::string &file_name); /** * @brief Initialize proc functions * @return Unique pointer to `deinit_t` to manage cleanup */ std::unique_ptr init(); /** * @brief Terminates all child processes in a process group. * @param proc The child process itself. * @param group The group of all children in the process tree. * @param exit_timeout The timeout to wait for the process group to gracefully exit. */ void terminate_process_group(bp::child &proc, bp::group &group, std::chrono::seconds exit_timeout); #ifdef _WIN32 /** Mark an app-triggered display revert to run after the final stream ends. */ void defer_display_revert(); /** Consume a previously deferred app-triggered display revert exactly once. */ bool consume_deferred_display_revert(); /** Discard a deferred revert when a replacement app takes ownership of the display. */ void clear_deferred_display_revert(); #endif extern proc_t proc; extern int input_only_app_id; extern std::string input_only_app_id_str; extern int terminate_app_id; extern std::string terminate_app_id_str; } // namespace proc