Better screen layout support

This commit is contained in:
wheremyfoodat 2025-07-06 02:41:44 +03:00
parent 1c0f65c740
commit cf321b1ed8
17 changed files with 328 additions and 186 deletions

View file

@ -93,6 +93,10 @@ void EmulatorConfig::load() {
lightShadergenThreshold = toml::find_or<toml::integer>(gpu, "ShadergenLightThreshold", 1);
hashTextures = toml::find_or<toml::boolean>(gpu, "HashTextures", hashTexturesDefault);
enableRenderdoc = toml::find_or<toml::boolean>(gpu, "EnableRenderdoc", false);
auto screenLayoutName = toml::find_or<std::string>(gpu, "ScreenLayout", "Default");
screenLayout = ScreenLayout::layoutFromString(screenLayoutName);
topScreenSize = float(std::clamp(toml::find_or<toml::floating>(gpu, "TopScreenSize", 0.5), 0.0, 1.0));
}
}
@ -194,6 +198,8 @@ void EmulatorConfig::save() {
data["GPU"]["AccelerateShaders"] = accelerateShaders;
data["GPU"]["EnableRenderdoc"] = enableRenderdoc;
data["GPU"]["HashTextures"] = hashTextures;
data["GPU"]["ScreenLayout"] = std::string(ScreenLayout::layoutToString(screenLayout));
data["GPU"]["TopScreenSize"] = double(topScreenSize);
data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType));
data["Audio"]["EnableAudio"] = audioEnabled;

View file

