mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-11 08:39:28 +01:00
Add overlay (bezel) support
Draws a user image around or behind the PSP screen, to fill the black bars on displays whose aspect ratio doesn't match the PSP's (#20325). - Overlays are PNGs in PSP/OVERLAYS. A plain PNG there just works: it's drawn in front of the game, and its transparent hole is found by walking from the image center to each edge until a nearly opaque pixel. Existing RetroArch overlay images can be dropped in as-is. - A subfolder with an overlay.ini can define overlays with a Name, Layer (Foreground/Background) and an explicit Hole=x,y,w,h. - "Load overlay image..." in the display layout screen imports any PNG (including Android content URIs) into the overlays folder and selects it. - The overlay is fit to the screen (aspect preserved, centered). The hole is mapped with the same scale, and the game is fit into it with the normal stretch / integer scale / aspect ratio options. Offset and scale don't apply while an overlay with a hole is active. This goes through CalculateDisplayOutputRect, so touch mapping and post shaders agree. - The selection is a per-orientation, per-game DisplayLayout setting. - Disabled in VR, Cardboard, and with skip buffer effects (except software rendering), since the presentation path doesn't run there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QGBg32EFPsTXjAFu7dnTcz
This commit is contained in:
1 parent
ff3d184b43
commit
3ce3973781
18 files changed
+697
-6
No files matched your search
@@ -685,6 +685,7 @@ static const ConfigSetting displayLayoutSettings[] = {
|
||||
ConfigSetting("DisplayIntegerScale", SETTING(g_Config.displayLayoutLandscape, bDisplayIntegerScale), CfgFlag::PER_GAME),
|
||||
ConfigSetting("DisplayAspectRatio", SETTING(g_Config.displayLayoutLandscape, fDisplayAspectRatio), CfgFlag::PER_GAME),
|
||||
ConfigSetting("IgnoreScreenInsets", SETTING(g_Config.displayLayoutLandscape, bIgnoreScreenInsets), CfgFlag::PER_GAME),
|
||||
ConfigSetting("Overlay", SETTING(g_Config.displayLayoutLandscape, sOverlay), "", CfgFlag::PER_GAME),
|
||||
ConfigSetting("InternalScreenRotation", SETTING(g_Config.displayLayoutLandscape, iInternalScreenRotation), CfgFlag::PER_GAME),
|
||||
ConfigSetting("RotateControlsWithScreen", SETTING(g_Config.displayLayoutLandscape, bRotateControlsWithScreen), CfgFlag::PER_GAME),
|
||||
ConfigSetting("EnableCardboardVR", SETTING(g_Config.displayLayoutLandscape, bEnableCardboardVR), CfgFlag::PER_GAME),
|
||||
|
||||
@@ -85,6 +85,7 @@ struct DisplayLayoutConfig : public ConfigBlock {
|
||||
int iInternalScreenRotation = ROTATION_LOCKED_HORIZONTAL; // The internal screen rotation angle. Useful for vertical SHMUPs and similar.
|
||||
bool bRotateControlsWithScreen = true; // Rotate gamepad controls along with the internal screen rotation.
|
||||
bool bIgnoreScreenInsets = true; // Android: Center screen disregarding insets if this is enabled.
|
||||
std::string sOverlay; // Overlay (bezel) image id, see GPU/Common/Overlay.h. Empty means off.
|
||||
|
||||
// Deprecated
|
||||
bool bEnableCardboardVR = false; // Cardboard Master Switch
|
||||
|
||||
@@ -141,6 +141,8 @@ Path GetSysDirectory(PSPDirectories directoryType) {
|
||||
return PreferUpperCaseDir(pspDirectory, "SHADERS", "shaders");
|
||||
case DIRECTORY_CUSTOM_THEMES:
|
||||
return PreferUpperCaseDir(pspDirectory, "THEMES", "themes");
|
||||
case DIRECTORY_CUSTOM_OVERLAYS:
|
||||
return PreferUpperCaseDir(pspDirectory, "OVERLAYS", "overlays");
|
||||
case DIRECTORY_NAND:
|
||||
return pspDirectory / "NAND";
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ enum PSPDirectories {
|
||||
DIRECTORY_EXDATA,
|
||||
DIRECTORY_CUSTOM_SHADERS,
|
||||
DIRECTORY_CUSTOM_THEMES,
|
||||
DIRECTORY_CUSTOM_OVERLAYS,
|
||||
COUNT,
|
||||
};
|
||||
|
||||
|
||||
@@ -114,6 +114,8 @@ set(GPU_SOURCES
|
||||
Common/GPUStateUtils.h
|
||||
Common/DrawEngineCommon.cpp
|
||||
Common/DrawEngineCommon.h
|
||||
Common/Overlay.cpp
|
||||
Common/Overlay.h
|
||||
Common/PresentationCommon.cpp
|
||||
Common/PresentationCommon.h
|
||||
Common/ReinterpretFramebuffer.cpp
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
// Copyright (c) 2026- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
#include "Common/Data/Format/IniFile.h"
|
||||
#include "Common/Data/Format/PNGLoad.h"
|
||||
#include "Common/File/DirListing.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/VR/PPSSPPVR.h"
|
||||
#include "Core/Config.h"
|
||||
#include "Core/Util/PathUtil.h"
|
||||
#include "GPU/Common/Overlay.h"
|
||||
|
||||
// Overlays are full screen images, no need to go beyond what a 4K display needs.
|
||||
// Also keeps us within the max texture size of most mobile GPUs.
|
||||
static constexpr int MAX_OVERLAY_DIMENSION = 4096;
|
||||
// Pixels with alpha above this count as the overlay's border when searching for the hole.
|
||||
// Not 0, since many bezels have soft edges or a light glare over the screen area.
|
||||
static constexpr uint8_t HOLE_ALPHA_THRESHOLD = 250;
|
||||
static constexpr int MIN_HOLE_SIZE = 16;
|
||||
|
||||
static std::mutex g_overlayLock;
|
||||
static std::vector<OverlayInfo> g_overlayInfo;
|
||||
static bool g_overlayInfoLoaded = false;
|
||||
|
||||
// The currently loaded overlay.
|
||||
static std::string g_activeId;
|
||||
static bool g_activeFailed = false;
|
||||
static OverlayInfo g_activeInfo;
|
||||
static std::shared_ptr<const OverlayImage> g_activeImage;
|
||||
static bool g_activeHasHole = false;
|
||||
static int g_activeHole[4]{};
|
||||
static uint32_t g_generation = 0;
|
||||
|
||||
bool DetectOverlayHole(const uint8_t *rgba, int width, int height, int *x, int *y, int *w, int *h) {
|
||||
if (!rgba || width <= 0 || height <= 0)
|
||||
return false;
|
||||
|
||||
auto isBorder = [&](int px, int py) {
|
||||
return rgba[((size_t)py * width + px) * 4 + 3] > HOLE_ALPHA_THRESHOLD;
|
||||
};
|
||||
|
||||
const int cx = width / 2;
|
||||
const int cy = height / 2;
|
||||
if (isBorder(cx, cy))
|
||||
return false;
|
||||
|
||||
int left = cx;
|
||||
while (left > 0 && !isBorder(left - 1, cy))
|
||||
left--;
|
||||
int right = cx;
|
||||
while (right < width - 1 && !isBorder(right + 1, cy))
|
||||
right++;
|
||||
int top = cy;
|
||||
while (top > 0 && !isBorder(cx, top - 1))
|
||||
top--;
|
||||
int bottom = cy;
|
||||
while (bottom < height - 1 && !isBorder(cx, bottom + 1))
|
||||
bottom++;
|
||||
|
||||
const int hw = right - left + 1;
|
||||
const int hh = bottom - top + 1;
|
||||
if (hw < MIN_HOLE_SIZE || hh < MIN_HOLE_SIZE)
|
||||
return false;
|
||||
|
||||
*x = left;
|
||||
*y = top;
|
||||
*w = hw;
|
||||
*h = hh;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseHole(const std::string &str, OverlayInfo *info) {
|
||||
int x, y, w, h;
|
||||
if (sscanf(str.c_str(), "%d , %d , %d , %d", &x, &y, &w, &h) != 4 || w <= 0 || h <= 0 || x < 0 || y < 0)
|
||||
return false;
|
||||
info->holeFromIni = true;
|
||||
info->holeX = x;
|
||||
info->holeY = y;
|
||||
info->holeW = w;
|
||||
info->holeH = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void LoadOverlayIni(const Path &dir, const std::string &dirName, std::vector<OverlayInfo> *out) {
|
||||
IniFile ini;
|
||||
if (!ini.Load(dir / "overlay.ini"))
|
||||
return;
|
||||
|
||||
for (auto §ionPtr : ini.Sections()) {
|
||||
const Section §ion = *sectionPtr;
|
||||
std::string image;
|
||||
if (section.name().empty() || !section.Get("Image", &image) || image.empty())
|
||||
continue;
|
||||
if (HasPathTraversal(image)) {
|
||||
WARN_LOG(Log::G3D, "Overlay '%s/%s': ignoring image name '%s'", dirName.c_str(), std::string(section.name()).c_str(), image.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
OverlayInfo info;
|
||||
info.id = dirName + "/" + std::string(section.name());
|
||||
info.name = std::string(section.name());
|
||||
section.Get("Name", &info.name);
|
||||
info.imagePath = dir / image;
|
||||
|
||||
std::string layer;
|
||||
if (section.Get("Layer", &layer) && equalsNoCase(layer, "Background"))
|
||||
info.layer = OverlayLayer::Background;
|
||||
|
||||
std::string hole;
|
||||
if (section.Get("Hole", &hole) && !hole.empty() && !ParseHole(hole, &info)) {
|
||||
WARN_LOG(Log::G3D, "Overlay '%s': bad Hole '%s', expected x,y,w,h. Will auto-detect.", info.id.c_str(), hole.c_str());
|
||||
}
|
||||
out->push_back(info);
|
||||
}
|
||||
}
|
||||
|
||||
static void ScanOverlays(std::vector<OverlayInfo> *out) {
|
||||
out->clear();
|
||||
const Path root = GetSysDirectory(DIRECTORY_CUSTOM_OVERLAYS);
|
||||
std::vector<File::FileInfo> files;
|
||||
File::GetFilesInDir(root, &files);
|
||||
|
||||
for (const auto &file : files) {
|
||||
if (file.isDirectory) {
|
||||
LoadOverlayIni(file.fullName, file.name, out);
|
||||
} else if (endsWithNoCase(file.name, ".png")) {
|
||||
OverlayInfo info;
|
||||
info.id = file.name;
|
||||
info.name = file.name.substr(0, file.name.size() - 4);
|
||||
info.imagePath = file.fullName;
|
||||
out->push_back(info);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(out->begin(), out->end(), [](const OverlayInfo &a, const OverlayInfo &b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
INFO_LOG(Log::G3D, "Found %d overlays in %s", (int)out->size(), root.c_str());
|
||||
}
|
||||
|
||||
static void UnloadActiveLocked() {
|
||||
g_activeId.clear();
|
||||
g_activeFailed = false;
|
||||
g_activeImage.reset();
|
||||
g_activeHasHole = false;
|
||||
g_generation++;
|
||||
}
|
||||
|
||||
void ReloadAllOverlayInfo() {
|
||||
std::lock_guard<std::mutex> guard(g_overlayLock);
|
||||
ScanOverlays(&g_overlayInfo);
|
||||
g_overlayInfoLoaded = true;
|
||||
UnloadActiveLocked();
|
||||
}
|
||||
|
||||
std::vector<OverlayInfo> GetAllOverlayInfo() {
|
||||
std::lock_guard<std::mutex> guard(g_overlayLock);
|
||||
if (!g_overlayInfoLoaded) {
|
||||
ScanOverlays(&g_overlayInfo);
|
||||
g_overlayInfoLoaded = true;
|
||||
}
|
||||
return g_overlayInfo;
|
||||
}
|
||||
|
||||
bool ImportOverlayImage(const Path &source, std::string *id) {
|
||||
// Check the format by content, since content URIs may not have a usable extension.
|
||||
FILE *f = File::OpenCFile(source, "rb");
|
||||
uint8_t magic[8]{};
|
||||
const bool readOk = f && fread(magic, 1, sizeof(magic), f) == sizeof(magic);
|
||||
if (f)
|
||||
fclose(f);
|
||||
static const uint8_t pngMagic[8] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n' };
|
||||
if (!readOk || memcmp(magic, pngMagic, sizeof(magic)) != 0) {
|
||||
WARN_LOG(Log::G3D, "Overlay import: %s is not a PNG", source.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const Path root = GetSysDirectory(DIRECTORY_CUSTOM_OVERLAYS);
|
||||
File::CreateFullPath(root);
|
||||
|
||||
std::string base = source.GetFilename();
|
||||
if (endsWithNoCase(base, ".png"))
|
||||
base = base.substr(0, base.size() - 4);
|
||||
if (!IsSafePathComponent(base) || base.empty())
|
||||
base = "overlay";
|
||||
|
||||
std::string filename = base + ".png";
|
||||
for (int i = 2; File::Exists(root / filename); i++) {
|
||||
filename = StringFromFormat("%s_%d.png", base.c_str(), i);
|
||||
}
|
||||
|
||||
if (!File::Copy(source, root / filename)) {
|
||||
ERROR_LOG(Log::G3D, "Overlay import: failed to copy %s to %s", source.c_str(), (root / filename).c_str());
|
||||
return false;
|
||||
}
|
||||
INFO_LOG(Log::G3D, "Imported overlay %s as %s", source.c_str(), filename.c_str());
|
||||
ReloadAllOverlayInfo();
|
||||
*id = filename;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called with the lock held. Sets g_activeFailed on failure.
|
||||
static void LoadActiveLocked(const std::string &id) {
|
||||
UnloadActiveLocked();
|
||||
g_activeId = id;
|
||||
|
||||
if (!g_overlayInfoLoaded) {
|
||||
ScanOverlays(&g_overlayInfo);
|
||||
g_overlayInfoLoaded = true;
|
||||
}
|
||||
|
||||
auto iter = std::find_if(g_overlayInfo.begin(), g_overlayInfo.end(), [&](const OverlayInfo &info) { return info.id == id; });
|
||||
if (iter == g_overlayInfo.end()) {
|
||||
WARN_LOG(Log::G3D, "Overlay '%s' not found", id.c_str());
|
||||
g_activeFailed = true;
|
||||
return;
|
||||
}
|
||||
g_activeInfo = *iter;
|
||||
|
||||
std::string data;
|
||||
if (!File::ReadBinaryFileToString(g_activeInfo.imagePath, &data)) {
|
||||
ERROR_LOG(Log::G3D, "Overlay '%s': failed to read %s", id.c_str(), g_activeInfo.imagePath.c_str());
|
||||
g_activeFailed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0, height = 0;
|
||||
unsigned char *pixels = nullptr;
|
||||
if (pngLoadPtr((const unsigned char *)data.data(), data.size(), &width, &height, &pixels, MAX_OVERLAY_DIMENSION, MAX_OVERLAY_DIMENSION) != 1 || !pixels) {
|
||||
ERROR_LOG(Log::G3D, "Overlay '%s': failed to decode %s (PNG, max %dx%d)", id.c_str(), g_activeInfo.imagePath.c_str(), MAX_OVERLAY_DIMENSION, MAX_OVERLAY_DIMENSION);
|
||||
free(pixels);
|
||||
g_activeFailed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
auto image = std::make_shared<OverlayImage>();
|
||||
image->width = width;
|
||||
image->height = height;
|
||||
image->rgba.assign(pixels, pixels + (size_t)width * height * 4);
|
||||
free(pixels);
|
||||
|
||||
if (g_activeInfo.holeFromIni) {
|
||||
// Clamp to the image.
|
||||
int x0 = std::clamp(g_activeInfo.holeX, 0, width);
|
||||
int y0 = std::clamp(g_activeInfo.holeY, 0, height);
|
||||
int x1 = std::clamp(g_activeInfo.holeX + g_activeInfo.holeW, 0, width);
|
||||
int y1 = std::clamp(g_activeInfo.holeY + g_activeInfo.holeH, 0, height);
|
||||
g_activeHasHole = x1 - x0 >= MIN_HOLE_SIZE && y1 - y0 >= MIN_HOLE_SIZE;
|
||||
g_activeHole[0] = x0;
|
||||
g_activeHole[1] = y0;
|
||||
g_activeHole[2] = x1 - x0;
|
||||
g_activeHole[3] = y1 - y0;
|
||||
} else {
|
||||
g_activeHasHole = DetectOverlayHole(image->rgba.data(), width, height, &g_activeHole[0], &g_activeHole[1], &g_activeHole[2], &g_activeHole[3]);
|
||||
}
|
||||
|
||||
if (g_activeHasHole) {
|
||||
INFO_LOG(Log::G3D, "Overlay '%s': %dx%d, hole at %d,%d size %dx%d%s", id.c_str(), width, height,
|
||||
g_activeHole[0], g_activeHole[1], g_activeHole[2], g_activeHole[3], g_activeInfo.holeFromIni ? "" : " (detected)");
|
||||
} else {
|
||||
WARN_LOG(Log::G3D, "Overlay '%s': %dx%d, no hole found, the game keeps its normal position. Set Hole=x,y,w,h in overlay.ini to place it.", id.c_str(), width, height);
|
||||
}
|
||||
|
||||
g_activeImage = image;
|
||||
}
|
||||
|
||||
bool GetOverlayLayout(const DisplayLayoutConfig &config, float frameX, float frameY, float frameW, float frameH, OverlayLayout *layout) {
|
||||
if (config.sOverlay.empty() || config.bEnableCardboardVR || IsVREnabled() || frameW <= 0.0f || frameH <= 0.0f)
|
||||
return false;
|
||||
// With skip buffer effects, the game draws straight to the backbuffer and the presentation
|
||||
// code that draws the overlay doesn't run. Keep the normal layout then, rather than a game in a
|
||||
// hole with nothing around it. The software renderer always presents, so it's fine there.
|
||||
if (g_Config.bSkipBufferEffects && !g_Config.bSoftwareRendering)
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> guard(g_overlayLock);
|
||||
if (config.sOverlay != g_activeId) {
|
||||
LoadActiveLocked(config.sOverlay);
|
||||
}
|
||||
if (g_activeFailed || !g_activeImage)
|
||||
return false;
|
||||
|
||||
const float imgW = (float)g_activeImage->width;
|
||||
const float imgH = (float)g_activeImage->height;
|
||||
|
||||
// Fit the overlay to the frame, keeping the aspect ratio, centered.
|
||||
const float scale = std::min(frameW / imgW, frameH / imgH);
|
||||
layout->overlayW = imgW * scale;
|
||||
layout->overlayH = imgH * scale;
|
||||
layout->overlayX = frameX + (frameW - layout->overlayW) * 0.5f;
|
||||
layout->overlayY = frameY + (frameH - layout->overlayH) * 0.5f;
|
||||
|
||||
layout->layer = g_activeInfo.layer;
|
||||
layout->hasHole = g_activeHasHole;
|
||||
if (g_activeHasHole) {
|
||||
layout->holeX = layout->overlayX + g_activeHole[0] * scale;
|
||||
layout->holeY = layout->overlayY + g_activeHole[1] * scale;
|
||||
layout->holeW = g_activeHole[2] * scale;
|
||||
layout->holeH = g_activeHole[3] * scale;
|
||||
}
|
||||
layout->generation = g_generation;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<const OverlayImage> GetOverlayImage(uint32_t generation) {
|
||||
std::lock_guard<std::mutex> guard(g_overlayLock);
|
||||
if (generation != g_generation)
|
||||
return nullptr;
|
||||
return g_activeImage;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2026- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
// Overlays (bezels): an image drawn around (or behind) the emulated screen, filling the
|
||||
// black bars on displays whose aspect ratio doesn't match the PSP's.
|
||||
//
|
||||
// Overlays live in memstick/PSP/OVERLAYS (or overlays):
|
||||
//
|
||||
// - Any PNG directly in that folder is an overlay on its own. It's drawn in front of the game,
|
||||
// and the game is placed in its transparent hole, which is found automatically.
|
||||
// Existing RetroArch overlay PNGs work as-is this way. "Load overlay image..." in the display
|
||||
// layout screen copies any picked PNG into this folder, so no file management is needed.
|
||||
//
|
||||
// - A subfolder with an overlay.ini can define one or more overlays, one per section:
|
||||
//
|
||||
// [RP2S_Digimon]
|
||||
// Name=Retroid Pocket 2S - Digimon
|
||||
// Image=rp2s_640x480.png
|
||||
// Layer=Foreground ; or Background (drawn behind the game)
|
||||
// Hole=0,0,640,362 ; optional, in image pixels. Auto-detected if missing.
|
||||
//
|
||||
// Placement: the overlay is fit to the screen (aspect preserved, centered). The hole is mapped
|
||||
// to the screen with the same scale and offset, and the PSP screen is then fit into the hole
|
||||
// using the normal display layout options (stretch, integer scale, aspect ratio).
|
||||
// The display offset and scale options are ignored while an overlay with a hole is active.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/File/Path.h"
|
||||
|
||||
struct DisplayLayoutConfig;
|
||||
struct FRect;
|
||||
|
||||
enum class OverlayLayer {
|
||||
Foreground,
|
||||
Background,
|
||||
};
|
||||
|
||||
struct OverlayInfo {
|
||||
std::string id; // What gets stored in the config. "file.png" or "folder/section".
|
||||
std::string name; // Display name.
|
||||
Path imagePath;
|
||||
OverlayLayer layer = OverlayLayer::Foreground;
|
||||
// Hole from the ini, in image pixels. If not set, it's auto-detected.
|
||||
bool holeFromIni = false;
|
||||
int holeX = 0;
|
||||
int holeY = 0;
|
||||
int holeW = 0;
|
||||
int holeH = 0;
|
||||
};
|
||||
|
||||
// The decoded image of the active overlay, for uploading to the GPU.
|
||||
struct OverlayImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> rgba; // width * height * 4 bytes.
|
||||
};
|
||||
|
||||
// Where things go on screen, for the active overlay.
|
||||
struct OverlayLayout {
|
||||
OverlayLayer layer = OverlayLayer::Foreground;
|
||||
float overlayX = 0.0f, overlayY = 0.0f, overlayW = 0.0f, overlayH = 0.0f; // Overlay image, in output pixels.
|
||||
bool hasHole = false;
|
||||
float holeX = 0.0f, holeY = 0.0f, holeW = 0.0f, holeH = 0.0f; // The hole, in output pixels.
|
||||
// Changes whenever a different image gets loaded, so the presentation code knows to re-upload.
|
||||
uint32_t generation = 0;
|
||||
};
|
||||
|
||||
// Rescans the overlays folder. Also unloads the active overlay, so it gets reloaded from disk.
|
||||
void ReloadAllOverlayInfo();
|
||||
std::vector<OverlayInfo> GetAllOverlayInfo();
|
||||
|
||||
// The "just pick an image" flow: copies a PNG from anywhere (including Android content URIs) into the
|
||||
// overlays folder and rescans. On success, *id is what to put in DisplayLayoutConfig::sOverlay.
|
||||
// Such overlays are always foreground with an auto-detected hole, no ini needed.
|
||||
bool ImportOverlayImage(const Path &source, std::string *id);
|
||||
|
||||
// Returns false if no overlay is selected in this config, it failed to load, or overlays can't
|
||||
// be used right now (VR, Cardboard). Cheap to call every frame; loads the image on first use.
|
||||
bool GetOverlayLayout(const DisplayLayoutConfig &config, float frameX, float frameY, float frameW, float frameH, OverlayLayout *layout);
|
||||
|
||||
// The decoded image for the given generation, or null if it's no longer the active one.
|
||||
std::shared_ptr<const OverlayImage> GetOverlayImage(uint32_t generation);
|
||||
|
||||
// Finds the transparent hole by walking from the image center towards each edge, stopping at
|
||||
// the first nearly-opaque pixel. Returns false if the center itself is opaque, or the hole is tiny.
|
||||
// Exposed for unit tests.
|
||||
bool DetectOverlayHole(const uint8_t *rgba, int width, int height, int *x, int *y, int *w, int *h);
|
||||
@@ -19,6 +19,8 @@
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include "Common/GPU/thin3d.h"
|
||||
|
||||
#include "Common/System/Display.h"
|
||||
@@ -33,6 +35,7 @@
|
||||
#include "Core/ConfigValues.h"
|
||||
#include "Core/System.h"
|
||||
#include "Core/HW/Display.h"
|
||||
#include "GPU/Common/Overlay.h"
|
||||
#include "GPU/Common/PostShader.h"
|
||||
#include "GPU/Common/PresentationCommon.h"
|
||||
#include "GPU/GPUState.h"
|
||||
@@ -92,7 +95,7 @@ FRect GetScreenFrame(bool ignoreScreenInsets, float pixelWidth, float pixelHeigh
|
||||
return rc;
|
||||
}
|
||||
|
||||
void CalculateDisplayOutputRect(const DisplayLayoutConfig &config, FRect *rc, float origW, float origH, const FRect &frame, int rotation) {
|
||||
void CalculateDisplayOutputRect(const DisplayLayoutConfig &config, FRect *rc, float origW, float origH, const FRect &screenFrame, int rotation) {
|
||||
float outW;
|
||||
float outH;
|
||||
|
||||
@@ -104,6 +107,18 @@ void CalculateDisplayOutputRect(const DisplayLayoutConfig &config, FRect *rc, fl
|
||||
float offsetY = config.fDisplayOffsetY;
|
||||
|
||||
float scale = config.fDisplayScale;
|
||||
|
||||
// With an overlay that has a hole, the overlay is fit to the screen and the game is fit into
|
||||
// the hole instead. The offset and scale options don't apply then, the overlay decides those.
|
||||
FRect frame = screenFrame;
|
||||
OverlayLayout overlay;
|
||||
const bool inOverlayHole = GetOverlayLayout(config, screenFrame.x, screenFrame.y, screenFrame.w, screenFrame.h, &overlay) && overlay.hasHole;
|
||||
if (inOverlayHole) {
|
||||
frame = FRect{ overlay.holeX, overlay.holeY, overlay.holeW, overlay.holeH };
|
||||
offsetX = 0.5f;
|
||||
offsetY = 0.5f;
|
||||
scale = 1.0f;
|
||||
}
|
||||
float aspectRatioAdjust = config.fDisplayAspectRatio;
|
||||
|
||||
float origRatio = !rotated ? origW / origH : origH / origW;
|
||||
@@ -136,7 +151,7 @@ void CalculateDisplayOutputRect(const DisplayLayoutConfig &config, FRect *rc, fl
|
||||
|
||||
// Ye olde 1080p hack: If everything is setup to exactly cover the screen (defaults), and the screen display aspect ratio is 16:9,
|
||||
// cut off one line from the top and bottom.
|
||||
if (scale == 1.0f && aspectRatioAdjust == 1.0f && offsetX == 0.5f && offsetY == 0.5f && g_Config.bDisplayCropTo16x9) {
|
||||
if (!inOverlayHole && scale == 1.0f && aspectRatioAdjust == 1.0f && offsetX == 0.5f && offsetY == 0.5f && g_Config.bDisplayCropTo16x9) {
|
||||
if (fabsf(frame.w / frame.h - 16.0f / 9.0f) < 0.0001f) {
|
||||
outW *= 272.0f / 270.0f;
|
||||
outH *= 272.0f / 270.0f;
|
||||
@@ -173,6 +188,14 @@ void CalculateDisplayOutputRect(const DisplayLayoutConfig &config, FRect *rc, fl
|
||||
rc->h = floorf(frame.h);
|
||||
outW = frame.w;
|
||||
outH = frame.h;
|
||||
} else if (inOverlayHole && overlay.layer == OverlayLayer::Foreground) {
|
||||
// Round outwards, so no black seam shows between the game and the overlay's border.
|
||||
const float x0 = frame.x + frame.w * offsetX - outW * 0.5f;
|
||||
const float y0 = frame.y + frame.h * offsetY - outH * 0.5f;
|
||||
rc->x = floorf(x0);
|
||||
rc->y = floorf(y0);
|
||||
rc->w = ceilf(x0 + outW) - rc->x;
|
||||
rc->h = ceilf(y0 + outH) - rc->y;
|
||||
} else {
|
||||
rc->x = floorf(frame.x + frame.w * offsetX - outW * 0.5f);
|
||||
rc->y = floorf(frame.y + frame.h * offsetY - outH * 0.5f);
|
||||
@@ -491,7 +514,7 @@ void PresentationCommon::DeviceRestore(Draw::DrawContext *draw) {
|
||||
CreateDeviceObjects();
|
||||
}
|
||||
|
||||
Draw::Pipeline *PresentationCommon::CreatePipeline(std::vector<Draw::ShaderModule *> shaders, bool postShader, const UniformBufferDesc *uniformDesc) const {
|
||||
Draw::Pipeline *PresentationCommon::CreatePipeline(std::vector<Draw::ShaderModule *> shaders, bool postShader, const UniformBufferDesc *uniformDesc, bool alphaBlend) const {
|
||||
using namespace Draw;
|
||||
|
||||
Semantic pos = SEM_POSITION;
|
||||
@@ -514,7 +537,9 @@ Draw::Pipeline *PresentationCommon::CreatePipeline(std::vector<Draw::ShaderModul
|
||||
|
||||
InputLayout *inputLayout = draw_->CreateInputLayout(inputDesc);
|
||||
DepthStencilState *depth = draw_->CreateDepthStencilState({ false, false, Comparison::LESS });
|
||||
BlendState *blendstateOff = draw_->CreateBlendState({ false, 0xF });
|
||||
BlendState *blendstateOff = alphaBlend ?
|
||||
draw_->CreateBlendState({ true, 0xF, BlendFactor::SRC_ALPHA, BlendFactor::ONE_MINUS_SRC_ALPHA, BlendOp::ADD, BlendFactor::ONE, BlendFactor::ONE_MINUS_SRC_ALPHA, BlendOp::ADD }) :
|
||||
draw_->CreateBlendState({ false, 0xF });
|
||||
RasterState *rasterNoCull = draw_->CreateRasterState({});
|
||||
|
||||
PipelineDesc pipelineDesc{ Primitive::TRIANGLE_STRIP, shaders, inputLayout, depth, blendstateOff, rasterNoCull, uniformDesc };
|
||||
@@ -542,6 +567,8 @@ void PresentationCommon::CreateDeviceObjects() {
|
||||
|
||||
texColor_ = CreatePipeline({ draw_->GetVshaderPreset(VS_TEXTURE_COLOR_2D), draw_->GetFshaderPreset(FS_TEXTURE_COLOR_2D) }, false, &vsTexColBufDesc);
|
||||
texColorRBSwizzle_ = CreatePipeline({ draw_->GetVshaderPreset(VS_TEXTURE_COLOR_2D), draw_->GetFshaderPreset(FS_TEXTURE_COLOR_2D_RB_SWIZZLE) }, false, &vsTexColBufDesc);
|
||||
texColorBlend_ = CreatePipeline({ draw_->GetVshaderPreset(VS_TEXTURE_COLOR_2D), draw_->GetFshaderPreset(FS_TEXTURE_COLOR_2D) }, false, &vsTexColBufDesc, true);
|
||||
overlayVdata_ = draw_->CreateBuffer(sizeof(Vertex) * 4, BufferUsageFlag::DYNAMIC | BufferUsageFlag::VERTEXDATA);
|
||||
|
||||
restorePostShader_ = false;
|
||||
}
|
||||
@@ -567,6 +594,10 @@ static void DoReleaseVector(std::vector<T *> &list) {
|
||||
void PresentationCommon::DestroyDeviceObjects() {
|
||||
DoRelease(texColor_);
|
||||
DoRelease(texColorRBSwizzle_);
|
||||
DoRelease(texColorBlend_);
|
||||
DoRelease(overlayVdata_);
|
||||
DoRelease(overlayTexture_);
|
||||
overlayGeneration_ = 0xFFFFFFFF;
|
||||
DoRelease(samplerNearest_);
|
||||
DoRelease(samplerLinear_);
|
||||
DoRelease(vdata_);
|
||||
@@ -900,6 +931,74 @@ void PresentationCommon::RunPostshaderPasses(const DisplayLayoutConfig &config,
|
||||
}
|
||||
}
|
||||
|
||||
// Uploads the active overlay image if it changed. Returns false if there's nothing to draw.
|
||||
bool PresentationCommon::PrepareOverlayTexture(const OverlayLayout &layout) {
|
||||
if (overlayTexture_ && overlayGeneration_ == layout.generation)
|
||||
return true;
|
||||
|
||||
DoRelease(overlayTexture_);
|
||||
overlayGeneration_ = layout.generation;
|
||||
|
||||
std::shared_ptr<const OverlayImage> image = GetOverlayImage(layout.generation);
|
||||
if (!image || image->width <= 0 || image->height <= 0) {
|
||||
// Changed under us, we'll get it next frame. Retry then.
|
||||
overlayGeneration_ = 0xFFFFFFFF;
|
||||
return false;
|
||||
}
|
||||
|
||||
Draw::TextureDesc desc{};
|
||||
desc.type = Draw::TextureType::LINEAR2D;
|
||||
desc.format = Draw::DataFormat::R8G8B8A8_UNORM;
|
||||
desc.width = image->width;
|
||||
desc.height = image->height;
|
||||
desc.depth = 1;
|
||||
desc.mipLevels = 1;
|
||||
desc.generateMips = false;
|
||||
desc.tag = "overlay";
|
||||
desc.initData.push_back(image->rgba.data());
|
||||
overlayTexture_ = draw_->CreateTexture(desc);
|
||||
if (!overlayTexture_) {
|
||||
ERROR_LOG(Log::G3D, "Failed to create overlay texture (%dx%d)", image->width, image->height);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PresentationCommon::DrawOverlay(const OverlayLayout &layout) {
|
||||
const float x0 = layout.overlayX, y0 = layout.overlayY;
|
||||
const float x1 = x0 + layout.overlayW, y1 = y0 + layout.overlayH;
|
||||
Vertex verts[4] = {
|
||||
{ x0, y0, 0, 0.0f, 0.0f, 0xFFFFFFFF }, // TL
|
||||
{ x1, y0, 0, 1.0f, 0.0f, 0xFFFFFFFF }, // TR
|
||||
{ x0, y1, 0, 0.0f, 1.0f, 0xFFFFFFFF }, // BL
|
||||
{ x1, y1, 0, 1.0f, 1.0f, 0xFFFFFFFF }, // BR
|
||||
};
|
||||
// Same transform as the game quad, see RunPostshaderPasses.
|
||||
const bool flipped = draw_->GetDeviceCaps().coordConvention == CoordConvention::OpenGL;
|
||||
const float invDestW = 2.0f / pixelWidth_;
|
||||
const float invDestH = 2.0f / pixelHeight_;
|
||||
for (auto &v : verts) {
|
||||
v.x = v.x * invDestW - 1.0f;
|
||||
v.y = v.y * invDestH - 1.0f;
|
||||
if (flipped)
|
||||
v.y = -v.y;
|
||||
}
|
||||
draw_->UpdateBuffer(overlayVdata_, (const uint8_t *)verts, 0, sizeof(verts), Draw::UPDATE_DISCARD);
|
||||
|
||||
draw_->BindPipeline(texColorBlend_);
|
||||
Draw::VsTexColUB ub{};
|
||||
memcpy(ub.WorldViewProj, g_display.rot_matrix.m, sizeof(float) * 16);
|
||||
draw_->UpdateDynamicUniformBuffer(&ub, sizeof(ub));
|
||||
draw_->BindTexture(0, overlayTexture_);
|
||||
draw_->BindSamplerStates(0, 1, &samplerLinear_);
|
||||
draw_->BindVertexBuffer(overlayVdata_, 0);
|
||||
|
||||
Draw::Viewport viewport{ 0.0f, 0.0f, (float)pixelWidth_, (float)pixelHeight_, 0.0f, 1.0f };
|
||||
draw_->SetViewport(viewport);
|
||||
draw_->SetScissorRect(0, 0, pixelWidth_, pixelHeight_);
|
||||
draw_->Draw(4, 0);
|
||||
}
|
||||
|
||||
void PresentationCommon::CopyToOutput(const DisplayLayoutConfig &config) {
|
||||
bool useNearest = outputFlags_ & OutputFlags::NEAREST;
|
||||
bool useStereo = gstate_c.Use(GPU_USE_SIMPLE_STEREO_PERSPECTIVE) && stereoPipeline_ != nullptr; // TODO: Also check that the backend has support for it.
|
||||
@@ -911,12 +1010,27 @@ void PresentationCommon::CopyToOutput(const DisplayLayoutConfig &config) {
|
||||
|
||||
draw_->SetScissorRect(0, 0, pixelWidth_, pixelHeight_);
|
||||
|
||||
OverlayLayout overlay;
|
||||
bool drawOverlay = false;
|
||||
if (!(outputFlags_ & OutputFlags::NATIVE_SIZE)) {
|
||||
const FRect frame = GetScreenFrame(config.bIgnoreScreenInsets, (float)pixelWidth_, (float)pixelHeight_);
|
||||
drawOverlay = GetOverlayLayout(config, frame.x, frame.y, frame.w, frame.h, &overlay) && PrepareOverlayTexture(overlay);
|
||||
}
|
||||
|
||||
if (!srcFramebuffer_ && !srcTexture_) {
|
||||
// Bound blank. We're done (although we could draw a black rectangle here).
|
||||
// Bound blank. Keep the overlay up though, so it doesn't flicker during loading and such.
|
||||
if (drawOverlay) {
|
||||
DrawOverlay(overlay);
|
||||
draw_->Invalidate(InvalidationFlags::CACHED_RENDER_STATE);
|
||||
}
|
||||
presentedThisFrame_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawOverlay && overlay.layer == OverlayLayer::Background) {
|
||||
DrawOverlay(overlay);
|
||||
}
|
||||
|
||||
Draw::Pipeline *pipeline = (outputFlags_ & OutputFlags::RB_SWIZZLE) ? texColorRBSwizzle_ : texColor_;
|
||||
|
||||
if (useStereo) {
|
||||
@@ -983,6 +1097,10 @@ void PresentationCommon::CopyToOutput(const DisplayLayoutConfig &config) {
|
||||
draw_->Draw(4, 0);
|
||||
}
|
||||
|
||||
if (drawOverlay && overlay.layer == OverlayLayer::Foreground) {
|
||||
DrawOverlay(overlay);
|
||||
}
|
||||
|
||||
DoRelease(srcFramebuffer_);
|
||||
DoRelease(srcTexture_);
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ class Texture;
|
||||
}
|
||||
|
||||
struct ShaderInfo;
|
||||
struct OverlayLayout;
|
||||
class TextureCacheCommon;
|
||||
|
||||
enum class OutputFlags {
|
||||
@@ -148,13 +149,16 @@ protected:
|
||||
static void ShowPostShaderError(const std::string &errorString);
|
||||
|
||||
Draw::ShaderModule *CompileShaderModule(ShaderStage stage, ShaderLanguage lang, const std::string &src, std::string *errorString) const;
|
||||
Draw::Pipeline *CreatePipeline(std::vector<Draw::ShaderModule *> shaders, bool postShader, const UniformBufferDesc *uniformDesc) const;
|
||||
Draw::Pipeline *CreatePipeline(std::vector<Draw::ShaderModule *> shaders, bool postShader, const UniformBufferDesc *uniformDesc, bool alphaBlend = false) const;
|
||||
bool CompilePostShader(const ShaderInfo *shaderInfo, Draw::Pipeline **outPipeline) const;
|
||||
bool BuildPostShader(const DisplayLayoutConfig &config, const ShaderInfo *shaderInfo, const ShaderInfo *next, Draw::Pipeline **outPipeline);
|
||||
bool AllocateFramebuffer(int w, int h);
|
||||
|
||||
bool BindSource(int binding, bool bindStereo);
|
||||
|
||||
bool PrepareOverlayTexture(const OverlayLayout &layout);
|
||||
void DrawOverlay(const OverlayLayout &layout);
|
||||
|
||||
void GetCardboardSettings(const DisplayLayoutConfig &config, CardboardSettings *cardboardSettings) const;
|
||||
void CalculatePostShaderUniforms(int bufferWidth, int bufferHeight, int targetWidth, int targetHeight, const ShaderInfo *shaderInfo, PostShaderUniforms *uniforms) const;
|
||||
|
||||
@@ -165,6 +169,12 @@ protected:
|
||||
Draw::SamplerState *samplerLinear_ = nullptr;
|
||||
Draw::Buffer *vdata_ = nullptr;
|
||||
|
||||
// Overlay (bezel) drawn around or behind the game, see Overlay.h.
|
||||
Draw::Pipeline *texColorBlend_ = nullptr;
|
||||
Draw::Buffer *overlayVdata_ = nullptr;
|
||||
Draw::Texture *overlayTexture_ = nullptr;
|
||||
uint32_t overlayGeneration_ = 0xFFFFFFFF;
|
||||
|
||||
std::vector<Draw::Pipeline *> postShaderPipelines_;
|
||||
std::vector<Draw::Framebuffer *> postShaderFramebuffers_;
|
||||
std::vector<ShaderInfo> postShaderInfo_;
|
||||
|
||||
@@ -271,6 +271,7 @@
|
||||
<ClInclude Include="Common\GPUStateUtils.h" />
|
||||
<ClInclude Include="Common\IndexGenerator.h" />
|
||||
<ClInclude Include="Common\PostShader.h" />
|
||||
<ClInclude Include="Common\Overlay.h" />
|
||||
<ClInclude Include="Common\PresentationCommon.h" />
|
||||
<ClInclude Include="Common\ShaderCommon.h" />
|
||||
<ClInclude Include="Common\ShaderId.h" />
|
||||
@@ -378,6 +379,7 @@
|
||||
<ClCompile Include="Common\GPUStateUtils.cpp" />
|
||||
<ClCompile Include="Common\IndexGenerator.cpp" />
|
||||
<ClCompile Include="Common\PostShader.cpp" />
|
||||
<ClCompile Include="Common\Overlay.cpp" />
|
||||
<ClCompile Include="Common\PresentationCommon.cpp" />
|
||||
<ClCompile Include="Common\ShaderCommon.cpp" />
|
||||
<ClCompile Include="Common\ShaderId.cpp" />
|
||||
|
||||
@@ -213,6 +213,9 @@
|
||||
<ClInclude Include="Software\RasterizerRectangle.h">
|
||||
<Filter>Software</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Common\Overlay.h">
|
||||
<Filter>Common</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Common\PresentationCommon.h">
|
||||
<Filter>Common</Filter>
|
||||
</ClInclude>
|
||||
@@ -473,6 +476,9 @@
|
||||
<ClCompile Include="Software\RasterizerRectangle.cpp">
|
||||
<Filter>Software</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Common\Overlay.cpp">
|
||||
<Filter>Common</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Common\PresentationCommon.cpp">
|
||||
<Filter>Common</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
#include "Common/System/NativeApp.h"
|
||||
#include "Common/VR/PPSSPPVR.h"
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/System/OSD.h"
|
||||
#include "Common/System/Request.h"
|
||||
#include "Core/Util/PathUtil.h"
|
||||
|
||||
#include "Common/Data/Color/RGBAUtil.h"
|
||||
#include "Common/Data/Text/I18n.h"
|
||||
@@ -41,6 +45,7 @@
|
||||
#include "Core/System.h"
|
||||
#include "GPU/GPUCommon.h"
|
||||
#include "GPU/Common/FramebufferManagerCommon.h"
|
||||
#include "GPU/Common/Overlay.h"
|
||||
#include "GPU/Common/PresentationCommon.h"
|
||||
|
||||
static const int leftColumnWidth = 200;
|
||||
@@ -345,6 +350,54 @@ void DisplayLayoutScreen::CreateViews() {
|
||||
if (!IsVREnabled()) {
|
||||
static const char *bufFilters[] = { "Linear", "Nearest", };
|
||||
leftColumn->Add(new PopupMultiChoice(&config.iDisplayFilter, gr->T("Screen Scaling Filter"), bufFilters, 1, ARRAY_SIZE(bufFilters), I18NCat::GRAPHICS, screenManager()));
|
||||
|
||||
// Overlays (bezels), from PSP/OVERLAYS. Rescanned each time, so new files show up without a restart.
|
||||
ReloadAllOverlayInfo();
|
||||
const std::vector<OverlayInfo> overlays = GetAllOverlayInfo();
|
||||
std::vector<std::string> overlayNames{ std::string(sy->T("Off")) };
|
||||
std::vector<std::string> overlayIds{ "" };
|
||||
bool selectedFound = config.sOverlay.empty();
|
||||
for (const auto &overlay : overlays) {
|
||||
overlayNames.push_back(overlay.name);
|
||||
overlayIds.push_back(overlay.id);
|
||||
if (overlay.id == config.sOverlay)
|
||||
selectedFound = true;
|
||||
}
|
||||
if (!selectedFound) {
|
||||
// Keep showing a selected overlay whose files went missing, so it can be turned off.
|
||||
overlayNames.push_back(config.sOverlay);
|
||||
overlayIds.push_back(config.sOverlay);
|
||||
}
|
||||
if (overlayIds.size() > 1) {
|
||||
auto overlayChoice = leftColumn->Add(new PopupMultiChoiceDynamic(&config.sOverlay, gr->T("Overlay"), overlayNames, I18NCat::NONE, screenManager(), &overlayIds));
|
||||
overlayChoice->OnChoice.Add([](UI::EventParams &) {
|
||||
// The game rect may have moved, which matters for post shaders that run at output resolution.
|
||||
NotifyPostChanges();
|
||||
});
|
||||
}
|
||||
|
||||
// Just pick any PNG: it's copied into the overlays folder and everything is auto-detected.
|
||||
Choice *browseOverlay = leftColumn->Add(new Choice(gr->T("Load overlay image...")));
|
||||
const DeviceOrientation orientation = GetDeviceOrientation();
|
||||
browseOverlay->OnClick.Add([this, orientation](UI::EventParams &) {
|
||||
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
|
||||
const Path savePath = GetSysDirectory(DIRECTORY_CUSTOM_OVERLAYS) / "imported.png";
|
||||
System_BrowseForImage(GetRequesterToken(), gr->T("Load overlay image..."), savePath, [this, orientation, savePath](std::string_view value, int converted) {
|
||||
// If the platform converted the image, it's been saved to savePath already.
|
||||
const Path source = converted == 1 ? savePath : Path(value);
|
||||
std::string id;
|
||||
if (!source.empty() && ImportOverlayImage(source, &id)) {
|
||||
if (converted == 1)
|
||||
File::Delete(savePath);
|
||||
g_Config.GetDisplayLayoutConfig(orientation).sOverlay = id;
|
||||
NotifyPostChanges();
|
||||
RecreateViews();
|
||||
} else if (!value.empty() || converted == 1) {
|
||||
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
|
||||
g_OSD.Show(OSDType::MESSAGE_ERROR, gr->T("Overlay images must be PNG files"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Draw::DrawContext *draw = screenManager()->getDrawContext();
|
||||
|
||||
@@ -911,6 +911,7 @@ static void DrawInternals(ImConfig &cfg) {
|
||||
{DIRECTORY_EXDATA, "EXDATA"},
|
||||
{DIRECTORY_CUSTOM_SHADERS, "CUSTOM_SHADERS"},
|
||||
{DIRECTORY_CUSTOM_THEMES, "CUSTOM_THEMES"},
|
||||
{DIRECTORY_CUSTOM_OVERLAYS, "CUSTOM_OVERLAYS"},
|
||||
};
|
||||
|
||||
if (ImGui::CollapsingHeader("GetSysDirectory")) {
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
<ClInclude Include="..\..\GPU\Common\DrawEngineCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\FragmentShaderGenerator.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\FramebufferManagerCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\Overlay.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\PresentationCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\GPUDebugInterface.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\GPUStateUtils.h" />
|
||||
@@ -165,6 +166,7 @@
|
||||
<ClCompile Include="..\..\GPU\Common\DrawEngineCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\FragmentShaderGenerator.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\FramebufferManagerCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\Overlay.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\PresentationCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\GPUDebugInterface.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\GPUStateUtils.cpp" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<ClCompile Include="..\..\GPU\Common\DepalettizeShaderCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\DrawEngineCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\FramebufferManagerCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\Overlay.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\PresentationCommon.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\GPUDebugInterface.cpp" />
|
||||
<ClCompile Include="..\..\GPU\Common\GPUStateUtils.cpp" />
|
||||
@@ -86,6 +87,7 @@
|
||||
<ClInclude Include="..\..\GPU\Common\DepalettizeShaderCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\DrawEngineCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\FramebufferManagerCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\Overlay.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\PresentationCommon.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\GPUDebugInterface.h" />
|
||||
<ClInclude Include="..\..\GPU\Common\GPUStateUtils.h" />
|
||||
|
||||
@@ -594,6 +594,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/GPU/Common/DepalettizeShaderCommon.cpp \
|
||||
$(SRC)/GPU/Common/FragmentShaderGenerator.cpp \
|
||||
$(SRC)/GPU/Common/FramebufferManagerCommon.cpp \
|
||||
$(SRC)/GPU/Common/Overlay.cpp \
|
||||
$(SRC)/GPU/Common/PresentationCommon.cpp \
|
||||
$(SRC)/GPU/Common/GPUDebugInterface.cpp \
|
||||
$(SRC)/GPU/Common/IndexGenerator.cpp.arm \
|
||||
|
||||
@@ -557,6 +557,7 @@ SOURCES_CXX += \
|
||||
$(GPUCOMMONDIR)/DrawEngineCommon.cpp \
|
||||
$(GPUCOMMONDIR)/SplineCommon.cpp \
|
||||
$(GPUCOMMONDIR)/FramebufferManagerCommon.cpp \
|
||||
$(GPUCOMMONDIR)/Overlay.cpp \
|
||||
$(GPUCOMMONDIR)/PresentationCommon.cpp \
|
||||
$(GPUCOMMONDIR)/ReinterpretFramebuffer.cpp \
|
||||
$(GPUCOMMONDIR)/ShaderId.cpp \
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
#include "GPU/Common/DepthSwizzle.h"
|
||||
#include "GPU/Common/TextureDecoder.h"
|
||||
#include "GPU/Common/GPUStateUtils.h"
|
||||
#include "GPU/Common/Overlay.h"
|
||||
#include "GPU/Math3D.h"
|
||||
|
||||
#include "Common/File/AndroidContentURI.h"
|
||||
@@ -3415,6 +3416,52 @@ bool TestLang() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TestOverlayHole() {
|
||||
const int W = 64, H = 48;
|
||||
std::vector<uint8_t> img(W * H * 4);
|
||||
auto fill = [&](int x0, int y0, int x1, int y1, uint8_t a) {
|
||||
for (int y = y0; y < y1; y++)
|
||||
for (int x = x0; x < x1; x++)
|
||||
img[(y * W + x) * 4 + 3] = a;
|
||||
};
|
||||
int x, y, w, h;
|
||||
|
||||
// Opaque frame with a transparent hole at 8,4 size 40x30 (contains the center 32,24).
|
||||
fill(0, 0, W, H, 255);
|
||||
fill(8, 4, 48, 34, 0);
|
||||
EXPECT_TRUE(DetectOverlayHole(img.data(), W, H, &x, &y, &w, &h));
|
||||
EXPECT_EQ_INT(x, 8);
|
||||
EXPECT_EQ_INT(y, 4);
|
||||
EXPECT_EQ_INT(w, 40);
|
||||
EXPECT_EQ_INT(h, 30);
|
||||
|
||||
// A semi-transparent glare over the screen area and soft edges still count as the hole.
|
||||
fill(20, 10, 40, 30, 200);
|
||||
fill(7, 4, 8, 34, 128);
|
||||
EXPECT_TRUE(DetectOverlayHole(img.data(), W, H, &x, &y, &w, &h));
|
||||
EXPECT_EQ_INT(x, 7);
|
||||
EXPECT_EQ_INT(w, 41);
|
||||
|
||||
// Fully transparent image: the hole is the whole image.
|
||||
fill(0, 0, W, H, 0);
|
||||
EXPECT_TRUE(DetectOverlayHole(img.data(), W, H, &x, &y, &w, &h));
|
||||
EXPECT_EQ_INT(x, 0);
|
||||
EXPECT_EQ_INT(y, 0);
|
||||
EXPECT_EQ_INT(w, W);
|
||||
EXPECT_EQ_INT(h, H);
|
||||
|
||||
// Hole not covering the center: no valid start point.
|
||||
fill(0, 0, W, H, 255);
|
||||
fill(0, 0, 30, 20, 0);
|
||||
EXPECT_FALSE(DetectOverlayHole(img.data(), W, H, &x, &y, &w, &h));
|
||||
|
||||
// Tiny hole is rejected.
|
||||
fill(0, 0, W, H, 255);
|
||||
fill(30, 22, 36, 28, 0);
|
||||
EXPECT_FALSE(DetectOverlayHole(img.data(), W, H, &x, &y, &w, &h));
|
||||
return true;
|
||||
}
|
||||
|
||||
typedef bool (*TestFunc)();
|
||||
struct TestItem {
|
||||
const char *name;
|
||||
@@ -3877,6 +3924,7 @@ TestItem availableTests[] = {
|
||||
TEST_ITEM(BlockShadow),
|
||||
TEST_ITEM(ShaderGenerators),
|
||||
TEST_ITEM(Path),
|
||||
TEST_ITEM(OverlayHole),
|
||||
TEST_ITEM(AndroidContentURI),
|
||||
TEST_ITEM(ThreadManager),
|
||||
TEST_ITEM(WrapText),
|
||||
|
||||
Reference in new issue
Block a user