1447 lines
61 KiB
C++
1447 lines
61 KiB
C++
/**
|
|
******************************************************************************
|
|
* 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 <algorithm>
|
|
#include <utility>
|
|
|
|
#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<std::mutex> 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<bool(GuestOutputRefreshContext& context)> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<uint32_t, uint32_t> 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<uint32_t, uint32_t> 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<uint32_t, uint32_t>* 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<std::mutex> 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<const Win32Window*>(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<IDXGIOutput> old_factory_output_to_release;
|
|
{
|
|
std::scoped_lock<std::mutex> 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<IDXGIOutput> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> dxgi_ui_tick_lock(dxgi_ui_tick_mutex_);
|
|
dxgi_ui_tick_force_requested_ = true;
|
|
#endif // XE_PLATFORM
|
|
}
|
|
|
|
#if XE_PLATFORM_WIN32
|
|
Microsoft::WRL::ComPtr<IDXGIOutput> Presenter::GetDXGIOutputForMonitor(
|
|
IDXGIFactory1* factory, HMONITOR monitor) {
|
|
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
|
|
for (UINT adapter_index = 0; SUCCEEDED(factory->EnumAdapters(
|
|
adapter_index, adapter.ReleaseAndGetAddressOf()));
|
|
++adapter_index) {
|
|
Microsoft::WRL::ComPtr<IDXGIOutput> 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<std::mutex> 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<IDXGIOutput> 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
|