@ -11,7 +11,7 @@
#include "PICA/pica_hash.hpp"
#include "PICA/pica_simd.hpp"
#include "PICA/regs.hpp"
#include "PICA/screen_layout.hpp"
#include "screen_layout.hpp"
#include "PICA/shader_decompiler.hpp"
#include "config.hpp"
#include "math_util.hpp"
@ -570,15 +570,18 @@ void RendererGL::display() {
if constexpr (!Helpers::isHydraCore()) {
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
OpenGL::clearColor();
screenFramebuffer.bind(OpenGL::ReadFramebuffer);
constexpr auto layout = ScreenLayout::Layout::Default;
if (outputSizeChanged) {
outputSizeChanged = false;
auto layout = emulatorConfig->screenLayout;
// Get information about our new screen layout to use for blitting the output
ScreenLayout::WindowCoordinates windowCoords;
ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, layout);
ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, emulatorConfig->topScreenSize, layout);
blitInfo.topScreenX = windowCoords.topScreenX;
blitInfo.topScreenY = windowCoords.topScreenY;
@ -590,20 +593,16 @@ void RendererGL::display() {
blitInfo.bottomScreenWidth = windowCoords.bottomScreenWidth;
blitInfo.bottomScreenHeight = windowCoords.bottomScreenHeight;
// Flip topScreenY and bottomScreenY because glBlitFramebuffer uses bottom-left origin
// Flip topScreenY and bottomScreenY because glBlitFramebuffer uses bottom-left origin
blitInfo.topScreenY = outputWindowHeight - (blitInfo.topScreenY + blitInfo.topScreenHeight);
blitInfo.bottomScreenY = outputWindowHeight - (blitInfo.bottomScreenY + blitInfo.bottomScreenHeight);
// Used for optimizing the screen blit into a single blit
blitInfo.canDoSingleBlit = windowCoords.singleBlitInfo.canDoSingleBlit;
blitInfo.destX = windowCoords.singleBlitInfo.destX;
blitInfo.destY = windowCoords.singleBlitInfo.destY;
blitInfo.destWidth = windowCoords.singleBlitInfo.destWidth;
blitInfo.destHeight = windowCoords.singleBlitInfo.destHeight;
// Check if we can blit the screens in 1 blit. If not, we'll break it into two.
// TODO: Maybe add some size-related checks too.
blitInfo.canDoSingleBlit =
windowCoords.topScreenY + windowCoords.topScreenHeight == windowCoords.bottomScreenY && layout == ScreenLayout::Layout::Default;
}
if (blitInfo.canDoSingleBlit) {

View file

@ -10,7 +10,7 @@
#include "PICA/gpu.hpp"
#include "PICA/pica_hash.hpp"
#include "PICA/screen_layout.hpp"
#include "screen_layout.hpp"
#include "SDL_metal.h"
using namespace PICA;
@ -107,7 +107,9 @@ void RendererMTL::display() {
if (outputSizeChanged) {
outputSizeChanged = false;
ScreenLayout::WindowCoordinates windowCoords;
ScreenLayout::calculateCoordinates(windowCoords, outputWindowWidth, outputWindowHeight, ScreenLayout::Layout::Default);
ScreenLayout::calculateCoordinates(
windowCoords, outputWindowWidth, outputWindowHeight, emulatorConfig->topScreenSize, emulatorConfig->screenLayout
);
blitInfo.topScreenX = float(windowCoords.topScreenX);
blitInfo.topScreenY = float(windowCoords.topScreenY);

155
src/core/screen_layout.cpp Normal file
View file

@ -0,0 +1,155 @@
#include "screen_layout.hpp"
#include <algorithm>
#include <cstdio>
#include <unordered_map>
using namespace ScreenLayout;
// Calculate screen coordinates on the screen for a given layout & a given size for the output window
// Used in both the renderers and in the frontends (To eg calculate touch screen boundaries)
void ScreenLayout::calculateCoordinates(
WindowCoordinates& coordinates, u32 outputWindowWidth, u32 outputWindowHeight, float topScreenPercentage, Layout layout
) {
const float destAspect = float(outputWindowWidth) / float(outputWindowHeight);
if (layout == Layout::Default || layout == Layout::DefaultFlipped) {
// Calculate available height for each screen based on split
int availableTopHeight = int(outputWindowHeight * topScreenPercentage + 0.5f);
int availableBottomHeight = outputWindowHeight - availableTopHeight;
// Calculate scales for top and bottom screens, and then the actual sizes
float scaleTopX = float(outputWindowWidth) / float(TOP_SCREEN_WIDTH);
float scaleTopY = float(availableTopHeight) / float(TOP_SCREEN_HEIGHT);
float scaleTop = std::min(scaleTopX, scaleTopY);
float scaleBottomX = float(outputWindowWidth) / float(BOTTOM_SCREEN_WIDTH);
float scaleBottomY = float(availableBottomHeight) / float(BOTTOM_SCREEN_HEIGHT);
float scaleBottom = std::min(scaleBottomX, scaleBottomY);
int topScreenWidth = int(TOP_SCREEN_WIDTH * scaleTop + 0.5f);
int topScreenHeight = int(TOP_SCREEN_HEIGHT * scaleTop + 0.5f);
int bottomScreenWidth = int(BOTTOM_SCREEN_WIDTH * scaleBottom + 0.5f);
int bottomScreenHeight = int(BOTTOM_SCREEN_HEIGHT * scaleBottom + 0.5f);
// Center screens horizontally
int topScreenX = (outputWindowWidth - topScreenWidth) / 2;
int bottomScreenX = (outputWindowWidth - bottomScreenWidth) / 2;
coordinates.topScreenWidth = topScreenWidth;
coordinates.topScreenHeight = topScreenHeight;
coordinates.bottomScreenWidth = bottomScreenWidth;
coordinates.bottomScreenHeight = bottomScreenHeight;
coordinates.topScreenX = topScreenX;
coordinates.bottomScreenX = bottomScreenX;
if (layout == Layout::Default) {
coordinates.topScreenY = 0;
coordinates.bottomScreenY = topScreenHeight;
} else {
coordinates.bottomScreenY = 0;
coordinates.topScreenY = bottomScreenHeight;
}
coordinates.windowWidth = outputWindowWidth;
coordinates.windowHeight = outputWindowHeight;
// Default layout can be rendered using a single blit, flipped layout can't
if (layout == Layout::Default) {
coordinates.singleBlitInfo.destX = coordinates.topScreenX;
coordinates.singleBlitInfo.destY = coordinates.topScreenY;
coordinates.singleBlitInfo.destWidth = coordinates.topScreenWidth;
coordinates.singleBlitInfo.destHeight = coordinates.topScreenHeight + coordinates.bottomScreenHeight;
} else {
// Dummy data for the single blit info, as we can't render the screen in 1 blit when the screens are side-by-sid
coordinates.singleBlitInfo.destX = 0;
coordinates.singleBlitInfo.destY = 0;
coordinates.singleBlitInfo.destWidth = 0;
coordinates.singleBlitInfo.destHeight = 0;
}
// Check if we can blit the screens in 1 blit. If not, we'll break it into two.
// TODO: Maybe add some more size-related checks too.
coordinates.singleBlitInfo.canDoSingleBlit = layout == Layout::Default && topScreenPercentage == 0.5 &&
coordinates.topScreenY + coordinates.topScreenHeight == coordinates.bottomScreenY;
} else if (layout == Layout::SideBySide || layout == Layout::SideBySideFlipped) {
// Calculate available width for each screen based on split
int availableTopWidth = int(outputWindowWidth * topScreenPercentage + 0.5f);
int availableBottomWidth = outputWindowWidth - availableTopWidth;
// Calculate scales for top and bottom screens, and then the actual sizes
float scaleTop = std::min(float(availableTopWidth) / float(TOP_SCREEN_WIDTH), float(outputWindowHeight) / float(TOP_SCREEN_HEIGHT));
float scaleBottom =
std::min(float(availableBottomWidth) / float(BOTTOM_SCREEN_WIDTH), float(outputWindowHeight) / float(BOTTOM_SCREEN_HEIGHT));
int topScreenWidth = int(TOP_SCREEN_WIDTH * scaleTop + 0.5f);
int topScreenHeight = int(TOP_SCREEN_HEIGHT * scaleTop + 0.5f);
int bottomScreenWidth = int(BOTTOM_SCREEN_WIDTH * scaleBottom + 0.5f);
int bottomScreenHeight = int(BOTTOM_SCREEN_HEIGHT * scaleBottom + 0.5f);
// Vertically center the tallest screen
int maxHeight = std::max(topScreenHeight, bottomScreenHeight);
int baseY = (outputWindowHeight - maxHeight) / 2;
int topScreenY = baseY + (maxHeight - topScreenHeight) / 2;
int bottomScreenY = baseY + (maxHeight - bottomScreenHeight) / 2;
if (layout == Layout::SideBySide) {
coordinates.topScreenX = (outputWindowWidth - (topScreenWidth + bottomScreenWidth)) / 2;
coordinates.bottomScreenX = coordinates.topScreenX + topScreenWidth;
} else {
coordinates.bottomScreenX = (outputWindowWidth - (topScreenWidth + bottomScreenWidth)) / 2;
coordinates.topScreenX = coordinates.bottomScreenX + bottomScreenWidth;
}
coordinates.topScreenY = topScreenY;
coordinates.topScreenWidth = topScreenWidth;
coordinates.topScreenHeight = topScreenHeight;
coordinates.bottomScreenY = bottomScreenY;
coordinates.bottomScreenWidth = bottomScreenWidth;
coordinates.bottomScreenHeight = bottomScreenHeight;
coordinates.windowWidth = outputWindowWidth;
coordinates.windowHeight = outputWindowHeight;
// Dummy data for the single blit info, as we can't render the screen in 1 blit when the screens are side-by-side
coordinates.singleBlitInfo.canDoSingleBlit = false;
coordinates.singleBlitInfo.destX = 0;
coordinates.singleBlitInfo.destY = 0;
coordinates.singleBlitInfo.destWidth = 0;
coordinates.singleBlitInfo.destHeight = 0;
} else {
Helpers::panic("Unimplemented screen layout");
}
}
Layout ScreenLayout::layoutFromString(std::string inString) {
// Transform to lower-case to make the setting case-insensitive
std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); });
static const std::unordered_map<std::string, Layout> map = {
{"default", Layout::Default},
{"defaultflipped", Layout::DefaultFlipped},
{"sidebyside", Layout::SideBySide},
{"sidebysideflipped", Layout::SideBySideFlipped},
};
if (auto search = map.find(inString); search != map.end()) {
return search->second;
}
printf("Invalid screen layout. Defaulting to Default\n");
return Layout::Default;
}
const char* ScreenLayout::layoutToString(Layout layout) {
switch (layout) {
case Layout::Default: return "default";
case Layout::DefaultFlipped: return "defaultFlipped";
case Layout::SideBySide: return "sideBySide";
case Layout::SideBySideFlipped: return "sideBySideFlipped";
default: return "invalid";
}
}

View file

@ -240,6 +240,36 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
connectCheckbox(hashTextures, config.hashTextures);
gpuLayout->addRow(hashTextures);
QComboBox* screenLayout = new QComboBox();
screenLayout->addItem(tr("Default"));
screenLayout->addItem(tr("Default (Flipped)"));
screenLayout->addItem(tr("Side-by-Side"));
screenLayout->addItem(tr("Side-by-Side (Flipped)"));
screenLayout->setCurrentIndex(static_cast<int>(config.screenLayout));
connect(screenLayout, &QComboBox::currentIndexChanged, this, [&](int index) {
config.screenLayout = static_cast<ScreenLayout::Layout>(index);
updateConfig();
});
gpuLayout->addRow(tr("Screen Layout"), screenLayout);
// Screen size slider widgets
QLabel* topScreenSizeLabel = new QLabel(QString::number(int(config.topScreenSize * 100)));
QSlider* topScreenSizeSlider = new QSlider(Qt::Horizontal);
topScreenSizeSlider->setRange(0, 100);
topScreenSizeSlider->setValue(int(config.topScreenSize * 100));
connect(topScreenSizeSlider, &QSlider::valueChanged, this, [this, topScreenSizeLabel](int value) {
config.topScreenSize = float(value) / 100.0f;
topScreenSizeLabel->setText(QString::number(value));
updateConfig();
});
QHBoxLayout* screenSizeLayout = new QHBoxLayout();
screenSizeLayout->setSpacing(4);
screenSizeLayout->addWidget(topScreenSizeSlider);
screenSizeLayout->addWidget(topScreenSizeLabel);
gpuLayout->addRow(tr("Top screen size (%)"), screenSizeLayout);
QCheckBox* forceShadergenForLights = new QCheckBox(tr("Force shadergen when rendering lights"));
connectCheckbox(forceShadergenForLights, config.forceShadergenForLights);
gpuLayout->addRow(forceShadergenForLights);
@ -302,7 +332,7 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
volumeSlider->setRange(0, 200);
volumeSlider->setValue(int(config.audioDeviceConfig.volumeRaw * 100));
connect(volumeSlider, &QSlider::valueChanged, this, [this, volumeLabel](int value) {
config.audioDeviceConfig.volumeRaw = static_cast<float>(value) / 100.0f;
config.audioDeviceConfig.volumeRaw = float(value) / 100.0f;
volumeLabel->setText(QString::number(value));
updateConfig();
@ -467,12 +497,14 @@ void ConfigWindow::setTheme(Theme theme) {
p.setColor(QPalette::Highlight, QColor(42, 130, 218));
p.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(p);
qApp->setStyleSheet("QLineEdit {"
qApp->setStyleSheet(
"QLineEdit {"
"background-color: #000000; color: #ffffff; border: 1px solid #a0a0a0; "
"border-radius: 4px; padding: 5px; }"
"QCheckBox::indicator:unchecked {"
"border: 1px solid #808080; border-radius: 4px; }");
"border: 1px solid #808080; border-radius: 4px; }"
);
break;
}

View file

@ -129,6 +129,8 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
if (config.printAppVersion) {
printf("Welcome to Panda3DS v%s!\n", PANDA3DS_VERSION);
}
screen->reloadScreenLayout(config.screenLayout, config.topScreenSize);
}
// The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work
@ -435,13 +437,24 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) {
break;
}
case MessageType::UpdateConfig:
emu->getConfig() = configWindow->getConfig();
case MessageType::UpdateConfig: {
auto& emuConfig = emu->getConfig();
auto& newConfig = configWindow->getConfig();
// If the screen layout changed, we have to notify the emulator & the screen widget
bool reloadScreenLayout = (emuConfig.screenLayout != newConfig.screenLayout || emuConfig.topScreenSize != newConfig.topScreenSize);
emuConfig = newConfig;
emu->reloadSettings();
if (reloadScreenLayout) {
emu->reloadScreenLayout();
screen->reloadScreenLayout(newConfig.screenLayout, newConfig.topScreenSize);
}
// Save new settings to disk
emu->getConfig().save();
emuConfig.save();
break;
}
}
}

