mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 03:57:17 +01:00
The screenshot readback ran in ScreenManager's post-render callback, and on Vulkan it ends the backbuffer render step. renderImDebugger() draws after that, so taking a screenshot with the debugger open made its first SetViewport() write through a null render step (an assert in debug builds, a Play crash report in release). Do the readback at the end of the frame in NativeFrame instead, which also puts the debugger in the screenshot, and remove the now unused callback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
566 lines
17 KiB
C++
566 lines
17 KiB
C++
#include "Common/System/Display.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/Data/Collections/TinySet.h"
|
|
#include "Common/UI/ScreenManager.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/Render/DrawBuffer.h"
|
|
#include "Common/UI/Root.h"
|
|
#include "Common/UI/View.h"
|
|
#include "Common/UI/ScrollView.h"
|
|
#include "Common/GPU/thin3d.h"
|
|
#include "Common/TimeUtil.h"
|
|
|
|
ScreenManager::~ScreenManager() {
|
|
shutdown();
|
|
}
|
|
|
|
void ScreenManager::switchScreen(Screen *screen) {
|
|
if (!screen) {
|
|
ERROR_LOG(Log::UI, "Can't switch to empty screen");
|
|
return;
|
|
}
|
|
|
|
INFO_LOG(Log::UI, "ScreenManager::switchScreen('%s')", screen->tag());
|
|
|
|
if (!nextStack_.empty() && screen == nextStack_.front().screen) {
|
|
ERROR_LOG(Log::UI, "Already switching to this screen");
|
|
return;
|
|
}
|
|
|
|
if (!nextStack_.empty()) {
|
|
for (int i = 0; i < nextStack_.size(); i++) {
|
|
INFO_LOG(Log::UI, "NextStack contents[%d].screen->tag(): '%s'", i, nextStack_[i].screen->tag());
|
|
}
|
|
|
|
ERROR_LOG(Log::UI, "Already had a nextStack_! Asynchronous open while doing something? Deleting the new screen.");
|
|
delete screen;
|
|
return;
|
|
}
|
|
if (screen == nullptr) {
|
|
WARN_LOG(Log::UI, "Switching to a zero screen, this can't be good");
|
|
}
|
|
if (stack_.empty() || screen != stack_.back().screen) {
|
|
if (screen) {
|
|
screen->setScreenManager(this);
|
|
}
|
|
nextStack_.push_back({screen, 0});
|
|
}
|
|
}
|
|
|
|
void ScreenManager::cancelScreensAbove(Screen *screen) {
|
|
bool found = false;
|
|
for (int i = 0; i < stack_.size(); i++) {
|
|
if (stack_[i].screen == screen) {
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
cancelScreensAbove_ = screen;
|
|
}
|
|
}
|
|
|
|
void ScreenManager::ProcessScreenSwitches() {
|
|
if (cancelScreensAbove_) {
|
|
bool found = false;
|
|
for (int i = (int)stack_.size() - 1; i >= 0; i--) {
|
|
if (stack_[i].screen == cancelScreensAbove_) {
|
|
break;
|
|
}
|
|
Layer temp = stack_.back();
|
|
stack_.pop_back();
|
|
delete temp.screen;
|
|
}
|
|
cancelScreensAbove_ = nullptr;
|
|
}
|
|
|
|
if (!nextStack_.empty()) {
|
|
switchToNext();
|
|
}
|
|
}
|
|
|
|
void ScreenManager::ProcessInputEvent(const QueuedEvent &event) {
|
|
switch (event.type) {
|
|
case QueuedEventType::TOUCH:
|
|
{
|
|
const TouchInput &touch = event.touch;
|
|
// Send release all events to every screen layer.
|
|
if (touch.flags & TouchInputFlags::RELEASE_ALL) {
|
|
for (auto &layer : stack_) {
|
|
Screen *screen = layer.screen;
|
|
layer.screen->touch(screen->transformTouch(touch));
|
|
}
|
|
} else if (!stack_.empty()) {
|
|
// Let the overlay know about touch-downs, to be able to dismiss popups.
|
|
bool skip = false;
|
|
if (overlayScreen_ && (touch.flags & TouchInputFlags::DOWN)) {
|
|
skip = overlayScreen_->touch(overlayScreen_->transformTouch(touch));
|
|
}
|
|
if (!skip) {
|
|
Screen *screen = stack_.back().screen;
|
|
stack_.back().screen->touch(screen->transformTouch(touch));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case QueuedEventType::KEY:
|
|
{
|
|
const KeyInput &key = event.key;
|
|
// Send key up to every screen layer, to avoid stuck keys. A press and release in one (iOS's back swipe,
|
|
// the mouse wheel) is a press only for the top one, or every dialog below would act on it too.
|
|
if (key.flags & KeyInputFlags::UP) {
|
|
KeyInput upOnly = key;
|
|
upOnly.flags &= ~KeyInputFlags::DOWN;
|
|
for (size_t i = 0; i < stack_.size(); i++) {
|
|
stack_[i].screen->key(i == stack_.size() - 1 ? key : upOnly);
|
|
}
|
|
} else if (!stack_.empty()) {
|
|
stack_.back().screen->key(key);
|
|
}
|
|
break;
|
|
}
|
|
case QueuedEventType::AXIS:
|
|
{
|
|
const AxisInput &axis = event.axis;
|
|
if (!stack_.empty()) {
|
|
stack_.back().screen->axis(axis);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
ERROR_LOG(Log::UI, "Unknown queued event type: %d", (int)event.type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ScreenManager::Update() {
|
|
if (overlayScreen_) {
|
|
// NOTE: This is not a full UIScreen update, to avoid double global event processing.
|
|
overlayScreen_->update();
|
|
}
|
|
|
|
bool topLevel = false;
|
|
if (!stack_.empty()) {
|
|
Screen *backScreen = stack_.back().screen;
|
|
backScreen->update();
|
|
passInputToMapper_ = backScreen->PassInputToMapper();
|
|
topLevel = backScreen->isTopLevel();
|
|
}
|
|
if (topIsTopLevel_.exchange(topLevel) != topLevel) {
|
|
System_Notify(SystemNotification::TOP_LEVEL_CHANGED);
|
|
}
|
|
}
|
|
|
|
void ScreenManager::switchToNext() {
|
|
if (nextStack_.empty()) {
|
|
ERROR_LOG(Log::System, "switchToNext: No nextStack_!");
|
|
return;
|
|
}
|
|
|
|
// Replaces the whole stack. Most callers switch from a dialog (Play on the game screen, the store,
|
|
// installers), and replacing only the top left the screens below alive, like one more MainScreen
|
|
// under every game launched that way.
|
|
std::vector<Layer> old = std::move(stack_);
|
|
stack_.clear();
|
|
if (!old.empty()) {
|
|
old.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
|
|
}
|
|
stack_.push_back(nextStack_.front());
|
|
nextStack_.front().screen->focusChanged(ScreenFocusChange::FOCUS_BECAME_TOP);
|
|
for (auto iter = old.rbegin(); iter != old.rend(); ++iter) {
|
|
delete iter->screen;
|
|
}
|
|
UI::SetFocusedView(nullptr, UI::FocusFlags::CAUSE_SCREEN_CHANGE);
|
|
|
|
// When will this ever happen? Should handle focus here too?
|
|
for (size_t i = 1; i < nextStack_.size(); ++i) {
|
|
stack_.push_back(nextStack_[i]);
|
|
}
|
|
nextStack_.clear();
|
|
}
|
|
|
|
void ScreenManager::deviceLost() {
|
|
for (auto &iter : stack_)
|
|
iter.screen->deviceLost();
|
|
for (Output &output : outputs_) {
|
|
if (output.display.swapchain) {
|
|
draw_->DestroySwapchain(output.display.swapchain);
|
|
output.display.swapchain = nullptr;
|
|
}
|
|
}
|
|
// Outputs added before the device is restored get their swapchains then.
|
|
deviceLost_ = true;
|
|
}
|
|
|
|
void ScreenManager::deviceRestored(Draw::DrawContext *draw) {
|
|
draw_ = draw;
|
|
deviceLost_ = false;
|
|
for (auto &iter : stack_)
|
|
iter.screen->deviceRestored(draw);
|
|
for (Output &output : outputs_) {
|
|
std::string error;
|
|
output.display.swapchain = draw_->CreateSwapchain(output.desc, &error);
|
|
if (!output.display.swapchain) {
|
|
ERROR_LOG(Log::G3D, "Failed to recreate the swapchain for output %d: %s", output.display.id, error.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScreenManager::AddOutput(int id, OutputRole role, const Draw::SwapchainDesc &desc) {
|
|
Output output{};
|
|
output.display.id = id;
|
|
output.display.role = role;
|
|
output.desc = desc;
|
|
if (draw_ && !deviceLost_) {
|
|
std::string error;
|
|
output.display.swapchain = draw_->CreateSwapchain(desc, &error);
|
|
if (!output.display.swapchain) {
|
|
ERROR_LOG(Log::G3D, "Failed to create the swapchain for output %d: %s", id, error.c_str());
|
|
}
|
|
}
|
|
INFO_LOG(Log::G3D, "Added output %d (%dx%d)", id, desc.width, desc.height);
|
|
outputs_.push_back(output);
|
|
}
|
|
|
|
void ScreenManager::ResizeOutput(int id, int width, int height) {
|
|
for (Output &output : outputs_) {
|
|
if (output.display.id == id) {
|
|
output.desc.width = width;
|
|
output.desc.height = height;
|
|
if (output.display.swapchain) {
|
|
draw_->ResizeSwapchain(output.display.swapchain, width, height);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScreenManager::RemoveOutput(int id) {
|
|
for (auto iter = outputs_.begin(); iter != outputs_.end(); ++iter) {
|
|
if (iter->display.id == id) {
|
|
if (iter->display.swapchain) {
|
|
draw_->DestroySwapchain(iter->display.swapchain);
|
|
}
|
|
outputs_.erase(iter);
|
|
INFO_LOG(Log::G3D, "Removed output %d", id);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const OutputDisplay *ScreenManager::FindOutput(OutputRole role) const {
|
|
for (const Output &output : outputs_) {
|
|
if (output.display.role == role && output.display.swapchain) {
|
|
return &output.display;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void ScreenManager::resized() {
|
|
INFO_LOG(Log::UI, "ScreenManager::resized(g_display.dp_xres/dp_yres: %dx%d)", g_display.dp_xres, g_display.dp_yres);
|
|
// Have to notify the whole stack, otherwise there will be problems when going back
|
|
// to non-top screens.
|
|
for (auto &layer : stack_) {
|
|
layer.screen->resized();
|
|
}
|
|
}
|
|
|
|
ScreenRenderFlags ScreenManager::Render(std::function<void()> afterPreRender) {
|
|
using namespace Draw;
|
|
|
|
ScreenRenderFlags flags = ScreenRenderFlags::NONE;
|
|
|
|
// First, go through the whole stack and have every screen render any non-backbuffer render passes.
|
|
// In EmuScreen, this might result in running emulation.
|
|
for (size_t i = 0; i < stack_.size(); i++) {
|
|
const auto &layer = stack_[i];
|
|
ScreenRenderMode mode = ScreenRenderMode::DEFAULT;
|
|
if (i == stack_.size() - 1) {
|
|
mode |= ScreenRenderMode::TOP;
|
|
}
|
|
flags |= layer.screen->PreRender(mode);
|
|
}
|
|
|
|
afterPreRender();
|
|
|
|
// Secondary outputs. The topmost screen with something for one draws it, then the background screen.
|
|
UIContext *ui = getUIContext();
|
|
for (const Output &output : outputs_) {
|
|
Draw::Swapchain *swapchain = output.display.swapchain;
|
|
if (!swapchain) {
|
|
continue;
|
|
}
|
|
draw_->BindFramebufferAsRenderTarget(swapchain, {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "Output");
|
|
int w, h;
|
|
draw_->GetFramebufferDimensions(swapchain, &w, &h);
|
|
// Only with a render target bound, since it sets the scissor. That's to the primary's size, so ours goes after.
|
|
ui->BeginFrame();
|
|
draw_->SetViewport(Draw::Viewport{0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f});
|
|
draw_->SetScissorRect(0, 0, w, h);
|
|
|
|
// UI drawn there gets the primary's DPI scale, over the whole output.
|
|
const float dpWidth = w * g_display.dpi_scale_x;
|
|
const float dpHeight = h * g_display.dpi_scale_y;
|
|
const Bounds primaryBounds = ui->GetBounds();
|
|
ui->SetBounds(Bounds(0, 0, dpWidth, dpHeight));
|
|
ui->Draw()->PushDrawMatrix(ComputeOrthoMatrix(dpWidth, dpHeight, draw_->GetDeviceCaps().coordConvention, false));
|
|
bool drawn = false;
|
|
for (auto iter = stack_.rbegin(); iter != stack_.rend(); ++iter) {
|
|
if (iter->screen->renderOutput(output.display)) {
|
|
drawn = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!drawn && backgroundScreen_) {
|
|
backgroundScreen_->renderOutput(output.display);
|
|
}
|
|
ui->Flush();
|
|
ui->Draw()->PopDrawMatrix();
|
|
ui->SetBounds(primaryBounds);
|
|
}
|
|
|
|
// Now, start the final render pass. This is now the ONLY place where binding the backbuffer is allowed.
|
|
draw_->BindFramebufferAsRenderTarget(draw_->GetPrimarySwapchain(), {RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR}, "BackBuffer");
|
|
getUIContext()->BeginFrame();
|
|
|
|
const Draw::Viewport viewport{0.0f, 0.0f, (float)g_display.pixel_xres, (float)g_display.pixel_yres, 0.0f, 1.0f};
|
|
draw_->SetViewport(viewport);
|
|
draw_->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres);
|
|
draw_->SetTargetSize(g_display.pixel_xres, g_display.pixel_yres);
|
|
|
|
if (!stack_.empty()) {
|
|
// Collect the screens to render
|
|
TinySet<Screen *, 6> layers;
|
|
|
|
// Start at the end, collect screens to form the transparency stack.
|
|
// Then we'll iterate them in reverse order.
|
|
// Note that we skip the overlay screen, we handle it separately.
|
|
// Additionally, we pick up a "background" screen. Normally it will be either
|
|
// the EmuScreen or the actual global background screen.
|
|
auto iter = stack_.end();
|
|
Screen *coveringScreen = nullptr;
|
|
Screen *foundBackgroundScreen = nullptr;
|
|
bool first = true;
|
|
|
|
do {
|
|
--iter;
|
|
ScreenRenderRole role = iter->screen->renderRole(first);
|
|
if (!foundBackgroundScreen && (role & ScreenRenderRole::CAN_BE_BACKGROUND)) {
|
|
// There still might be a screen that wants to be background - generally the EmuScreen if present.
|
|
layers.push_back(iter->screen);
|
|
foundBackgroundScreen = iter->screen;
|
|
} else if (!coveringScreen) {
|
|
layers.push_back(iter->screen);
|
|
}
|
|
if (iter->flags != LAYER_TRANSPARENT) {
|
|
coveringScreen = iter->screen;
|
|
}
|
|
first = false;
|
|
if (role & ScreenRenderRole::MUST_BE_FIRST) {
|
|
break;
|
|
}
|
|
} while (iter != stack_.begin());
|
|
|
|
if (backgroundScreen_ && !foundBackgroundScreen) {
|
|
layers.push_back(backgroundScreen_);
|
|
foundBackgroundScreen = backgroundScreen_;
|
|
}
|
|
|
|
// OK, now we iterate backwards over our little pile of collected screens.
|
|
for (int i = (int)layers.size() - 1; i >= 0; i--) {
|
|
ScreenRenderMode mode = ScreenRenderMode::DEFAULT;
|
|
if (i == (int)layers.size() - 1) {
|
|
// Bottom.
|
|
mode = ScreenRenderMode::FIRST;
|
|
if (i == 0) {
|
|
mode |= ScreenRenderMode::TOP;
|
|
}
|
|
} else if (i == 0) {
|
|
mode = ScreenRenderMode::TOP;
|
|
} else {
|
|
mode = ScreenRenderMode::BEHIND;
|
|
}
|
|
flags |= layers[i]->render(mode);
|
|
}
|
|
|
|
if (overlayScreen_) {
|
|
// It doesn't care about mode.
|
|
flags |= overlayScreen_->render(ScreenRenderMode::TOP);
|
|
}
|
|
|
|
getUIContext()->Flush();
|
|
} else {
|
|
ERROR_LOG(Log::UI, "No current screen!");
|
|
}
|
|
|
|
processFinishDialog();
|
|
return flags;
|
|
}
|
|
|
|
void ScreenManager::getFocusPosition(float &x, float &y, float &z) {
|
|
UI::ScrollView::GetLastScrollPosition(x, y);
|
|
|
|
UI::View *v = UI::GetFocusedView();
|
|
x += v ? v->GetBounds().x : 0;
|
|
y += v ? v->GetBounds().y : 0;
|
|
z = stack_.size();
|
|
}
|
|
|
|
void ScreenManager::sendMessage(UIMessage message, const char *value) {
|
|
if (message == UIMessage::RECREATE_VIEWS) {
|
|
RecreateAllViews();
|
|
}
|
|
if (backgroundScreen_) {
|
|
backgroundScreen_->sendMessage(message, value);
|
|
}
|
|
|
|
// NOTE: Changed this to send the message to all screens, instead of just the top one,
|
|
// to allow EmuScreen to receive messages from popup menus. Hope this didn't break anything..
|
|
// NOTE: We do not use the iterator solution here, because the screen array may change by sendMessage!
|
|
// Although if it does, that's very bad.
|
|
// We also pick out the stack size before, so we don't get infinite growth if a screen is pushed from the handler.
|
|
// We probably should delay pushes, though...
|
|
const size_t stack_size = stack_.size();
|
|
for (int i = 0; i < stack_size; i++) {
|
|
if (stack_[i].screen) {
|
|
stack_[i].screen->sendMessage(message, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScreenManager::shutdown() {
|
|
for (const auto &layer : stack_)
|
|
delete layer.screen;
|
|
stack_.clear();
|
|
for (const auto &layer : nextStack_)
|
|
delete layer.screen;
|
|
nextStack_.clear();
|
|
delete overlayScreen_;
|
|
overlayScreen_ = nullptr;
|
|
delete backgroundScreen_;
|
|
backgroundScreen_ = nullptr;
|
|
}
|
|
|
|
void ScreenManager::push(Screen *screen, int layerFlags) {
|
|
screen->setScreenManager(this);
|
|
if (screen->isTransparent()) {
|
|
layerFlags |= LAYER_TRANSPARENT;
|
|
}
|
|
|
|
// Release touches and unfocus.
|
|
UI::SetFocusedView(nullptr, UI::FocusFlags::CAUSE_SCREEN_CHANGE);
|
|
|
|
Layer layer = {screen, layerFlags};
|
|
|
|
if (!stack_.empty()) {
|
|
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
|
|
}
|
|
|
|
if (nextStack_.empty()) {
|
|
layer.screen->focusChanged(ScreenFocusChange::FOCUS_BECAME_TOP);
|
|
stack_.push_back(layer);
|
|
} else {
|
|
nextStack_.push_back(layer);
|
|
}
|
|
}
|
|
|
|
void ScreenManager::pop() {
|
|
if (!stack_.empty()) {
|
|
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
|
|
|
|
delete stack_.back().screen;
|
|
stack_.pop_back();
|
|
|
|
if (!stack_.empty()) {
|
|
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
|
|
}
|
|
} else {
|
|
ERROR_LOG(Log::UI, "Can't pop when stack empty");
|
|
}
|
|
}
|
|
|
|
void ScreenManager::RecreateAllViews() {
|
|
for (auto &layer : stack_) {
|
|
layer.screen->RecreateViews();
|
|
}
|
|
}
|
|
|
|
void ScreenManager::finishDialog(Screen *dialog, DialogResult result) {
|
|
if (stack_.empty()) {
|
|
ERROR_LOG(Log::UI, "Must be in a dialog to finishDialog");
|
|
return;
|
|
}
|
|
if (dialog != stack_.back().screen) {
|
|
ERROR_LOG(Log::UI, "Wrong dialog being finished!");
|
|
return;
|
|
}
|
|
dialog->onFinish(result);
|
|
dialogFinished_ = dialog;
|
|
dialogResult_ = result;
|
|
}
|
|
|
|
bool ScreenManager::HasScreenAbove(const Screen *screen) const {
|
|
for (size_t i = 0; i + 1 < stack_.size(); ++i) {
|
|
if (stack_[i].screen == screen) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Screen *ScreenManager::dialogParent(const Screen *dialog) const {
|
|
for (size_t i = 1; i < stack_.size(); ++i) {
|
|
if (stack_[i].screen == dialog) {
|
|
// The previous screen was the caller (not necessarily the topmost.)
|
|
return stack_[i - 1].screen;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void ScreenManager::processFinishDialog() {
|
|
// The caller's dialogFinished() can finish the caller too, which sets dialogFinished_ again. So take it
|
|
// out first (or we'd delete the caller while it's still in the stack) and loop to process the new one.
|
|
while (dialogFinished_) {
|
|
const Screen *finished = dialogFinished_;
|
|
const DialogResult result = dialogResult_;
|
|
dialogFinished_ = nullptr;
|
|
|
|
// Another dialog may have been pushed before the render, so search for it.
|
|
Screen *caller = dialogParent(finished);
|
|
bool erased = false;
|
|
for (size_t i = 0; i < stack_.size(); ++i) {
|
|
if (stack_[i].screen == finished) {
|
|
stack_[i].screen->focusChanged(ScreenFocusChange::FOCUS_LOST_TOP);
|
|
stack_.erase(stack_.begin() + i);
|
|
erased = true;
|
|
}
|
|
}
|
|
|
|
if (erased && !stack_.empty()) {
|
|
stack_.back().screen->focusChanged(ScreenFocusChange::FOCUS_BECAME_TOP);
|
|
}
|
|
|
|
if (!caller) {
|
|
ERROR_LOG(Log::UI, "ERROR: no top screen when finishing dialog");
|
|
} else if (caller != topScreen()) {
|
|
// The caller may get confused if we call dialogFinished() now.
|
|
WARN_LOG(Log::UI, "Skipping non-top dialog when finishing dialog.");
|
|
} else {
|
|
caller->dialogFinished(finished, result);
|
|
}
|
|
delete finished;
|
|
}
|
|
}
|
|
|
|
void ScreenManager::SetBackgroundOverlayScreens(Screen *backgroundScreen, Screen *overlayScreen) {
|
|
delete backgroundScreen_;
|
|
backgroundScreen_ = backgroundScreen;
|
|
backgroundScreen_->setScreenManager(this);
|
|
|
|
delete overlayScreen_;
|
|
overlayScreen_ = overlayScreen;
|
|
overlayScreen_->setScreenManager(this);
|
|
}
|