/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #ifndef XENIA_UI_PRESENTER_H_ #define XENIA_UI_PRESENTER_H_ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "xenia/base/assert.h" #include "xenia/base/byte_order.h" #include "xenia/base/cvar.h" #include "xenia/base/math.h" #include "xenia/base/platform.h" #include "xenia/ui/surface.h" #include "xenia/ui/ui_drawer.h" #if XE_PLATFORM_WIN32 // Must be included before DXGI for things like NOMINMAX, and also needed for // Windows handle types. #include "xenia/base/platform_win.h" #include "xenia/ui/dxgi_include_win.h" #include #endif // XE_PLATFORM // For implementation use. DECLARE_bool(present_render_pass_clear); namespace xe { namespace ui { class Presenter; class Window; class Win32Window; class UIDrawContext { public: UIDrawContext(const UIDrawContext& context) = delete; UIDrawContext& operator=(const UIDrawContext& context) = delete; virtual ~UIDrawContext() = default; Presenter& presenter() const { return presenter_; } // It's assumed that the render target size will be either equal to the size // of the surface, or the render target will be stretched to cover the entire // surface (not in the corner of the surface). uint32_t render_target_width() const { return render_target_width_; } uint32_t render_target_height() const { return render_target_height_; } protected: explicit UIDrawContext(Presenter& presenter, uint32_t render_target_width, uint32_t render_target_height) : presenter_(presenter), render_target_width_(render_target_width), render_target_height_(render_target_height) {} private: Presenter& presenter_; uint32_t render_target_width_; uint32_t render_target_height_; }; struct RawImage { uint32_t width = 0; uint32_t height = 0; size_t stride = 0; // R8 G8 B8 X8. The last row is not required to be padded to the stride. std::vector data; }; // The presenter displays up to two layers of content on a host surface: // - Guest output image, focusing on lowering latency and maintaining stable // frame pacing, with various scaling and sharpening methods and letterboxing; // - Xenia's internal UI (such as the profiler and Dear ImGui). // // The guest output image may be refreshed from any thread generating it // (usually the GPU emulation thread), as long as there are no multiple threads // doing that simultaneously (since that would functionally be a race condition // even if refreshing is performed in a critical section). // // The UI overlays are managed entirely by the UI thread. // // Painting on the host surface may occur in two places: // - If there are no UI overlays, painting of the guest output may be performed // immediately from the thread refreshing it, to bypass the OS scheduling and // event handling. This is especially important on platforms where the native // surface paint event has a frame rate limit (such as the display refresh // rate), and the limit may differ greatly from the guest frame rate (such as // presenting a 30 or 60 FPS guest to a 144 Hz host surface). // - If the UI overlays (owned by the UI thread) are present, painting of both // the guest output (is available) and the UI is done exclusively from the // platform paint event handler. The guest output without UI overlays may also // be painted from the platform paint callback in certain cases, such as when // an additional paint beyond the guest's frame rate may be needed (like when // resizing the window), or when painting from the thread refreshing the guest // output is undesirable (for instance, if it will result in waiting for host // vertical sync in that thread too early if host vertical sync can't be // disabled on the platform, blocking the next frame of GPU emulation). // // The composition of the guest and the UI is done by Xenia manually, as opposed // to using platform functionality such as DirectComposition, in order to have // more predictability of GPU queue scheduling, but primarily to be able to take // advantage of independent host presentation where it's available, so variable // refresh rate may be used where possible, and latency may be significantly // reduced. Also, at least on some configurations (checked on Windows 11 21H2 on // Nvidia GeForce GTX 1070 with driver version 472.12), when in borderless // fullscreen, any composition causes the DXGI Present to wait for vertical sync // on the GPU even if the sync interval 0 is specified. // // An intermediate image with the size requested by the guest is used for guest // output in all cases. Even though it adds some GPU overhead, especially in the // 1:1 size case, using it solves multiple issues: // - Presentation may be done more often than by the guest. // - There is clear separation between pre-scaling and mid- / post-scaling // operations. The gamma ramp, for instance, may be applied before scaling, // with one lookup per pixel rather than four with fetch4. // - A simpler compute shader may be used instead of setting up the whole // graphics pipeline for copying in the GPU command processor in all cases, // while Direct3D 12 does not allow UAVs for swap chain buffers. // // The presenter limits the frame rate of the UI overlay (when possible) to a // value that's ideally the refresh rate of the monitor containing the window if // the platform's paint event doesn't have an internal limiter. However, where // possible, the arrival of a new guest output image will interrupt the UI tick // wait. // // Because the UI overlays preclude the possibility of presenting directly from // the thread refreshing the guest output, and on some platforms, result in the // frame rate limiting of paint events manifesting itself, there must be no // persistent UI overlays that haven't been explicitly requested by the user. // However, for temporary (primarily non-modal) UI elements such as various // timed notifications, using the Presenter should be preferred to implementing // them via overlaying native windows on top of the presentation surface on // platforms where the concept of independent presentation exists, as multiple // windows will result in native composition disabling it. // // The painting connection between the Presenter and the Surface can be managed // only by the UI thread. However, the thread refreshing the guest output may // still mark the current connection as outdated and ask the UI thread (by // requesting painting) to try to recover - but the guest output refresh thread // must not try to reconnect by itself, as methods of the Surface are available // only to the UI thread. class Presenter { public: // May be actually called on the UI thread even if statically_from_ui_thread // is false, such as when the guest output is refreshed by the UI thread. using HostGpuLossCallback = std::function; static void FatalErrorHostGpuLossCallback(bool is_responsible, bool statically_from_ui_thread); class GuestOutputRefreshContext { public: GuestOutputRefreshContext(const GuestOutputRefreshContext& context) = delete; GuestOutputRefreshContext& operator=( const GuestOutputRefreshContext& context) = delete; virtual ~GuestOutputRefreshContext() = default; // Sets whether the source actually has no more than 8 bits of precision // (though the image provided by the refresher may still have a higher // storage precision). If never called, assuming it's false. void SetIs8bpc(bool is_8bpc) { is_8bpc_out_ref_ = is_8bpc; } protected: GuestOutputRefreshContext(bool& is_8bpc_out_ref) : is_8bpc_out_ref_(is_8bpc_out_ref) { is_8bpc_out_ref = false; } private: bool& is_8bpc_out_ref_; }; class GuestOutputPaintConfig { public: enum class Effect { kBilinear, kCas, // AMD FidelityFX Super Resolution upsampling, Contrast Adaptive // Sharpening otherwise. kFsr, }; // This value is used as a lerp factor. static constexpr float kCasAdditionalSharpnessMin = 0.0f; static constexpr float kCasAdditionalSharpnessMax = 1.0f; static constexpr float kCasAdditionalSharpnessDefault = 0.0f; static_assert(kCasAdditionalSharpnessDefault >= kCasAdditionalSharpnessMin && kCasAdditionalSharpnessDefault <= kCasAdditionalSharpnessMax); // EASU (as well as CAS) is designed for scaling by factors of up to 2x2. // Some sensible limit for unusual cases, when the game for some reason // presents a very small back buffer. // This is enough for 480p > 960p > 1920p > 3840p > 7680p (bigger than 8K, // or 4320p). static constexpr uint32_t kFsrMaxUpscalingPassesMax = 4; static constexpr float kFsrSharpnessReductionMin = 0.0f; // "Values above 2.0 won't make a visible difference." // https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/docs/FidelityFX-FSR-Overview-Integration.pdf static constexpr float kFsrSharpnessReductionMax = 2.0f; static constexpr float kFsrSharpnessReductionDefault = 0.2f; static_assert(kFsrSharpnessReductionDefault >= kFsrSharpnessReductionMin && kFsrSharpnessReductionDefault <= kFsrSharpnessReductionMax); // In the sharpness setters, min / max with a constant as the first argument // also drops NaNs. bool GetAllowOverscanCutoff() const { return allow_overscan_cutoff_; } void SetAllowOverscanCutoff(bool new_allow_overscan_cutoff) { allow_overscan_cutoff_ = new_allow_overscan_cutoff; } Effect GetEffect() const { return effect_; } void SetEffect(Effect new_effect) { effect_ = new_effect; } float GetCasAdditionalSharpness() const { return cas_additional_sharpness_; } void SetCasAdditionalSharpness(float new_cas_additional_sharpness) { cas_additional_sharpness_ = std::min( kCasAdditionalSharpnessMax, std::max(kCasAdditionalSharpnessMin, new_cas_additional_sharpness)); } uint32_t GetFsrMaxUpsamplingPasses() const { return fsr_max_upsampling_passes_; } void SetFsrMaxUpsamplingPasses(uint32_t new_fsr_max_upsampling_passes) { fsr_max_upsampling_passes_ = std::min(kFsrMaxUpscalingPassesMax, std::max(uint32_t(1), new_fsr_max_upsampling_passes)); } // In stops. float GetFsrSharpnessReduction() const { return fsr_sharpness_reduction_; } void SetFsrSharpnessReduction(float new_fsr_sharpness_reduction) { fsr_sharpness_reduction_ = std::min( kFsrSharpnessReductionMax, std::max(kFsrSharpnessReductionMin, new_fsr_sharpness_reduction)); } // Very tiny effect, but highly noticeable, for instance, on the sky in the // 4D5307E6 main menu (prominently in Custom Games, especially with FSR - // banding around the clouds can be clearly seen without dithering with 8bpc // final host output). bool GetDither() const { return dither_; } void SetDither(bool new_dither) { dither_ = new_dither; } private: // Tools, rather than the emulator itself, must not allow overscan cutoff // and must use the kBilinear effect as the image must be as close to the // original front buffer as possible. bool allow_overscan_cutoff_ = false; Effect effect_ = Effect::kBilinear; float cas_additional_sharpness_ = kCasAdditionalSharpnessDefault; uint32_t fsr_max_upsampling_passes_ = kFsrMaxUpscalingPassesMax; float fsr_sharpness_reduction_ = kFsrSharpnessReductionDefault; bool dither_ = false; }; Presenter(const Presenter& presenter) = delete; Presenter& operator=(const Presenter& presenter) = delete; virtual ~Presenter(); virtual Surface::TypeFlags GetSupportedSurfaceTypes() const = 0; // For calling from the Window for the Presenter attached to it. // May be called from the destructor of the presenter through the window. void SetWindowSurfaceFromUIThread(Window* new_window, Surface* new_surface); void OnSurfaceMonitorUpdateFromUIThread( bool old_monitor_potentially_disconnected); void OnSurfaceResizeFromUIThread(); // For calling from the platform paint event handler. Refreshes the surface // connection if needed, and also paints if possible and if needed (if there // are no UI overlays, and the guest output is presented directly from the // thread refreshing it, the paint may be skipped unless there has been an // explicit request previously or force_paint is true). If painting happens, // both the guest output and the UI overlays (if any are active) are drawn. // The background / letterbox of the painted context will be black - windows // should preferably have a black background before a Presenter is attached to // them too. void PaintFromUIThread(bool force_paint = false); // Pass 0 as width or height to disable guest output until the next refresh // with an actual size. The display aspect ratio may be specified like 16:9 or // like 1280:720, both are accepted, for simplicity, the guest display size // may just be passed. The callback will receive a backend-specific context, // and will not be called in case of an error such as the wrong size, or if // guest output is disabled. Returns whether the callback was called and it // returned true. The callback must submit all updating work to the host GPU // before successfully returning, and also signal all the GPU synchronization // primitives required by the GuestOutputRefreshContext implementation. bool RefreshGuestOutput( uint32_t frontbuffer_width, uint32_t frontbuffer_height, uint32_t display_aspect_ratio_x, uint32_t display_aspect_ratio_y, std::function refresher); // The implementation must be callable from any thread, including from // multiple at the same time, and it should acquire the latest guest output // image via ConsumeGuestOutput. virtual bool CaptureGuestOutput(RawImage& image_out) = 0; const GuestOutputPaintConfig& GetGuestOutputPaintConfigFromUIThread() const { return guest_output_paint_config_; } // For simplicity, may be called repeatedly even if no changes have been made. void SetGuestOutputPaintConfigFromUIThread( const GuestOutputPaintConfig& new_config); void AddUIDrawerFromUIThread(UIDrawer* drawer, size_t z_order); void RemoveUIDrawerFromUIThread(UIDrawer* drawer); // Requests (re)painting with the UI if there's UI to draw. void RequestUIPaintFromUIThread(); protected: enum class PaintResult { kPresented, kPresentedSuboptimal, // Refused for internal reasons or a host API side failure, but still may // try to present without resetting the graphics provider in the future. kNotPresented, kNotPresentedConnectionOutdated, kGpuLostExternally, kGpuLostResponsible, }; enum class SurfacePaintConnectResult { // Redrawing not necessary, nothing changed. Must not be returned for a new // connection (when was previously disconnected from the surface). kSuccessUnchanged, kSuccess, kFailure, kFailureSurfaceUnusable, }; static constexpr uint32_t kGuestOutputMailboxSize = 3; struct GuestOutputProperties { // At least any value being 0 here means the guest output is disabled for // this frame. uint32_t frontbuffer_width; uint32_t frontbuffer_height; // Guest display aspect ratio numerator and denominator (both 16:9 and // 1280:720 kinds of values are accepted). uint32_t display_aspect_ratio_x; uint32_t display_aspect_ratio_y; bool is_8bpc; GuestOutputProperties() { SetToInactive(); } bool IsActive() const { return frontbuffer_width && frontbuffer_height && display_aspect_ratio_x && display_aspect_ratio_y; } void SetToInactive() { frontbuffer_width = 0; frontbuffer_height = 0; display_aspect_ratio_x = 0; display_aspect_ratio_y = 0; is_8bpc = false; } }; enum class GuestOutputPaintEffect { kBilinear, kBilinearDither, kCasSharpen, kCasSharpenDither, kCasResample, kCasResampleDither, kFsrEasu, kFsrRcas, kFsrRcasDither, kCount, }; static constexpr bool CanGuestOutputPaintEffectBeIntermediate( GuestOutputPaintEffect effect) { switch (effect) { case GuestOutputPaintEffect::kBilinear: // Dithering is never performed in intermediate passes because it may be // interpreted as features by the subsequent passes. case GuestOutputPaintEffect::kBilinearDither: case GuestOutputPaintEffect::kCasSharpenDither: case GuestOutputPaintEffect::kCasResampleDither: case GuestOutputPaintEffect::kFsrRcasDither: return false; default: // The result of any other effect can be stretched with bilinear // filtering to the final resolution. return true; }; } static constexpr bool CanGuestOutputPaintEffectBeFinal( GuestOutputPaintEffect effect) { switch (effect) { case GuestOutputPaintEffect::kFsrEasu: return false; default: return true; }; } // The longest path is kFsrMaxUpscalingPassesMax + optionally RCAS + // optionally bilinear, when upscaling by more than // 2^kFsrMaxUpscalingPassesMax along any direction. // Non-FSR paths are either only bilinear, only CAS, or (when upscaling by // more than 2 along any direction) CAS followed by bilinear. static constexpr size_t kMaxGuestOutputPaintEffects = GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax + 2; struct GuestOutputPaintFlow { // Letterbox on up to 4 sides. static constexpr size_t kMaxClearRectangles = 4; struct ClearRectangle { uint32_t x; uint32_t y; uint32_t width; uint32_t height; }; GuestOutputProperties properties; // If 0, don't display the guest output. size_t effect_count; std::array effects; std::array, kMaxGuestOutputPaintEffects> effect_output_sizes; // Offset of the rectangle for final drawing to the host window with // letterboxing. int32_t output_x; int32_t output_y; // If there is guest output (effect_count is not 0), contains the letterbox // rectangles around the guest output. size_t letterbox_clear_rectangle_count; std::array letterbox_clear_rectangles; void GetEffectInputSize(size_t effect_index, uint32_t& width_out, uint32_t& height_out) const { assert_true(effect_index < effect_count); if (!effect_index) { width_out = properties.frontbuffer_width; height_out = properties.frontbuffer_height; return; } const std::pair& intermediate_size = effect_output_sizes[effect_index - 1]; width_out = intermediate_size.first; height_out = intermediate_size.second; } void GetEffectOutputOffset(size_t effect_index, int32_t& x_out, int32_t& y_out) const { assert_true(effect_index < effect_count); if (effect_index + 1 < effect_count) { x_out = 0; y_out = 0; return; } x_out = output_x; y_out = output_y; } }; struct BilinearConstants { int32_t output_offset[2]; float output_size_inv[2]; void Initialize(const GuestOutputPaintFlow& flow, size_t effect_index) { flow.GetEffectOutputOffset(effect_index, output_offset[0], output_offset[1]); const std::pair& output_size = flow.effect_output_sizes[effect_index]; output_size_inv[0] = 1.0f / float(output_size.first); output_size_inv[1] = 1.0f / float(output_size.second); } }; static constexpr float CalculateCasPostSetupSharpness(float sharpness) { // CasSetup const1.x. return -1.0f / (8.0f - 3.0f * sharpness); } struct CasSharpenConstants { int32_t output_offset[2]; float sharpness_post_setup; void Initialize(const GuestOutputPaintFlow& flow, size_t effect_index, const GuestOutputPaintConfig& config) { flow.GetEffectOutputOffset(effect_index, output_offset[0], output_offset[1]); sharpness_post_setup = CalculateCasPostSetupSharpness(config.GetCasAdditionalSharpness()); } }; struct CasResampleConstants { int32_t output_offset[2]; // Input size / output size. float input_output_size_ratio[2]; float sharpness_post_setup; void Initialize(const GuestOutputPaintFlow& flow, size_t effect_index, const GuestOutputPaintConfig& config) { flow.GetEffectOutputOffset(effect_index, output_offset[0], output_offset[1]); uint32_t input_width, input_height; flow.GetEffectInputSize(effect_index, input_width, input_height); const std::pair& output_size = flow.effect_output_sizes[effect_index]; input_output_size_ratio[0] = float(input_width) / float(output_size.first); input_output_size_ratio[1] = float(input_height) / float(output_size.second); sharpness_post_setup = CalculateCasPostSetupSharpness(config.GetCasAdditionalSharpness()); } }; struct FsrEasuConstants { // No output offset because the EASU pass is always done to an intermediate // framebuffer. float input_output_size_ratio[2]; float input_size_inv[2]; void Initialize(const GuestOutputPaintFlow& flow, size_t effect_index) { uint32_t input_width, input_height; flow.GetEffectInputSize(effect_index, input_width, input_height); const std::pair& output_size = flow.effect_output_sizes[effect_index]; input_output_size_ratio[0] = float(input_width) / float(output_size.first); input_output_size_ratio[1] = float(input_height) / float(output_size.second); input_size_inv[0] = 1.0f / float(input_width); input_size_inv[1] = 1.0f / float(input_height); } }; struct FsrRcasConstants { int32_t output_offset[2]; float sharpness_post_setup; static float CalculatePostSetupSharpness(float sharpness_reduction_stops) { // FsrRcasCon const0.x. return std::exp2f(-sharpness_reduction_stops); } void Initialize(const GuestOutputPaintFlow& flow, size_t effect_index, const GuestOutputPaintConfig& config) { flow.GetEffectOutputOffset(effect_index, output_offset[0], output_offset[1]); sharpness_post_setup = CalculatePostSetupSharpness(config.GetFsrSharpnessReduction()); } }; explicit Presenter(HostGpuLossCallback host_gpu_loss_callback) : host_gpu_loss_callback_(host_gpu_loss_callback) {} // Must be called by the implementation's initialization, before the presenter // is used for anything. bool InitializeCommonSurfaceIndependent(); // ConnectOrReconnect and Disconnect are callable only by the UI thread and // only when it has access to painting (PaintMode is not // kGuestOutputThreadImmediately). // Called only for a non-zero-area surface potentially supporting painting via // the presenter. In case of a failure, internally no resources referencing // the surface must be held by the implementation anymore - the implementation // must be left in the same state as after // DisconnectPaintingFromSurfaceFromUIThreadImpl. If the call is successful, // the implementation must write to is_vsync_implicit_out whether the // connection will now have vertical sync forced by the host window system, // which may cause undesirable waits on the CPU when beginning or ending // frames. virtual SurfacePaintConnectResult ConnectOrReconnectPaintingToSurfaceFromUIThread( Surface& new_surface, uint32_t new_surface_width, uint32_t new_surface_height, bool was_paintable, bool& is_vsync_implicit_out) = 0; // Releases resources referencing the surface in the implementation if they // are held by it. Call through DisconnectPaintingFromSurfaceFromUIThread to // ensure the implementation is only called while the connection is active. virtual void DisconnectPaintingFromSurfaceFromUIThreadImpl() = 0; // The returned lock interlocks multiple consumers (but not the producer and // the consumer) and must be held while accessing implementation-specific // objects that depend on the image or its index in the mailbox (unless there // are other locking mechanisms involved for the resources, such as reference // counting for the guest output images, which doesn't have to be atomic // though for the reason described later in this paragraph, or assumptions // like of main target painting being possible only in at most one thread at // once). While this lock is held, the currently acquired image index can't be // changed (by other consumers advancing the acquired image index to the new // ready image index), so the image with the index given by this function // can't be released and be made writable or given to a different consumer // (thus it's owned exclusively by the consumer who has called this function). // The properties are returned by copy rather than returning a pointer to them // or asking the consumer to pull them for the current mailbox index, so there // are less things to take into consideration while leaving the guest output // consumer critical section earlier (as if a pointer was returned, the data // behind it could be overwritten at any time after leaving the consumer // critical section) if the implementation has its own synchronization // mechanisms that allow for doing so as described earlier. Returns UINT32_MAX // as the mailbox index if the image is inactive (if it's active, it has // proper properties though). [[nodiscard]] std::unique_lock ConsumeGuestOutput( uint32_t& mailbox_index_or_max_if_inactive_out, GuestOutputProperties* properties_out, GuestOutputPaintConfig* paint_config_out); // The properties are passed explicitly, not taken from the current acquired // image, so it can be called for a copy of the acquired image's properties // outside the consumer lock if the implementation has its own synchronization // (like reference counting for the guest output images) that makes it // possible to leave the consumer critical section earlier. Also, the guest // output paint configuration is passed explicitly too so calling this // function multiple times is safer. GuestOutputPaintFlow GetGuestOutputPaintFlow( const GuestOutputProperties& properties, uint32_t host_rt_width, uint32_t host_rt_height, uint32_t max_rt_width, uint32_t max_rt_height, const GuestOutputPaintConfig& config) const; // is_8bpc_out_ref is where to write whether the source actually has no more // than 8 bits of precision per channel (though the image provided by the // refresher may still have a higher storage precision) - if not written, it // will be assumed to be false. virtual bool RefreshGuestOutputImpl( uint32_t mailbox_index, uint32_t frontbuffer_width, uint32_t frontbuffer_height, std::function refresher, bool& is_8bpc_out_ref) = 0; // For guest output capturing (for debugging use thus - shouldn't be adding // any noise like dithering that's not present in the original image), // converting a 10bpc RGB pixel to 8bpc that can be stored in common image // formats. static uint32_t Packed10bpcRGBTo8bpcBytes(uint32_t rgb10) { // Conversion almost according to the Direct3D 10+ rules (unorm > float > // unorm), but with one multiplication rather than separate division and // multiplication - the results are the same for unorm10 to unorm8. if constexpr (std::endian::native == std::endian::big) { return (uint32_t(float(rgb10 & 0x3FF) * (255.0f / 1023.0f) + 0.5f) << 24) | (uint32_t(float((rgb10 >> 10) & 0x3FF) * (255.0f / 1023.0f) + 0.5f) << 16) | (uint32_t(float((rgb10 >> 20) & 0x3FF) * (255.0f / 1023.0f) + 0.5f) << 8) | uint32_t(0xFF); } return uint32_t(float(rgb10 & 0x3FF) * (255.0f / 1023.0f) + 0.5f) | (uint32_t(float((rgb10 >> 10) & 0x3FF) * (255.0f / 1023.0f) + 0.5f) << 8) | (uint32_t(float((rgb10 >> 20) & 0x3FF) * (255.0f / 1023.0f) + 0.5f) << 16) | (uint32_t(0xFF) << 24); } // Paints and presents the guest output if available (or just solid black // color), and if requested, the UI on top of it. // // May be called from the non-UI thread, but only to paint the guest output // (no UI drawing, with execute_ui_drawers disabled). // // Call via PaintAndPresent. virtual PaintResult PaintAndPresentImpl(bool execute_ui_drawers) = 0; // For calling from the painting implementations if requested. void ExecuteUIDrawersFromUIThread(UIDrawContext& ui_draw_context); private: enum class PaintMode { // Don't paint at all. // Painting lifecycle is accessible only by the UI thread. // window_->RequestPaint() must not be called in this mode at all regardless // of whether the Window object exists because the Window object in this // case may correspond to a window without a paintable Surface (in a closed // state, or in the middle of a surface change), and non-UI threads (such as // the guest output thread) may result in a race condition internally inside // Window::RequestPaint during the access to the Window's state, such as the // availability of the Surface that can handle the paint (therefore, if // there's no Surface, this is the only valid mode). kNone, // Guest output refreshing notifies the `window_`, which must be valid and // safe to call RequestPaint for, that painting should be done in the UI // thread (including the UI if needed). Painting is possible, and painting // lifecycle is accessible, only by the UI thread. kUIThreadOnRequest, // Paint immediately in the guest output thread for lower latency. The // `window_`, however, may be notified that the surface painting connection // has become outdated (via RequestPaint, as in this case the UI thread will // need to repaint as sooner as possible after reconnecting anyway), and // change the surface connection state accordingly (only to // kConnectedOutdated). // Painting is possible only by the guest output thread, lifecycle // management cannot be done from the UI thread until it takes over. kGuestOutputThreadImmediately, }; enum class SurfacePaintConnectionState { // No surface at all, or couldn't connect with the current state of the // surface (such as because the surface was zero-sized because the window // was minimized, for example). Or, the connection has become outdated, and // the attempt to reconnect at kRetryConnectingSoon has failed. Try to // reconnect if anything changes in the state of the surface, such as its // size. kUnconnectedRetryAtStateChange, // Can't connect to the current existing surface (the surface has been lost // or it's completely incompatible). No point in retrying connecting until // the surface is replaced. kUnconnectedSurfaceReportedUnusable, // Everything is fine, can paint. The connection might have become // suboptimal though, and haven't tried refreshing yet, but still usable for // painting nonetheless. kConnectedPaintable, // The implementation still holds resources associated with the connection, // but presentation has reported that it has become outdated, try // reconnecting as soon as possible (at the next paint attempt, requesting // it if needed). This is the only state that the guest output thread may // transition the connection to (from kConnectedPaintable only) if it has // access to painting (the paint mode is kGuestOutputThreadImmediately). kConnectedOutdated, }; static constexpr bool IsConnectedSurfacePaintConnectionState( SurfacePaintConnectionState connection_state) { return connection_state == SurfacePaintConnectionState::kConnectedPaintable || connection_state == SurfacePaintConnectionState::kConnectedOutdated; } struct UIDrawerReference { UIDrawer* drawer; uint64_t last_draw; explicit UIDrawerReference(UIDrawer* drawer, uint64_t last_draw = UINT64_MAX) : drawer(drawer), last_draw(last_draw) {} }; void SetPaintModeFromUIThread(PaintMode new_mode); // Based on conditions like whether UI needs to be drawn and whether vertical // sync is implicit - see the implementation for the requirements. // is_paintable is an explicit parameter because this function may be called // in two scenarios: // - After connection updates - painting connection is owned by the UI thread, // so the actual state can be obtained and passed here so kNone can be // returned. // - When merely toggling something local to the UI thread - only to toggle // between the two threads, but not to switch from or to kNone (make sure // it's not kNone before calling), pass `true` in this case. PaintMode GetDesiredPaintModeFromUIThread(bool is_paintable) const; // Callable only by the UI thread and only when it has access to painting // (PaintMode is not kGuestOutputThreadImmediately). // This can be called to a surface after having not been connected to any (in // this case, surface_paint_connection_state_ must be // kUnconnectedRetryAtStateChange, not kUnconnectedNoUsableSurface, otherwise // the call will be dropped), or to handle surface state changes such as // resizing. However, this must not be called to change directly from one // surface to another - need to disconnect prior to that, because the // implementation may assume that the surface is still the same, and may try // to, for instance, resize the buffers for the existing surface. void UpdateSurfacePaintConnectionFromUIThread( bool* repaint_needed_out, bool update_paint_mode_to_desired); // Callable only by the UI thread and only when it has access to painting // (PaintMode is not kGuestOutputThreadImmediately). // See DisconnectPaintingFromSurfaceFromUIThreadImpl for more information. void DisconnectPaintingFromSurfaceFromUIThread( SurfacePaintConnectionState new_state); // Can be called from any thread if an existing window_ safe to RequestPaint // (not closed) is available in it, so doesn't check the surface painting // connection state. Returns whether the window_->RequestPaint() call has been // made. bool RequestPaintOrConnectionRecoveryViaWindow( bool force_ui_thread_paint_tick); // Platform-specific function refreshing the monitor the current window // surface is on, through the Surface or its Window. A reference to the // monitor is held only when a Surface is available, so it's automatically // dropped when the Window loses its Surface when it's being closed (but the // Window object keeps being attached to the Presenter), for instance. void UpdateSurfaceMonitorFromUIThread( bool old_monitor_potentially_disconnected); // Platform-specific function returning whether the surface the presenter is // currently attached it is actually visible on any monitor. UI thread // painting may be dropped if this returns false - need to request painting if // the surface appears on a monitor again. May be using the state cached at // window / surface state changes, not the actual state from the platform. bool InSurfaceOnMonitorFromUIThread() const; // Calls PaintAndPresentImpl and does post-paint checks that are safe to do on // both the UI thread and the guest output thread. See the information about // PaintAndPresentImpl for details. // A kPresentedSuboptimal result is returned as is, but the connection may or // may not be made outdated if that happens - though if it's // kPresentedSuboptimal rather than kNotPresentedConnectionOutdated, the image // has been successfully sent to the OS presentation at least. PaintResult PaintAndPresent(bool execute_ui_drawers); void HandleUIDrawersChangeFromUIThread(bool drawers_were_empty); bool AreUITicksNeededFromUIThread() const { // UI drawing should be done, and painting needs to be possible (coarsely // checking because the actual connection state, including outdated, may be // currently unavailable from the UI thread). // There's no need to limit the frame rate manually if there is vertical // sync in the presentation already as that might result in inconsistent // frame pacing and potentially skipped vertical sync intervals. return !ui_drawers_.empty() && paint_mode_ != PaintMode::kNone && !surface_paint_connection_has_implicit_vsync_; } void UpdateUITicksNeededFromUIThread(); void WaitForUITickFromUIThread(); // May be called from any thread. void ForceUIThreadPaintTick(); // Must be called only in the end of entry points - reinitialization of the // presenter may be done by the handler if it was called from the UI thread // (even if the UI thread argument is false - such as when the guest output is // refreshed on the UI thread). HostGpuLossCallback host_gpu_loss_callback_; // May be accessed by the guest output thread if the paint mode is not kNone, // to request painting (for kUIThreadOnRequest) or reconnection (for // kGuestOutputThreadImmediately) in the UI thread. Set the paint mode to // kNone before modifying (that naturally has to be done anyway by // disconnecting painting). Window* window_ = nullptr; // The surface of the `window_` the presenter is currently attached to. Surface* surface_ = nullptr; // Mutex protecting paint_mode_ (and, in the guest output thread, objects // related to painting themselves). // // The UI thread (as the mode is modifiable only by it) can use it as // "barriers", like: // 1) If needed, lock and disable guest output thread access to painting. // 2) Interact with the painting connection. // 3) If needed, lock and re-enable guest output thread access to the // painting. // // On the other hand, the guest output thread _must_ hold it all the time it's // painting, to ensure the mode stays the same while it's painting. std::mutex paint_mode_mutex_; // UI thread: writable, guest output thread: read-only. PaintMode paint_mode_ = PaintMode::kNone; // These fields can be accessed _exclusively_ by either the UI thread or the // guest output thread, depending on paint_mode_. // If it's kGuestOutputThreadImmediately, they can be accessed _only_ by the // guest output thread (though the UI thread can still read, but not modify, // fields that are writable by the UI thread and readadable by both). // Otherwise, they can be accessed _only_ by the UI thread. // The connection state may be changed from the guest output thread, but only // from kConnectedPaintable to kConnectedOutdated. SurfacePaintConnectionState surface_paint_connection_state_ = SurfacePaintConnectionState::kUnconnectedRetryAtStateChange; // If the surface connection was optimal at the last paint attempt, but now // has become suboptimal, need to try to reconnect. But only in this case - if // the connection has been suboptimal from the very beginning don't try to // reconnect every frame. bool surface_paint_connection_was_optimal_at_successful_paint_ = false; // Modifiable only by the UI thread (therefore can be accessed by the UI // thread regardless of the paint mode) while (re)connecting painting to the // surface. bool surface_paint_connection_has_implicit_vsync_ = false; // Modifiable only by the UI thread, can be read by the thread that's // painting. uint32_t surface_width_in_paint_connection_ = 0; uint32_t surface_height_in_paint_connection_ = 0; // Can be set by both the UI thread and the guest output thread before doing // window_->RequestPaint() - whether an extra painting (preceded by // reconnection if needed, and painting) was requested, primarily after some // state change that may effect the surface painting connection, resulting in // the need to refresh it as soon as possible. // // Relaxed memory order is enough, everything that may influence painting is // either local to the UI thread or protected with barriers elsewhere. // // There's no need to bother about resetting this variable when losing // connection as the next successful reconnection should be followed by a // repaint request anyway. std::atomic ui_thread_paint_requested_{false}; std::mutex guest_output_paint_config_mutex_; // UI thread: writable, guest output thread: read-only. GuestOutputPaintConfig guest_output_paint_config_; // Single-producer-multiple-consumers (lock-free SPSC + consumer lock) mailbox // for presenting of the most up-to-date guest output image without long // interlocking between guest output refreshing and painting. static_assert(kGuestOutputMailboxSize == 3); // The "acquired" image (in bits 0:1) is the one that is currently being read, // or was last read, by a consumer of the guest output. The index of it can be // modified only by the consumer and stays the same while it's processing the // image. // The "ready" image (in bits 2:3) is the most up-to-date image that the // refresher has completely written, and a consumer may acquire it. It may be // == acquired if there has been no refresh since the last acquisition. // These two images can be accessed by painting in parallel, in an unordered // way, with guest output refreshing. std::atomic guest_output_mailbox_acquired_and_ready_{0}; // The "writable" image is different than both "acquired" and "ready" and is // accessible only by the guest output refreshing - it's the image that the // refresher may write to. uint32_t guest_output_mailbox_writable_ = 1; // The guest output images may be consumed by two operations - painting, and // capturing to a CPU-side buffer. These two usually never happen in parallel // in reality though, as they're usually not even needed both at once in the // same app within Xenia, so there's no need to create any // complex lock-free synchronization between the two, but still, the situation // when multiple consumers want the guest output image at the same is // perfectly valid (unlike for producers, because even with a producer lock // that would still be a race condition since the two refreshes themselves // will be done in an undefined order) - so, a sufficient synchronization // mechanism is used to make sure multiple consumers can acquire images // without interfering with each other. // While this is held, paint_mode_mutex_ must not be locked (the lock order is // the reverse when painting in the guest output thread - painting is done // with paint_mode_mutex_ held in this case, and guest output consumption // happens as part of painting. std::mutex guest_output_mailbox_consumer_mutex_; std::array guest_output_properties_; // Accessible only by refreshing, whether the last refresh contained an image // rather than being blank. bool guest_output_active_last_refresh_ = false; // Ordered by the Z order, and then by the time of addition. // Note: All the iteration logic involving this Z ordering must be the same as // in input handling (in the input listeners in the Window), but in reverse. std::multimap ui_drawers_; size_t ui_draw_current_ = 0; size_t ui_draw_current_z_order_; std::multimap::iterator ui_draw_next_iterator_; bool is_executing_ui_drawers_ = false; // Whether currently running the logic of PaintFromUIThread, so certain // actions (such as changing the paint mode, requesting a redraw) must be // deferred and be handled by the tail of PaintFromUIThread for consistency // with what PaintFromUIThread does internally. bool is_in_ui_thread_paint_ = false; bool request_guest_output_paint_after_current_ui_thread_paint_; bool request_ui_paint_after_current_ui_thread_paint_; // Platform-specific, but implementation-agnostic parts, primarily for // limiting of the frame rate of the UI to avoid drawing the UI at extreme // frame rates wasting the CPU and the GPU resources and starving everything // else. The waits performed here must be interruptible by guest output // presentation requests to prevent adding arbitrary amounts of latency to it. // On Android and GTK, this is not needed, the frame rate of draw events is // limited to the display refresh rate internally. #if XE_PLATFORM_WIN32 static Microsoft::WRL::ComPtr GetDXGIOutputForMonitor( IDXGIFactory1* factory, HMONITOR monitor); bool AreDXGIUITicksWaitable( [[maybe_unused]] const std::unique_lock& dxgi_ui_tick_lock) { return dxgi_ui_ticks_needed_ && !dxgi_ui_tick_thread_shutdown_ && dxgi_ui_tick_output_; } void DXGIUITickThread(); // Accessible only from the UI thread, to avoid updating monitor-dependent // information such as the DXGI output if the monitor hasn't actually been // changed in the current state change (such as window positioning changes). HMONITOR surface_win32_monitor_ = nullptr; // Requiring the lowest version of DXGI for IDXGIOutput::WaitForVBlank, which // is available even on Windows Vista, but for IDXGIFactory1::IsCurrent, // DXGI 1.1 is needed (available starting from Windows 7; also mixing DXGI 1.0 // and 1.1+ in the Direct3D 12 code is not supported, see CreateDXGIFactory on // MSDN). The factory is created when it's needed, and may be released and // recreated when it's not current anymore and that becomes relevant. Microsoft::WRL::ComPtr dxgi_ui_tick_factory_; // Accessible only from the UI thread, though the value is taken from the // tick-mutex-protected variable. uint64_t dxgi_ui_tick_last_draw_ = 0; std::mutex dxgi_ui_tick_mutex_; uint64_t dxgi_ui_tick_last_vblank_ = 1; // If output is null or shutdown is true, the signal may not be sent, either // don't limit the frame rate in this case (an exceptional situation, such as // a failure to find the output in DXGI), or don't draw at all if the window // was removed from a connected monitor. Microsoft::WRL::ComPtr dxgi_ui_tick_output_; // To avoid allocating processing resources to the thread when nothing needs // the ticks (not drawing the UI), the thread waits for vertical blanking // intervals only when the UI drawing ticks are needed, and sleeping waiting // for the control condition variable signals otherwise. Modifiable only from // the UI thread, so readable by it without locking the mutex. bool dxgi_ui_ticks_needed_ = false; // The shutdown flag is modifiable only from the UI thread. bool dxgi_ui_tick_thread_shutdown_ = false; bool dxgi_ui_tick_force_requested_ = false; std::condition_variable dxgi_ui_tick_control_condition_; // May be signaled by guest output refreshing. std::condition_variable dxgi_ui_tick_signal_condition_; std::thread dxgi_ui_tick_thread_; #endif // XE_PLATFORM }; } // namespace ui } // namespace xe #endif // XENIA_UI_PRESENTER_H_