View file

@ -47,7 +47,7 @@ void ScreenWidget::resizeEvent(QResizeEvent* event) {
this->windowInfo = *windowInfo;
}
ScreenLayout::calculateCoordinates(screenCoordinates, u32(width()), u32(height()), ScreenLayout::Layout::Default);
reloadScreenCoordinates();
// This will call take care of calling resizeSurface from the emulator thread
resizeCallback(surfaceWidth, surfaceHeight);
@ -62,6 +62,17 @@ void ScreenWidget::resizeSurface(u32 width, u32 height) {
}
}
void ScreenWidget::reloadScreenCoordinates() {
ScreenLayout::calculateCoordinates(screenCoordinates, u32(width()), u32(height()), topScreenSize, screenLayout);
}
void ScreenWidget::reloadScreenLayout(ScreenLayout::Layout newLayout, float newTopScreenSize) {
screenLayout = newLayout;
topScreenSize = newTopScreenSize;
reloadScreenCoordinates();
}
bool ScreenWidget::createGLContext() {
// List of GL context versions we will try. Anything 4.1+ is good for desktop OpenGL, and 3.1+ for OpenGL ES
static constexpr std::array<GL::Context::Version, 8> versionsToTry = {

View file

@ -55,7 +55,12 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp
windowWidth = 400;
windowHeight = 480;
}
// Initialize output size and screen layout
emu.setOutputSize(windowWidth, windowHeight);
ScreenLayout::calculateCoordinates(
screenCoordinates, u32(windowWidth), u32(windowHeight), emu.getConfig().topScreenSize, emu.getConfig().screenLayout
);
if (needOpenGL) {
// Demand 4.1 core for OpenGL renderer (max available on MacOS), 3.3 for the software & null renderers
@ -365,7 +370,11 @@ void FrontendSDL::run() {
if (type == SDL_WINDOWEVENT_RESIZED) {
windowWidth = event.window.data1;
windowHeight = event.window.data2;
ScreenLayout::calculateCoordinates(screenCoordinates, u32(windowWidth), u32(windowHeight), ScreenLayout::Layout::Default);
const auto& config = emu.getConfig();
ScreenLayout::calculateCoordinates(
screenCoordinates, u32(windowWidth), u32(windowHeight), emu.getConfig().topScreenSize, emu.getConfig().screenLayout
);
emu.setOutputSize(windowWidth, windowHeight);
}