/** ****************************************************************************** * 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. * ****************************************************************************** */ #include "xenia/ui/presenter.h" #include #include #include "xenia/base/assert.h" #include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/platform.h" #include "xenia/ui/window.h" #if XE_PLATFORM_WIN32 #include "xenia/ui/window_win.h" #endif // On Windows, InvalidateRect causes WM_PAINT to be sent quite quickly, so // presenting from the thread refreshing the guest output is not absolutely // necessary, but still may be nice for bypassing the scheduling and the // message queue. // On Android and GTK, the frame rate of draw events is limited to the display // refresh rate internally, so for the lowest latency especially in case the // refresh rates differ significantly on the guest and the host (like 30/60 Hz // presented to 144 Hz), drawing from the guest output refreshing thread is // highly desirable. Presenting directly from the GPU emulation thread also // makes debugging GPU emulation easier with external tools, as presenting in // most cases happens exactly between emulation frames. DEFINE_bool( host_present_from_non_ui_thread, true, "Allow the GPU emulation thread to present the guest output to the host " "surface directly instead of requesting the UI thread to do so through the " "host window system.", "Display"); DEFINE_bool( present_render_pass_clear, true, "On graphics backends where this is supported, use the clear render pass " "load operation in presentation instead of clear commands clearing only " "the letterbox area.", "Display"); DEFINE_bool( present_letterbox, true, "Maintain aspect ratio when stretching by displaying bars around the image " "when there's no more overscan area to crop out.", "Display"); // https://github.com/MonoGame/MonoGame/issues/4697#issuecomment-217779403 // Using the value from DirectXTK (5% cropped out from each side, thus 90%), // which is not exactly the Xbox One title-safe area, but close, and within the // action-safe area: // https://github.com/microsoft/DirectXTK/blob/1e80a465c6960b457ef9ab6716672c1443a45024/Src/SimpleMath.cpp#L144 // XNA TitleSafeArea is 80%, but it's very conservative, designed for CRT, and // is the title-safe area rather than the action-safe area. // 90% is also exactly the fraction of 16:9 height in 16:10. DEFINE_int32( present_safe_area_x, 90, "Percentage of the image width that can be kept when presenting to " "maintain aspect ratio without letterboxing or stretching.", "Display"); DEFINE_int32( present_safe_area_y, 90, "Percentage of the image height that can be kept when presenting to " "maintain aspect ratio without letterboxing or stretching.", "Display"); namespace xe { namespace ui { void Presenter::FatalErrorHostGpuLossCallback( [[maybe_unused]] bool is_responsible, [[maybe_unused]] bool statically_from_ui_thread) { xe::FatalError("Graphics device lost (probably due to an internal error)"); } Presenter::~Presenter() { // No intrusive lifetime management must be performed from UI drawers - defer // it if needed. assert_false(is_executing_ui_drawers_); #if XE_PLATFORM_WIN32 if (dxgi_ui_tick_thread_.joinable()) { { std::scoped_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); dxgi_ui_tick_thread_shutdown_ = true; } dxgi_ui_tick_control_condition_.notify_all(); dxgi_ui_tick_thread_.join(); } #endif // XE_PLATFORM if (window_) { Window* old_window = window_; // Null the pointer to prevent an infinite loop between SetPresenter and // SetWindowSurfaceFromUIThread calling each other. window_ = nullptr; old_window->SetPresenter(nullptr); } } void Presenter::SetWindowSurfaceFromUIThread(Window* new_window, Surface* new_surface) { // No intrusive lifetime management must be performed from UI drawers - defer // it if needed. assert_false(is_executing_ui_drawers_); // There can't be a valid surface pointer without a window, as a surface is // created and owned by the window. assert_false(new_surface && !new_window); if (window_ == new_window && (!window_ || surface_ == new_surface)) { // Nothing has changed (or a recursive SetWindowSurfaceFromUIThread > // SetPresenter > SetWindowSurfaceFromUIThread call). return; } // Disconnect from the current surface. if (surface_) { // Take ownership of painting, and also stop accepting paint requests from // the guest output thread - the window (which is required for making them) // may be going away, and there will be a forced paint when the connection // becomes available. SetPaintModeFromUIThread(PaintMode::kNone); DisconnectPaintingFromSurfaceFromUIThread( SurfacePaintConnectionState::kUnconnectedRetryAtStateChange); surface_ = nullptr; UpdateSurfaceMonitorFromUIThread(true); } if (window_ != new_window) { // The window pointer may be accessed by the guest output thread if painting // is possible (or was possible, but the paint attempt has resulted in the // implementation reporting that the connection has become outdated). // However, a painting connection is currently not established at all, so // it's safe to modify the window pointer here. // Detach from the old window if attaching to a different one or just // detaching. SetPresenter for the new window might have been called without // it having been called with nullptr for the old window. if (window_) { Window* old_window = window_; // Null the pointer to prevent an infinite loop between SetPresenter and // SetWindowSurfaceFromUIThread calling each other. window_ = nullptr; old_window->SetPresenter(nullptr); } // Attach to the new one. // This function is called from SetPresenter - don't need to notify the // window of this, as it itself has triggered this. window_ = new_window; } if (new_surface) { assert_true(paint_mode_ == PaintMode::kNone); surface_ = new_surface; UpdateSurfaceMonitorFromUIThread(true); assert_true(surface_paint_connection_state_ == SurfacePaintConnectionState::kUnconnectedRetryAtStateChange); bool request_repaint; UpdateSurfacePaintConnectionFromUIThread(&request_repaint, true); // Request to paint as soon as possible in the UI thread if connected // successfully. if (request_repaint) { RequestPaintOrConnectionRecoveryViaWindow(true); } } } void Presenter::OnSurfaceMonitorUpdateFromUIThread( bool old_monitor_potentially_disconnected) { // No intrusive lifetime management must be performed from UI drawers - defer // it if needed. assert_false(is_executing_ui_drawers_); if (!surface_) { return; } UpdateSurfaceMonitorFromUIThread(old_monitor_potentially_disconnected); } void Presenter::OnSurfaceResizeFromUIThread() { // No intrusive lifetime management must be performed from UI drawers - defer // it if needed. assert_false(is_executing_ui_drawers_); if (!surface_) { return; } // Let the UI thread take ownership of painting (so the connection can be // updated) in a smooth way - downgrade to kUIThreadOnRequest rather than // kNone, because a forced repaint may not be necessary if, for example, the // size internally turns out to be the same after the update, and in this case // the current image may be kept - but the new one must not be missed either // if it becomes available during the resize. if (paint_mode_ == PaintMode::kGuestOutputThreadImmediately) { SetPaintModeFromUIThread(PaintMode::kUIThreadOnRequest); } bool request_repaint; UpdateSurfacePaintConnectionFromUIThread(&request_repaint, true); // Request to repaint as soon as possible in the UI thread if needed. if (request_repaint) { RequestPaintOrConnectionRecoveryViaWindow(true); } } void Presenter::PaintFromUIThread(bool force_paint) { // If there is no surface, this will be a no-op, nothing outdated, nothing to // paint. However, an explicit monitor check is needed because UI framerate // limiting may be tied to signals from the OS for the monitor - but painting // may still occur, for instance, if drawing to a composition surface in the // OS (which will still be live even if the window goes outside any monitor). // But a surface check still won't cause harm, for simplicity. if (!InSurfaceOnMonitorFromUIThread()) { return; } // Defer changes to the paint mode as well as window paint requests, and do // them in this function so they're consistent with the assumptions made here. assert_false(is_in_ui_thread_paint_); is_in_ui_thread_paint_ = true; request_guest_output_paint_after_current_ui_thread_paint_ = false; request_ui_paint_after_current_ui_thread_paint_ = false; // Actualize the connection if the UI needs to be drawn if there was some // explicit paint request (the guest output has been refreshed, and the guest // output thread was asked not to present directly due as the UI needs to be // drawn, or some surface state change has happened so the guest output needs // to be displayed as soon as possible without waiting for the guest to // refresh it, or the guest output thread has been notified that the // connection has become outdated and has requested the UI thread to // reconnect). bool draw_ui = !ui_drawers_.empty(); bool do_paint = force_paint || draw_ui; // Reset ui_thread_paint_requested_ unconditionally also, regardless of // whether the UI needs to be drawn - the flag may be set to try reconnecting, // for example. if (ui_thread_paint_requested_.exchange(false, std::memory_order_relaxed)) { do_paint = true; } PaintResult paint_result = PaintResult::kNotPresented; bool request_repaint_at_tick = false; bool request_repaint_immediately = false; if (do_paint) { // Take ownership of painting if it's currently owned by the guest output // thread (downgrade from kGuestOutputThreadImmediately to // kUIThreadOnRequest - not to kNone so if during this paint a new guest // output frame is generated, the notification will still be sent to the UI // thread rather than dropped, so the frame won't be skipped). This is // needed to be able not only to paint, but also to try to recover from an // outdated surface. if (paint_mode_ == PaintMode::kGuestOutputThreadImmediately) { SetPaintModeFromUIThread(PaintMode::kUIThreadOnRequest); } // Try to recover from the connection becoming outdated in the previous // paint. if (surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedOutdated) { UpdateSurfacePaintConnectionFromUIThread(nullptr, false); } // If still paintable or recovered successfully, paint. if (surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedPaintable) { // The paint mode might have been set to kNone when the connection was // marked as outdated last time. Or, if wasn't reconnecting, there was // some other incorrect situation that caused the paint mode to be set to // kNone for an active connection. Make sure that the current paint mode // is consistent with painting from the UI thread. SetPaintModeFromUIThread(PaintMode::kUIThreadOnRequest); // Limit the frame rate of the UI, usually to the monitor refresh rate, // in a way so that the UI won't be stealing all the remaining GPU // resources if it's repainted continuously, and the window system itself // doesn't limit the frame rate. WaitForUITickFromUIThread(); paint_result = PaintAndPresent(draw_ui); if (surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedOutdated) { // Request another PaintFromUIThread which will try to recover from the // outdated connection in the next frame (not immediately, so the // windowing system has some time to prepare what may be required to // recover from it, such as to send a resize event). request_repaint_immediately = true; } } // If can't paint anymore, notify the paint mode refresh below (which is not // guaranteed to have access to have ownership of painting as it's taken // here only conditionally, thus can't know whether the connection is // actually in a paintable state). if (surface_paint_connection_state_ != SurfacePaintConnectionState::kConnectedPaintable) { SetPaintModeFromUIThread(PaintMode::kNone); } } // Transfer the ownership of painting back to the guest output thread if it // was taken or if needed for any reason (however, it's taken conditionally - // no guarantees that the actual connection state is accessible here, so only // checking whether the mode is not kNone currently, not the connection // state), and overall synchronize the state taking into account both what has // been done in this function and what could have been done by the UI drawer // callbacks. if (paint_mode_ != PaintMode::kNone) { SetPaintModeFromUIThread(GetDesiredPaintModeFromUIThread(true)); } is_in_ui_thread_paint_ = false; // Check if the device has been lost. There's no point in requesting repaint // if it has happened anyway, it won't be possible to satisfy such request // with the current Presenter. if (paint_result == PaintResult::kGpuLostExternally || paint_result == PaintResult::kGpuLostResponsible) { if (host_gpu_loss_callback_) { host_gpu_loss_callback_(paint_result == PaintResult::kGpuLostResponsible, true); } // The loss callback might have destroyed the presenter, must not do // anything with `this` anymore. return; } // Request refresh if needed. // Can't check the exact paintability as the connection state may currently // be owned by the guest output thread, so check conservatively via // paint_mode_. if (paint_mode_ != PaintMode::kNone) { // Immediately paint the guest output if requested explicitly or if the UI // has hidden itself. if (request_guest_output_paint_after_current_ui_thread_paint_ || (draw_ui && ui_drawers_.empty())) { request_repaint_immediately = true; } if (request_ui_paint_after_current_ui_thread_paint_ && !ui_drawers_.empty()) { request_repaint_at_tick = true; } } if (request_repaint_at_tick || request_repaint_immediately) { RequestPaintOrConnectionRecoveryViaWindow(request_repaint_immediately); } } bool Presenter::RefreshGuestOutput( uint32_t frontbuffer_width, uint32_t frontbuffer_height, uint32_t screen_width, uint32_t screen_height, std::function refresher) { GuestOutputProperties& writable_properties = guest_output_properties_[guest_output_mailbox_writable_]; writable_properties.frontbuffer_width = frontbuffer_width; writable_properties.frontbuffer_height = frontbuffer_height; writable_properties.screen_width = screen_width; writable_properties.screen_height = screen_height; writable_properties.is_8bpc = false; bool is_active = writable_properties.IsActive(); if (is_active) { if (!RefreshGuestOutputImpl(guest_output_mailbox_writable_, frontbuffer_width, frontbuffer_height, refresher, writable_properties.is_8bpc)) { // If failed to refresh, don't send the currently writable image to the // mailbox as it may be in an undefined state. Don't disable the guest // output either though because the failure may be something transient. return false; } guest_output_active_last_refresh_ = true; } else { // Request presenting a blank image if there was a true image previously, // but not now. if (!guest_output_active_last_refresh_) { return false; } guest_output_active_last_refresh_ = false; } // Make the new image the next to present on the host (the "ready" one), // replacing the one already specified as the next (dropping it instead of // enqueueing the new image after it) to achieve the lowest latency (also, // after switching from UI thread painting to doing it in the guest output // thread, will immediately recover to having the latest frame always sent to // the host present call on the CPU and all frames reaching a present call). uint32_t last_acquired_and_ready = guest_output_mailbox_acquired_and_ready_.load(std::memory_order_relaxed); // Desired acquired = current acquired (changed only by the consumers). // Desired ready = current writable. // memory_order_acq_rel to acquire the new writable image and to release the // current one (to let the consumers take it, from ready to acquired). while (!guest_output_mailbox_acquired_and_ready_.compare_exchange_weak( last_acquired_and_ready, (last_acquired_and_ready & 3) | (guest_output_mailbox_writable_ << 2), std::memory_order_acq_rel, std::memory_order_relaxed)) { } // Now, it's known that `ready == writable` on the host presentation side. // Take the next `writable` with this assumption about its current value in // mind. uint32_t last_acquired = last_acquired_and_ready & 3; if (last_acquired == guest_output_mailbox_writable_) { // The new image has already been acquired by the time the compare-exchange // loop has finished (acquired == ready == currently writable). // It's a valid situation from the ownership perspective, and the semantics // of the weak compare-exchange explicitly permit spurious `false` results. // (3 - a - b) % 3 cannot be used here, as (3 - a - a) % 3 results in `a` - // the same index. // Take any free image. Preferably using + 1, not ^ 1, so if the guest needs // to await any GPU work referencing the image, it will wait for the frame 3 // frames ago, not 2, if this happens repeatedly. guest_output_mailbox_writable_ = (guest_output_mailbox_writable_ + 1) % 3; } else { // Take the image other than the last acquired one and the new one, // currently not accessible to the host presentation. guest_output_mailbox_writable_ = (3 - last_acquired - guest_output_mailbox_writable_) % 3; } // Trigger the presentation on the host. PaintResult paint_result = PaintResult::kNotPresented; { std::lock_guard paint_mode_mutex_lock(paint_mode_mutex_); switch (paint_mode_) { case PaintMode::kNone: // Neither painting nor window paint requesting is accessible. break; case PaintMode::kUIThreadOnRequest: // Only window paint requesting is accessible. RequestPaintOrConnectionRecoveryViaWindow(true); break; case PaintMode::kGuestOutputThreadImmediately: // Both painting and window paint requesting are accessible. if (surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedPaintable) { paint_result = PaintAndPresent(false); if (surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedOutdated) { RequestPaintOrConnectionRecoveryViaWindow(true); } } break; } } // Handle GPU loss when not in the middle of the function anymore, and // lifecycle management from the GPU loss callback is fine on the UI thread. if (host_gpu_loss_callback_) { if (paint_result == PaintResult::kGpuLostResponsible) { host_gpu_loss_callback_(true, false); } else if (paint_result == PaintResult::kGpuLostExternally) { host_gpu_loss_callback_(false, false); } } return is_active; } void Presenter::SetGuestOutputPaintConfigFromUIThread( const GuestOutputPaintConfig& new_config) { // For simplicity, this may be called externally repeatedly. // Lock the mutex only when something has been modified, and also don't // request UI thread guest output redraws when not needed. bool modified = false; bool request_repaint = false; if (guest_output_paint_config_.GetEffect() != new_config.GetEffect()) { modified = true; request_repaint = true; } if (guest_output_paint_config_.GetFsrSharpnessReduction() != new_config.GetFsrSharpnessReduction()) { modified = true; if (new_config.GetEffect() == GuestOutputPaintConfig::Effect::kFsr) { request_repaint = true; } } if (guest_output_paint_config_.GetCasAdditionalSharpness() != new_config.GetCasAdditionalSharpness()) { modified = true; if (new_config.GetEffect() == GuestOutputPaintConfig::Effect::kCas || new_config.GetEffect() == GuestOutputPaintConfig::Effect::kFsr) { request_repaint = true; } } if (guest_output_paint_config_.GetDither() != new_config.GetDither()) { modified = true; request_repaint = true; } if (modified) { { std::unique_lock config_lock( guest_output_paint_config_mutex_); guest_output_paint_config_ = new_config; } // Coarsely check the availability of painting and of the window (for // calling RequestPaint) via paint_mode_ because the actual painting // connection state may currently be owned not by the UI thread. if (request_repaint && paint_mode_ != PaintMode::kNone) { if (is_in_ui_thread_paint_) { // Defer until the end of the current paint if called from, for // instance, a UI drawer. request_guest_output_paint_after_current_ui_thread_paint_ = true; } else { RequestPaintOrConnectionRecoveryViaWindow(true); } } } } void Presenter::AddUIDrawerFromUIThread(UIDrawer* drawer, size_t z_order) { assert_not_null(drawer); // Obtain whether the iterator list was empty before erasing in case of // replacing with a new entry with a different Z order happens. bool drawers_were_empty = ui_drawers_.empty(); uint64_t drawer_last_draw = UINT64_MAX; // Check if already added. for (auto it_existing = ui_drawers_.begin(); it_existing != ui_drawers_.end(); ++it_existing) { if (it_existing->second.drawer != drawer) { continue; } if (it_existing->first == z_order) { return; } // Keep the same last draw index to prevent the drawer from being executed // twice if increasing its Z order during the drawer loop. drawer_last_draw = it_existing->second.last_draw; // If removing the drawer that is the next in the current drawer loop, skip // it (in a multimap, only one element iterator is invalidated). if (is_executing_ui_drawers_ && ui_draw_next_iterator_ == it_existing) { ++ui_draw_next_iterator_; } ui_drawers_.erase(it_existing); break; } auto it_new = ui_drawers_.emplace(z_order, UIDrawerReference(drawer, drawer_last_draw)); // If adding to the Z layer currently being processed (for drawing, from the // lowest to the highest), or to layers in between the current and the // previously next, make sure the new drawer is executed too. if (is_executing_ui_drawers_ && z_order >= ui_draw_current_z_order_ && (ui_draw_next_iterator_ == ui_drawers_.end() || z_order < ui_draw_next_iterator_->first)) { ui_draw_next_iterator_ = it_new; } HandleUIDrawersChangeFromUIThread(drawers_were_empty); } void Presenter::RemoveUIDrawerFromUIThread(UIDrawer* drawer) { assert_not_null(drawer); for (auto it_existing = ui_drawers_.begin(); it_existing != ui_drawers_.end(); ++it_existing) { if (it_existing->second.drawer != drawer) { continue; } // If removing the drawer that is the next in the current drawer loop, skip // it (in a multimap, only one element iterator is invalidated). if (is_executing_ui_drawers_ && ui_draw_next_iterator_ == it_existing) { ++ui_draw_next_iterator_; } ui_drawers_.erase(it_existing); HandleUIDrawersChangeFromUIThread(false); return; } } void Presenter::RequestUIPaintFromUIThread() { if (is_in_ui_thread_paint_) { // The paint request will be done once in the end of PaintFromUIThread // according to the actual state at the moment that happens. It's common for // drawers to call this (even every frame), and no need to do too many OS // paint request calls. request_ui_paint_after_current_ui_thread_paint_ = true; return; } // The connection state may be owned by the guest output thread now rather // than the UI thread, check whether it's not pointless to make the request // coarsely via paint_mode_. if (!ui_drawers_.empty() && paint_mode_ != PaintMode::kNone) { // The window must be present, otherwise the conditions wouldn't have been // met. window_->RequestPaint(); } } bool Presenter::InitializeCommonSurfaceIndependent() { // Initialize UI frame rate limiting. #if XE_PLATFORM_WIN32 dxgi_ui_tick_thread_ = std::thread(&Presenter::DXGIUITickThread, this); #endif // XE_PLATFORM return true; } std::unique_lock Presenter::ConsumeGuestOutput( uint32_t& mailbox_index_or_max_if_inactive_out, GuestOutputProperties* properties_out, GuestOutputPaintConfig* paint_config_out) { if (paint_config_out) { // Get the up-to-date guest output paint configuration settings set by the // UI thread. std::unique_lock config_lock(guest_output_paint_config_mutex_); *paint_config_out = guest_output_paint_config_; } // Lock the mutex to make sure the image that will be acquired now is owned // exclusively by the calling thread for the time while this mutex is still // locked (it needs to be held by the consumer while working with anything // that depends on the image now being acquired or its index in the mailbox). std::unique_lock consumer_lock( guest_output_mailbox_consumer_mutex_); // Acquire the up-to-date ready guest image (may be new, in this case the last // acquired one will be released, or still the same or no refresh has happened // since the last consumption). // memory_order_relaxed here because the ready index from this load will be // used directly only if it's the same as during the last consumption - thus // the image has already been acquired previously, no need for // memory_order_acquire (if the image has been acquired by a different // consumer though, the consumer mutex performs memory access ordering). uint32_t old_acquired_and_ready = guest_output_mailbox_acquired_and_ready_.load(std::memory_order_relaxed); // Desired acquired = current ready. // Desired ready = current ready (changed only by the producer). uint32_t desired_acquired_and_ready = (old_acquired_and_ready & ~uint32_t(3)) | (old_acquired_and_ready >> 2); // Either the same image as during the last consumption, or a new one, is // satisfying. However, if it's new, using memory_order_acq_rel to acquire the // new ready image (to make it acquired) and to release the old acquired image // (to let the producer take it as writable). while (old_acquired_and_ready != desired_acquired_and_ready && !guest_output_mailbox_acquired_and_ready_.compare_exchange_weak( old_acquired_and_ready, desired_acquired_and_ready, std::memory_order_acq_rel, std::memory_order_relaxed)) { desired_acquired_and_ready = (old_acquired_and_ready & ~uint32_t(3)) | (old_acquired_and_ready >> 2); } uint32_t mailbox_index = desired_acquired_and_ready & 3; // Give the current acquired image to the caller, or UINT32_MAX if it's // inactive. const GuestOutputProperties& properties = guest_output_properties_[mailbox_index]; mailbox_index_or_max_if_inactive_out = properties.IsActive() ? mailbox_index : UINT32_MAX; if (properties_out) { *properties_out = properties; } return std::move(consumer_lock); } Presenter::GuestOutputPaintFlow Presenter::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 { GuestOutputPaintFlow flow = {}; // FIXME(Triang3l): Configuration variables racing with per-game config // loading. assert_not_zero(max_rt_width); assert_not_zero(max_rt_height); // Initialize one clear rectangle for the case of drawing no guest output, for // consistency with fewer state dependencies. flow.letterbox_clear_rectangle_count = 1; flow.letterbox_clear_rectangles[0].width = host_rt_width; flow.letterbox_clear_rectangles[0].height = host_rt_height; // For safety such as division by zero prevention. if (!properties.IsActive() || !host_rt_width || !host_rt_height || !surface_width_in_paint_connection_ || !surface_height_in_paint_connection_) { return flow; } flow.properties = properties; // Multiplication-division rounding to the nearest. auto rescale_unsigned = [](uint32_t value, uint32_t new_scale, uint32_t old_scale) -> uint32_t { return uint32_t((uint64_t(value) * new_scale + (old_scale >> 1)) / old_scale); }; auto rescale_signed = [](int32_t value, uint32_t new_scale, uint32_t old_scale) -> int32_t { // Plus old_scale / 2 for positive values, minus old_scale / 2 for // negative values for consistent rounding for both positive and // negative values (as the `/` operator rounds towards zero). // Example: // (-3 - 1) / 3 == -1 // (-2 - 1) / 3 == -1 // (-1 - 1) / 3 == 0 // --- // (0 + 1) / 3 == 0 // (1 + 1) / 3 == 0 // (2 + 1) / 3 == 1 return int32_t((int64_t(value) * new_scale + int32_t(old_scale >> 1) * (value < 0 ? -1 : 1)) / old_scale); }; // Final output location and dimensions. // All host location calculations are DPI-independent, conceptually depending // only on the aspect ratios, not the absolute values. uint32_t output_width, output_height; if (uint64_t(surface_width_in_paint_connection_) * properties.screen_height > uint64_t(properties.screen_width) * surface_height_in_paint_connection_) { // The window is wider that the source - crop along Y to preserve the aspect // ratio while stretching throughout the entire surface's width, then limit // the Y cropping via letterboxing or stretching along X. uint32_t present_safe_area; if (cvars::present_safe_area_y > 0 && cvars::present_safe_area_y < 100) { present_safe_area = uint32_t(cvars::present_safe_area_y); } else { present_safe_area = 100; } // Scale the desired width by the H:W aspect ratio (inverse of W:H) to get // the height. output_height = rescale_unsigned(surface_width_in_paint_connection_, properties.screen_height, properties.screen_width); bool letterbox = false; if (output_height * present_safe_area > surface_height_in_paint_connection_ * 100) { // Don't crop out more than the safe area margin - letterbox or stretch. output_height = rescale_unsigned(surface_height_in_paint_connection_, 100, present_safe_area); letterbox = true; } if (letterbox && cvars::present_letterbox) { output_width = rescale_unsigned( properties.screen_width, surface_height_in_paint_connection_ * 100, properties.screen_height * present_safe_area); // output_width might have been rounded up already by rescale_unsigned, so // rounding down in this division. flow.output_x = (int32_t(surface_width_in_paint_connection_) - int32_t(output_width)) / 2; } else { output_width = surface_width_in_paint_connection_; flow.output_x = 0; } // output_height might have been rounded up already by rescale_unsigned, so // rounding down in this division. flow.output_y = (int32_t(surface_height_in_paint_connection_) - int32_t(output_height)) / 2; } else { // The window is taller that the source - crop along X to preserve the // aspect ratio while stretching throughout the entire surface's height, // then limit the X cropping via letterboxing or stretching along Y. uint32_t present_safe_area; if (cvars::present_safe_area_x > 0 && cvars::present_safe_area_x < 100) { present_safe_area = uint32_t(cvars::present_safe_area_x); } else { present_safe_area = 100; } // Scale the desired height by the W:H aspect ratio to get the width. output_width = rescale_unsigned(surface_height_in_paint_connection_, properties.screen_width, properties.screen_height); bool letterbox = false; if (output_width * present_safe_area > surface_width_in_paint_connection_ * 100) { // Don't crop out more than the safe area margin - letterbox or stretch. output_width = rescale_unsigned(surface_width_in_paint_connection_, 100, present_safe_area); letterbox = true; } if (letterbox && cvars::present_letterbox) { output_height = rescale_unsigned( properties.screen_height, surface_width_in_paint_connection_ * 100, properties.screen_width * present_safe_area); // output_height might have been rounded up already by rescale_unsigned, // so rounding down in this division. flow.output_y = (int32_t(surface_height_in_paint_connection_) - int32_t(output_height)) / 2; } else { output_height = surface_height_in_paint_connection_; flow.output_y = 0; } // output_width might have been rounded up already by rescale_unsigned, so // rounding down in this division. flow.output_x = (int32_t(surface_width_in_paint_connection_) - int32_t(output_width)) / 2; } // Convert the location from surface pixels (which have 1:1 aspect ratio // relatively to the physical display) to render target pixels (the render // target size may be arbitrary with any aspect ratio, but if it's different // than the surface size, the OS is expected to stretch it to the surface // boundaries), preserving the aspect ratio. if (host_rt_width != surface_width_in_paint_connection_) { flow.output_x = rescale_signed(flow.output_x, host_rt_width, surface_width_in_paint_connection_); output_width = rescale_unsigned(output_width, host_rt_width, surface_width_in_paint_connection_); } if (host_rt_height != surface_height_in_paint_connection_) { flow.output_y = rescale_signed(flow.output_y, host_rt_height, surface_height_in_paint_connection_); output_height = rescale_unsigned(output_height, host_rt_height, surface_height_in_paint_connection_); } // The out-of-bounds checks are needed for correct letterbox calculations. // Though this normally shouldn't happen, but in case of rounding issues with // extreme values. int32_t output_right = flow.output_x + int32_t(output_width); int32_t output_bottom = flow.output_y + int32_t(output_height); if (!output_width || !output_height || output_right <= 0 || output_bottom <= 0 || flow.output_x >= int32_t(host_rt_width) || flow.output_y >= int32_t(host_rt_height)) { return flow; } // The output image may have a part of it outside the final render target (if // using the overscan area to stretch the image to the entire surface while // preserving the guest aspect ratio if it differs from the host one, for // instance). While the final render target size is known to be within the // host render target / image size limit, the intermediate images may be // larger than that as they include the overscan area that will be outside the // screen. Make sure the intermediate images can't be larger than the maximum // render target size. uint32_t output_width_clamped = std::min(output_width, max_rt_width); uint32_t output_height_clamped = std::min(output_height, max_rt_height); if (config.GetEffect() == GuestOutputPaintConfig::Effect::kCas || config.GetEffect() == GuestOutputPaintConfig::Effect::kFsr) { // FidelityFX Super Resolution and Contrast Adaptive Sharpening only work // good for up to 2x2 upscaling due to the way they fetch texels. // CAS is primarily a sharpening filter, not an upscaling one (its upscaling // eliminates reduces blurriness, but doesn't preserve the shapes of edges, // and executing it multiple times will only result in oversharpening. So, // using it for scales only of up to 2x2, then simply stretching with // bilinear filtering. // EASU of FSR, however, preserves edges, it's not supposed to blur them or // to make them jagged, so it can be executed multiple times - running // multiple EASU passes for scale factors of over 2x2. // Just one EASU pass rather than multiple for scaling to factors bigger // than 2x2 (especially significantly bigger, such as 1152x640 to 3840x2160, // or 3.333x3.375) results in blurry edges and an overall noisy look, // multiple passes improve visual stability. std::pair ffx_last_size; if (flow.effect_count) { ffx_last_size = flow.effect_output_sizes[flow.effect_count - 1]; } else { ffx_last_size.first = properties.frontbuffer_width; ffx_last_size.second = properties.frontbuffer_height; } if (config.GetEffect() == GuestOutputPaintConfig::Effect::kFsr && (ffx_last_size.first < output_width_clamped || ffx_last_size.second < output_height_clamped)) { // AMD FidelityFX Super Resolution - upsample along at least one axis. // Using the output size clamped to the maximum render target size here as // EASU will always write to intermediate images, and RCAS supports only // 1:1. uint32_t easu_max_passes = config.GetFsrMaxUpsamplingPasses(); uint32_t easu_pass_count = 0; while (easu_pass_count < easu_max_passes && (ffx_last_size.first < output_width_clamped || ffx_last_size.second < output_height_clamped)) { ffx_last_size.first = std::min(ffx_last_size.first * uint32_t(2), output_width_clamped); ffx_last_size.second = std::min(ffx_last_size.second * uint32_t(2), output_height_clamped); assert_true(flow.effect_count < flow.effects.size()); flow.effect_output_sizes[flow.effect_count] = ffx_last_size; flow.effects[flow.effect_count++] = GuestOutputPaintEffect::kFsrEasu; ++easu_pass_count; } assert_true(flow.effect_count < flow.effects.size()); flow.effect_output_sizes[flow.effect_count] = ffx_last_size; flow.effects[flow.effect_count++] = GuestOutputPaintEffect::kFsrRcas; } else { // AMD FidelityFX Contrast Adaptive Sharpening - sharpen or downsample, or // upsample up to 2x2 if CAS is specified to be used for upscaling too. // Using the unclamped output size as CAS may be the last pass - if a // bilinear pass is needed afterwards, and the CAS pass will be writing to // an intermediate image, the CAS pass output size will be clamped while // adding the bilinear stretch. std::pair pre_cas_size = ffx_last_size; ffx_last_size.first = std::min(ffx_last_size.first * uint32_t(2), output_width); ffx_last_size.second = std::min(ffx_last_size.second * uint32_t(2), output_height); assert_true(flow.effect_count < flow.effects.size()); flow.effect_output_sizes[flow.effect_count] = ffx_last_size; flow.effects[flow.effect_count++] = ffx_last_size == pre_cas_size ? GuestOutputPaintEffect::kCasSharpen : GuestOutputPaintEffect::kCasResample; } } std::pair* last_pre_bilinear_effect_size = flow.effect_count ? &flow.effect_output_sizes[flow.effect_count - 1] : nullptr; if (!last_pre_bilinear_effect_size || last_pre_bilinear_effect_size->first != output_width || last_pre_bilinear_effect_size->second != output_height) { // If not using FidelityFX, or it has reached its upscaling capabilities, // but more is needed, stretch via bilinear filtering. // Clamp the output size of the last effect to the maximum render target // size because it will go to an intermediate image now. if (last_pre_bilinear_effect_size) { // RCAS only works for 1:1, clamping must be done explicitly for FSR. assert_false(flow.effects[flow.effect_count - 1] == GuestOutputPaintEffect::kFsrRcas && (last_pre_bilinear_effect_size->first > max_rt_width || last_pre_bilinear_effect_size->second > max_rt_height)); last_pre_bilinear_effect_size->first = std::min(last_pre_bilinear_effect_size->first, max_rt_width); last_pre_bilinear_effect_size->second = std::min(last_pre_bilinear_effect_size->second, max_rt_height); } assert_true(flow.effect_count < flow.effects.size()); flow.effect_output_sizes[flow.effect_count] = std::make_pair(output_width, output_height); flow.effects[flow.effect_count++] = GuestOutputPaintEffect::kBilinear; } assert_not_zero(flow.effect_count); if (config.GetDither()) { // Dithering must be applied only to the final effect since resampling and // sharpening filters may considering the dithering noise features and // amplify it. GuestOutputPaintEffect& last_effect = flow.effects[flow.effect_count - 1]; switch (last_effect) { case GuestOutputPaintEffect::kBilinear: // Dithering has no effect for 1:1 copying of a 8bpc image. if (!properties.is_8bpc || flow.effect_count > 1 || output_width != properties.frontbuffer_width || output_height != properties.frontbuffer_height) { last_effect = GuestOutputPaintEffect::kBilinearDither; } break; case GuestOutputPaintEffect::kCasSharpen: last_effect = GuestOutputPaintEffect::kCasSharpenDither; break; case GuestOutputPaintEffect::kCasResample: last_effect = GuestOutputPaintEffect::kCasResampleDither; break; case GuestOutputPaintEffect::kFsrRcas: last_effect = GuestOutputPaintEffect::kFsrRcasDither; break; default: break; } } #ifndef NDEBUG for (size_t i = 0; i + 1 < flow.effect_count; ++i) { assert_true(CanGuestOutputPaintEffectBeIntermediate(flow.effects[i])); } assert_true( CanGuestOutputPaintEffectBeFinal(flow.effects[flow.effect_count - 1])); #endif // Calculate the letterbox geometry. if (flow.effect_count) { flow.letterbox_clear_rectangle_count = 0; uint32_t letterbox_mid_top = uint32_t(std::max(flow.output_y, int32_t(0))); // Top. if (letterbox_mid_top) { assert_true(flow.letterbox_clear_rectangle_count < flow.letterbox_clear_rectangles.size()); GuestOutputPaintFlow::ClearRectangle& letterbox_clear_rectangle_top = flow.letterbox_clear_rectangles [flow.letterbox_clear_rectangle_count++]; letterbox_clear_rectangle_top.x = 0; letterbox_clear_rectangle_top.y = 0; letterbox_clear_rectangle_top.width = host_rt_width; letterbox_clear_rectangle_top.height = letterbox_mid_top; } uint32_t letterbox_mid_bottom = std::min(uint32_t(output_bottom), host_rt_height); uint32_t letterbox_mid_height = letterbox_mid_bottom - letterbox_mid_top; // Middle-left. if (flow.output_x > 0) { assert_true(flow.letterbox_clear_rectangle_count < flow.letterbox_clear_rectangles.size()); GuestOutputPaintFlow::ClearRectangle& letterbox_clear_rectangle_left = flow.letterbox_clear_rectangles [flow.letterbox_clear_rectangle_count++]; letterbox_clear_rectangle_left.x = 0; letterbox_clear_rectangle_left.y = letterbox_mid_top; letterbox_clear_rectangle_left.width = uint32_t(flow.output_x); letterbox_clear_rectangle_left.height = letterbox_mid_height; } // Middle-right. if (uint32_t(output_right) < host_rt_width) { assert_true(flow.letterbox_clear_rectangle_count < flow.letterbox_clear_rectangles.size()); GuestOutputPaintFlow::ClearRectangle& letterbox_clear_rectangle_right = flow.letterbox_clear_rectangles [flow.letterbox_clear_rectangle_count++]; letterbox_clear_rectangle_right.x = uint32_t(output_right); letterbox_clear_rectangle_right.y = letterbox_mid_top; letterbox_clear_rectangle_right.width = host_rt_width - uint32_t(output_right); letterbox_clear_rectangle_right.height = letterbox_mid_height; } // Bottom. if (letterbox_mid_bottom < host_rt_height) { assert_true(flow.letterbox_clear_rectangle_count < flow.letterbox_clear_rectangles.size()); GuestOutputPaintFlow::ClearRectangle& letterbox_clear_rectangle_top = flow.letterbox_clear_rectangles [flow.letterbox_clear_rectangle_count++]; letterbox_clear_rectangle_top.x = 0; letterbox_clear_rectangle_top.y = letterbox_mid_bottom; letterbox_clear_rectangle_top.width = host_rt_width; letterbox_clear_rectangle_top.height = host_rt_height - letterbox_mid_bottom; } } return flow; } void Presenter::ExecuteUIDrawersFromUIThread(UIDrawContext& ui_draw_context) { // May be called by the implementations only when requested. assert_true(is_in_ui_thread_paint_); // Drawers can add or remove drawers (including themselves), need to ensure // iterator validity in this case. assert_false(is_executing_ui_drawers_); ui_draw_next_iterator_ = ui_drawers_.begin(); is_executing_ui_drawers_ = true; while (ui_draw_next_iterator_ != ui_drawers_.end()) { // The current iterator may be invalidated, and ui_draw_next_iterator_ may // be changed, during the execution of the drawer if the list of the drawers // is modified by it - don't assume that after the call // ui_draw_next_iterator_ will be the same as // std::next(ui_draw_next_iterator_) before it. auto it_current = ui_draw_next_iterator_++; // Don't draw twice if already drawn in this frame (may happen if the Z // order of a drawer was increased from below the current one to above it by // one of the drawers). if (it_current->second.last_draw != ui_draw_current_) { ui_draw_current_z_order_ = it_current->first; it_current->second.last_draw = ui_draw_current_; it_current->second.drawer->Draw(ui_draw_context); } } is_executing_ui_drawers_ = false; ++ui_draw_current_; } void Presenter::SetPaintModeFromUIThread(PaintMode new_mode) { // Can be modified only from the UI thread, so can skip locking if it's the // same. if (paint_mode_ == new_mode) { return; } { std::lock_guard lock(paint_mode_mutex_); paint_mode_ = new_mode; } UpdateUITicksNeededFromUIThread(); } Presenter::PaintMode Presenter::GetDesiredPaintModeFromUIThread( bool is_paintable) const { if (!is_paintable) { // The only case when kNone can be returned, for surface connection updates // when it's known that the UI thread currently has access to the connection // lifecycle. return PaintMode::kNone; } if (!cvars::host_present_from_non_ui_thread) { return PaintMode::kUIThreadOnRequest; } if (surface_paint_connection_has_implicit_vsync_) { // Don't be causing host vertical sync CPU waits in the thread generating // the guest output. return PaintMode::kUIThreadOnRequest; } if (!ui_drawers_.empty()) { // The UI can be drawn only by the UI thread, and it needs to be drawn - // paint in the UI thread. return PaintMode::kUIThreadOnRequest; } // Only the guest output needs to be drawn - let the guest output thread // present immediately for a lower latency. return PaintMode::kGuestOutputThreadImmediately; } void Presenter::DisconnectPaintingFromSurfaceFromUIThread( SurfacePaintConnectionState new_state) { assert_false(IsConnectedSurfacePaintConnectionState(new_state)); if (IsConnectedSurfacePaintConnectionState(surface_paint_connection_state_)) { DisconnectPaintingFromSurfaceFromUIThreadImpl(); } surface_paint_connection_state_ = new_state; surface_paint_connection_has_implicit_vsync_ = false; surface_width_in_paint_connection_ = 0; surface_height_in_paint_connection_ = 0; } void Presenter::UpdateSurfacePaintConnectionFromUIThread( bool* repaint_needed_out, bool update_paint_mode_to_desired) { assert_not_null(surface_); // Validate that painting lifecycle is accessible by the UI thread currently, // not given to the guest output thread. The mode can be modified only by // the UI thread, so no need to lock the mutex. assert_true(paint_mode_ != PaintMode::kGuestOutputThreadImmediately); // Initialize repaint_needed_out for failure cases. if (repaint_needed_out) { *repaint_needed_out = false; } // If the connection state is kUnconnectedSurfaceReportedUnusable, the // implementation has reported that the surface is not usable by the presenter // at all, and it's pointless to retry connecting to it. if (surface_paint_connection_state_ != SurfacePaintConnectionState::kUnconnectedSurfaceReportedUnusable) { uint32_t surface_width = 0, surface_height = 0; bool surface_area_available = surface_->GetSize(surface_width, surface_height); if (!surface_area_available) { // The surface is currently zero-area (or has become zero-area), try again // when it's resized. DisconnectPaintingFromSurfaceFromUIThread( SurfacePaintConnectionState::kUnconnectedRetryAtStateChange); } else { bool is_reconnect = IsConnectedSurfacePaintConnectionState( surface_paint_connection_state_); bool is_vsync_implicit = false; SurfacePaintConnectResult connect_result = ConnectOrReconnectPaintingToSurfaceFromUIThread( *surface_, surface_width, surface_height, surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedPaintable, is_vsync_implicit); switch (connect_result) { case SurfacePaintConnectResult::kSuccess: if (repaint_needed_out) { *repaint_needed_out = true; } // Fallthrough to common success handling. case SurfacePaintConnectResult::kSuccessUnchanged: // Don't know yet what the first result was (success or suboptimal). surface_paint_connection_was_optimal_at_successful_paint_ = false; surface_paint_connection_state_ = SurfacePaintConnectionState::kConnectedPaintable; surface_paint_connection_has_implicit_vsync_ = is_vsync_implicit; surface_width_in_paint_connection_ = surface_width; surface_height_in_paint_connection_ = surface_height; if (!is_reconnect) { *repaint_needed_out = true; } break; case SurfacePaintConnectResult::kFailure: surface_paint_connection_state_ = SurfacePaintConnectionState::kUnconnectedRetryAtStateChange; break; case SurfacePaintConnectResult::kFailureSurfaceUnusable: surface_paint_connection_state_ = SurfacePaintConnectionState::kUnconnectedSurfaceReportedUnusable; break; } } } if (update_paint_mode_to_desired) { SetPaintModeFromUIThread(GetDesiredPaintModeFromUIThread( surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedPaintable)); } } bool Presenter::RequestPaintOrConnectionRecoveryViaWindow( bool force_ui_thread_paint_tick) { // Can be called from any thread if an existing window_ is available in it, // and it's known to have a Surface that will be the same throughout this // call - not doing any checks whether this request can be satisfied // theoretically. For safety, check whether the window exists unconditionally. assert_not_null(window_); assert_not_null(surface_); if (ui_thread_paint_requested_.exchange(true, std::memory_order_relaxed)) { // Invalidation pending already, no need to do it twice. return false; } if (force_ui_thread_paint_tick) { ForceUIThreadPaintTick(); } window_->RequestPaint(); return true; } void Presenter::UpdateSurfaceMonitorFromUIThread( bool old_monitor_potentially_disconnected) { // For dropping the monitor when the window is closing and is losing its // surface, the existence of `surface_` (which implies that `window_` exists // too) must be the condition for a non-null monitor, not just the existence // of `window_`. #if XE_PLATFORM_WIN32 HMONITOR surface_new_win32_monitor = nullptr; if (surface_) { HWND hwnd = static_cast(window_)->hwnd(); // The HWND may be non-existent if the window has been closed and destroyed // (the HWND, not the xe::ui::Window) already. if (hwnd) { surface_new_win32_monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL); } } if (old_monitor_potentially_disconnected || surface_win32_monitor_ != surface_new_win32_monitor) { surface_win32_monitor_ = surface_new_win32_monitor; if (dxgi_ui_tick_factory_ && !dxgi_ui_tick_factory_->IsCurrent()) { // If a monitor has been newly connected, it won't appear in the old // factory, need to recreate it. { Microsoft::WRL::ComPtr old_factory_output_to_release; { std::scoped_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); old_factory_output_to_release = std::move(dxgi_ui_tick_output_); } } dxgi_ui_tick_factory_.Reset(); } if (!dxgi_ui_tick_factory_) { if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_ui_tick_factory_)))) { XELOGE("Presenter: Failed to create a DXGI factory"); } } Microsoft::WRL::ComPtr new_dxgi_output; if (dxgi_ui_tick_factory_ && surface_new_win32_monitor) { new_dxgi_output = GetDXGIOutputForMonitor(dxgi_ui_tick_factory_.Get(), surface_new_win32_monitor); } // If the adapter was recreated, and the old output was released before its // destruction, notifying is still required - the vertical blank wait thread // might have entered the condition variable wait already as the output was // null. bool signal_dxgi_ui_tick_control; { std::unique_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); bool dxgi_output_was_null = (dxgi_ui_tick_output_ == nullptr); dxgi_ui_tick_output_ = new_dxgi_output; signal_dxgi_ui_tick_control = dxgi_output_was_null && AreDXGIUITicksWaitable(dxgi_ui_tick_lock); } if (signal_dxgi_ui_tick_control) { dxgi_ui_tick_control_condition_.notify_all(); } } #endif // XE_PLATFORM } bool Presenter::InSurfaceOnMonitorFromUIThread() const { if (!surface_) { return false; } #if XE_PLATFORM_WIN32 return surface_win32_monitor_ != nullptr; #else return true; #endif // XE_PLATFORM } Presenter::PaintResult Presenter::PaintAndPresent(bool execute_ui_drawers) { assert_false(execute_ui_drawers && !is_in_ui_thread_paint_); assert_true(surface_paint_connection_state_ == SurfacePaintConnectionState::kConnectedPaintable); PaintResult result = PaintAndPresentImpl(execute_ui_drawers); switch (result) { case PaintResult::kPresented: surface_paint_connection_was_optimal_at_successful_paint_ = true; break; case PaintResult::kPresentedSuboptimal: // Make outdated if previously optimal, now suboptimal, but don't cause // the connection to become outdated if it has been suboptimal from the // very beginning. if (surface_paint_connection_was_optimal_at_successful_paint_) { surface_paint_connection_state_ = SurfacePaintConnectionState::kConnectedOutdated; } break; case PaintResult::kNotPresentedConnectionOutdated: surface_paint_connection_state_ = SurfacePaintConnectionState::kConnectedOutdated; break; default: // Another issue not directly related to the surface connection. break; } return result; } void Presenter::HandleUIDrawersChangeFromUIThread(bool drawers_were_empty) { if (is_in_ui_thread_paint_) { // Defer the refresh so no dangerous lifecycle-related changes happen during // drawing. if (!ui_drawers_.empty()) { request_ui_paint_after_current_ui_thread_paint_ = true; } return; } if (paint_mode_ == PaintMode::kNone) { // Not connected, no point in refreshing (checking a more conservative // paint_mode_ because the actual connection state may currently be owned by // the guest output thread instead) or in toggling the ownership (the rest // of the function can assume it's not kNone). return; } if (ui_drawers_.empty() != drawers_were_empty) { // Require the UI thread to paint if it needs the UI, or let the guest // output thread paint immediately if not. SetPaintModeFromUIThread(GetDesiredPaintModeFromUIThread(true)); // Make sure the ticks for limiting the UI frame rate are sent. UpdateUITicksNeededFromUIThread(); } // Request painting so the changes to the UI drawer list are reflected as // quickly as possible. // RequestUIPaintFromUIThread is not enough, because a paint request is also // needed if disabling the UI, to force paint a frame without the UI as soon // as possible - it can't be dropped if ui_drawers_ is empty. // The coarse painting availability (and thus the availability of `window_`, // which is required for the paint mode to be anything else than kNone) has // already been checked above. ForceUIThreadPaintTick(); window_->RequestPaint(); } void Presenter::UpdateUITicksNeededFromUIThread() { #if XE_PLATFORM_WIN32 bool new_needed = AreUITicksNeededFromUIThread(); if (dxgi_ui_ticks_needed_ == new_needed) { return; } bool signal_dxgi_ui_tick_control; { std::unique_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); dxgi_ui_ticks_needed_ = new_needed; signal_dxgi_ui_tick_control = AreDXGIUITicksWaitable(dxgi_ui_tick_lock); } if (signal_dxgi_ui_tick_control) { dxgi_ui_tick_control_condition_.notify_all(); } #endif } void Presenter::WaitForUITickFromUIThread() { #if XE_PLATFORM_WIN32 if (!AreUITicksNeededFromUIThread()) { return; } std::unique_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); uint64_t last_vblank_before_wait = dxgi_ui_tick_last_vblank_; while (true) { // Guest output present requests should interrupt the wait as quickly as // possible as they should be fulfilled as early as possible. if (dxgi_ui_tick_force_requested_) { dxgi_ui_tick_force_requested_ = false; return; } if (!AreDXGIUITicksWaitable(dxgi_ui_tick_lock)) { return; } if (dxgi_ui_tick_last_vblank_ > dxgi_ui_tick_last_draw_) { // If there have been multiple vblanks during the wait for some reason, // next time draw the UI immediately. dxgi_ui_tick_last_draw_ = std::min(last_vblank_before_wait + uint64_t(1), dxgi_ui_tick_last_vblank_); return; } dxgi_ui_tick_signal_condition_.wait(dxgi_ui_tick_lock); } #endif // XE_PLATFORM } void Presenter::ForceUIThreadPaintTick() { #if XE_PLATFORM_WIN32 std::scoped_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); dxgi_ui_tick_force_requested_ = true; #endif // XE_PLATFORM } #if XE_PLATFORM_WIN32 Microsoft::WRL::ComPtr Presenter::GetDXGIOutputForMonitor( IDXGIFactory1* factory, HMONITOR monitor) { Microsoft::WRL::ComPtr adapter; for (UINT adapter_index = 0; SUCCEEDED(factory->EnumAdapters( adapter_index, adapter.ReleaseAndGetAddressOf())); ++adapter_index) { Microsoft::WRL::ComPtr output; for (UINT output_index = 0; SUCCEEDED(adapter->EnumOutputs(output_index, &output)); ++output_index) { DXGI_OUTPUT_DESC output_desc; if (SUCCEEDED(output->GetDesc(&output_desc)) && output_desc.Monitor == monitor) { return std::move(output); } } } return nullptr; } void Presenter::DXGIUITickThread() { std::unique_lock dxgi_ui_tick_lock(dxgi_ui_tick_mutex_); while (true) { if (dxgi_ui_tick_thread_shutdown_) { return; } if (!AreDXGIUITicksWaitable(dxgi_ui_tick_lock)) { dxgi_ui_tick_control_condition_.wait(dxgi_ui_tick_lock); continue; } // Wait for vertical blank, with the mutex unlocked (holding a new reference // to the current output while it's happening) so subscribers can still do // early-out checks. bool wait_succeeded; { Microsoft::WRL::ComPtr dxgi_output = dxgi_ui_tick_output_; dxgi_ui_tick_lock.unlock(); wait_succeeded = SUCCEEDED(dxgi_ui_tick_output_->WaitForVBlank()); } dxgi_ui_tick_lock.lock(); if (wait_succeeded) { ++dxgi_ui_tick_last_vblank_; } else { // Lost the ability to wait for a vertical blank on this output, notify // the waiting threads, and wait for a new one. dxgi_ui_tick_output_.Reset(); } dxgi_ui_tick_signal_condition_.notify_all(); } } #endif // XE_PLATFORM } // namespace ui } // namespace xe