diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index 653ff19e76..1d0717bfe0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -123,10 +124,25 @@ struct OverlayRect { enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 }; inline constexpr std::size_t OverlayColorCount = 3; +struct OverlayMetrics { + static constexpr std::int32_t GlyphWidth = 5; + static constexpr std::int32_t GlyphHeight = 7; + static constexpr std::int32_t Advance = 6; + static constexpr std::int32_t LinePitch = 9; + static constexpr std::int32_t Columns = 27; + static constexpr std::int32_t Padding = 3; + static constexpr std::int32_t Margin = 4; + static constexpr std::int32_t GraphHeight = 20; + static constexpr std::int32_t GraphGap = 2; + static constexpr std::int32_t InnerWidth = Columns * Advance - 1; +}; + struct OverlayContent { std::vector above; std::vector frameTimes; std::vector below; + std::int32_t columns = OverlayMetrics::Columns; + bool graph = true; }; struct OverlayMemory { @@ -147,20 +163,8 @@ struct OverlayLayout { std::int32_t scale = 1; }; -struct OverlayMetrics { - static constexpr std::int32_t GlyphWidth = 5; - static constexpr std::int32_t GlyphHeight = 7; - static constexpr std::int32_t Advance = 6; - static constexpr std::int32_t LinePitch = 9; - static constexpr std::int32_t Columns = 27; - static constexpr std::int32_t Padding = 3; - static constexpr std::int32_t Margin = 4; - static constexpr std::int32_t GraphHeight = 20; - static constexpr std::int32_t GraphGap = 2; - static constexpr std::int32_t InnerWidth = Columns * Advance - 1; -}; - const std::array& OverlayGlyph(char character); +std::string_view OverlayFontCharacters(); std::string FormatOverlayValue(double value); std::string FormatOverlayRate(double value); std::string FormatOverlayCount(double value); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index ca74a794d9..86fa7ec150 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -69,19 +69,20 @@ const OverlaySettings& settings() { return parsed; } -std::int32_t panelWidth(std::int32_t scale) { - return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale; +std::int32_t panelWidth(std::int32_t columns, std::int32_t scale) { + return (columns * OverlayMetrics::Advance - 1 + 2 * OverlayMetrics::Padding) * scale; } std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) { const auto lines = static_cast(content.above.size() + content.below.size()); const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight; - return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale; + const auto graph = content.graph ? OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap : 0; + return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + graph - glyphGap) * scale; } bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) { const auto margins = 2 * OverlayMetrics::Margin * scale; - return panelWidth(scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); + return panelWidth(content.columns, scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); } float srgbDecode(float value) { @@ -289,6 +290,10 @@ const std::array& OverlayGlyph(char c static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c}; static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; + static constexpr Glyph percent{0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03}; + static constexpr Glyph plus{0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00}; + static constexpr Glyph minus{0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00}; + static constexpr Glyph underscore{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f}; static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e}; static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e}; @@ -298,12 +303,14 @@ const std::array& OverlayGlyph(char c static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11}; static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph j{0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0c}; static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}; static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f}; static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph q{0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d}; static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; @@ -311,12 +318,18 @@ const std::array& OverlayGlyph(char c static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a}; static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; + static constexpr Glyph y{0x11, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x04}; + static constexpr Glyph z{0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f}; if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; switch (character) { case ' ': return space; case '.': return period; case ':': return colon; case '/': return slash; + case '%': return percent; + case '+': return plus; + case '-': return minus; + case '_': return underscore; case 'A': return a; case 'B': return b; case 'C': return c; @@ -326,12 +339,14 @@ const std::array& OverlayGlyph(char c case 'G': return g; case 'H': return h; case 'I': return i; + case 'J': return j; case 'K': return k; case 'L': return l; case 'M': return m; case 'N': return n; case 'O': return o; case 'P': return p; + case 'Q': return q; case 'R': return r; case 'S': return s; case 'T': return t; @@ -339,10 +354,16 @@ const std::array& OverlayGlyph(char c case 'V': return v; case 'W': return w; case 'X': return x; + case 'Y': return y; + case 'Z': return z; default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); } } +std::string_view OverlayFontCharacters() { + return " .:/%+-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +} + std::string FormatOverlayValue(double value) { const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0; char text[16]; @@ -393,7 +414,7 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); }; for (const std::string_view separator : {" / ", "/"}) { auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window); - if (line.size() > static_cast(OverlayMetrics::Columns)) continue; + if (line.size() > static_cast(content.columns)) continue; content.below.push_back(std::move(line)); break; } @@ -448,10 +469,12 @@ std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) { Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination"); Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames"); + Graphics::Require(content.columns > 0, "the performance overlay panel needs at least one column"); + Graphics::Require(content.graph ? content.columns * OverlayMetrics::Advance - 1 >= static_cast(OverlayStatistics::FrameHistory) : content.frameTimes.empty(), "the performance overlay graph needs a graph row as wide as its 120 frames"); OverlayLayout layout; const auto scale = OverlayScale(extent, content); layout.scale = scale; - const auto width = panelWidth(scale); + const auto width = panelWidth(content.columns, scale); const auto height = panelHeight(content, scale); const auto margin = OverlayMetrics::Margin * scale; const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight; @@ -461,7 +484,7 @@ OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, Ov layout.bounds = {left, top, width, height, static_cast(OverlayColor::Background)}; layout.rects.push_back(layout.bounds); const auto text = [&](const std::string& line, std::int32_t row) { - Graphics::Require(line.size() <= static_cast(OverlayMetrics::Columns), "a performance overlay line is wider than the panel"); + Graphics::Require(line.size() <= static_cast(content.columns), "a performance overlay line is wider than the panel"); for (std::size_t column = 0; column < line.size(); ++column) { const auto& glyph = OverlayGlyph(line[column]); const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance) * scale; @@ -487,7 +510,7 @@ OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, Ov row += OverlayMetrics::LinePitch; } const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale; - if (!content.frameTimes.empty()) { + if (content.graph && !content.frameTimes.empty()) { double maximum = 0; double sum = 0; for (const auto time : content.frameTimes) { @@ -501,7 +524,7 @@ OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, Ov layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast(i)) * scale, graphBottom - pixels, scale, pixels, static_cast(OverlayColor::Graph)}); } } - row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + if (content.graph) row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; for (const auto& line : content.below) { text(line, row); row += OverlayMetrics::LinePitch; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index b829a08c01..4f69924f69 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -431,6 +432,67 @@ void fullPanelLayoutTests() { } } +void fontTests() { + const auto characters = AgcDriver::OverlayFontCharacters(); + for (const char character : characters) { + const auto& glyph = AgcDriver::OverlayGlyph(character); + require(std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + character + "' is wider than 5 pixels"); + require(character == ' ' || std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }), std::string("the glyph of '") + character + "' is empty"); + for (const char other : characters) { + if (other != character) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + character + "' and '" + other + "' have the same glyph"); + } + } + for (const char letter : std::string("JQYZ%+-_")) require(characters.find(letter) != std::string_view::npos, std::string("the font lists no '") + letter + "'"); + for (const char missing : std::string("qjy(>,#")) { + bool refused = false; + try { + static_cast(AgcDriver::OverlayGlyph(missing)); + } catch (const std::exception&) { + refused = true; + } + require(refused && characters.find(missing) == std::string_view::npos, std::string("'") + missing + "' is drawn or listed although the font has no glyph for it"); + } +} + +void widePanelTests() { + OverlayContent wide; + wide.columns = 48; + wide.graph = false; + for (int i = 0; i < 20; ++i) wide.below.push_back(std::string(48, i % 2 == 0 ? 'W' : 'Z')); + const VkExtent2D small{1280, 720}; + const auto layout = AgcDriver::LayOutOverlay(wide, small, OverlayCorner::TopLeft); + const auto s = layout.scale; + require(layout.bounds.width == (48 * OverlayMetrics::Advance - 1 + 2 * OverlayMetrics::Padding) * s, "a 48-column panel is " + std::to_string(layout.bounds.width) + " pixels wide at scale " + std::to_string(s)); + require(layout.bounds.height == (2 * OverlayMetrics::Padding + 20 * OverlayMetrics::LinePitch - (OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight)) * s, "a panel without a graph keeps the graph's rows"); + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + for (std::size_t line = 0; line < wide.below.size(); ++line) requireLineDrawn(layout, text, wide.below[line], OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch); + for (const auto& rect : layout.rects) require(rect.color != static_cast(AgcDriver::OverlayColor::Graph), "a panel without a graph draws graph bars"); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{3840, 2160}, 5}, {{320, 180}, 1}}; + for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, wide) == scale, "a 48-column panel at " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " is drawn at scale " + std::to_string(AgcDriver::OverlayScale(extent, wide)) + ", not " + std::to_string(scale)); + auto widest = wide; + widest.columns = 140; + require(AgcDriver::OverlayScale({3840, 2160}, widest) == 4, "a 140-column panel at 3840x2160 is not shrunk from scale 5 to 4"); + const auto refused = [](const OverlayContent& content) { + try { + static_cast(AgcDriver::LayOutOverlay(content, {1920, 1080}, OverlayCorner::TopLeft)); + } catch (const std::exception&) { + return true; + } + return false; + }; + auto longer = wide; + longer.below.push_back(std::string(49, 'W')); + require(refused(longer), "a line longer than the panel's columns was drawn"); + auto graphless = wide; + graphless.frameTimes = {16.0}; + require(refused(graphless), "frame times were laid out on a panel without a graph"); + OverlayContent cramped{{"FPS: 60.0"}, {16.0}, {}}; + cramped.columns = 20; + require(refused(cramped), "a graph of 120 frames was laid out on a 20-column panel"); + cramped.columns = 21; + require(!refused(cramped), "a 21-column panel cannot hold the graph"); +} + void sampleTests() { require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled"); require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second"); @@ -489,6 +551,8 @@ int main() { countFormatTests(); detailComposeTests(); fullPanelLayoutTests(); + fontTests(); + widePanelTests(); sampleTests(); costReport(); std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n";