refactor(agc): take the performance overlay panel's width from its content

The panel was fixed at OverlayMetrics::Columns, 27 columns, and always
kept rows for the frame-time graph. OverlayContent now carries its
column count and whether it has a graph, and the panel's width, height,
fit test and scale come from them. ComposeOverlay still fills 27
columns with a graph, so the existing panel is drawn as before, line
for line.

LayOutOverlay refuses a panel with no columns, a graph on a panel
narrower than its 120 frames, and frame times on a panel without a
graph.

The 5x7 font gains %, +, -, _, J, Q, Y and Z, from the same HD44780
character set as its other glyphs, and OverlayFontCharacters lists
every character it can draw.

Tests: every listed character has a glyph no wider than 5 pixels,
non-empty unless it is the space, and unlike any other; characters
outside the list are refused; a 48-column panel without a graph is
laid out and scaled at 1280x720, 1920x1080, 3840x2160 and 320x180; a
140-column panel at 3840x2160 shrinks from scale 5 to 4; a too-long
line, a 20-column graph and frame times without a graph are refused.
This commit is contained in:
Yunus Shaikh authored and Adrià Franch committed 2026-10-26 20:42:15 +01:00
1 parent e0b13f2990
commit e11fb2a288
3 files changed
+113 -22

No files matched your search

@@ -14,6 +14,7 @@
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
@@ -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<std::string> above;
std::vector<double> frameTimes;
std::vector<std::string> 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<std::uint8_t, OverlayMetrics::GlyphHeight>& OverlayGlyph(char character);
std::string_view OverlayFontCharacters();
std::string FormatOverlayValue(double value);
std::string FormatOverlayRate(double value);
std::string FormatOverlayCount(double value);
@@ -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<std::int32_t>(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<std::int32_t>(extent.width) && panelHeight(content, scale) + margins <= static_cast<std::int32_t>(extent.height);
return panelWidth(content.columns, scale) + margins <= static_cast<std::int32_t>(extent.width) && panelHeight(content, scale) + margins <= static_cast<std::int32_t>(extent.height);
}
float srgbDecode(float value) {
@@ -289,6 +290,10 @@ const std::array<std::uint8_t, OverlayMetrics::GlyphHeight>& 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<std::uint8_t, OverlayMetrics::GlyphHeight>& 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<std::uint8_t, OverlayMetrics::GlyphHeight>& 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<std::size_t>(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<std::uint8_t, OverlayMetrics::GlyphHeight>& 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<std::uint8_t, OverlayMetrics::GlyphHeight>& 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<std::size_t>(OverlayMetrics::Columns)) continue;
if (line.size() > static_cast<std::size_t>(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<std::int32_t>(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<std::uint32_t>(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<std::size_t>(OverlayMetrics::Columns), "a performance overlay line is wider than the panel");
Graphics::Require(line.size() <= static_cast<std::size_t>(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<std::int32_t>(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<std::int32_t>(i)) * scale, graphBottom - pixels, scale, pixels, static_cast<std::uint32_t>(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;
@@ -10,6 +10,7 @@
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
@@ -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<void>(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<std::uint32_t>(AgcDriver::OverlayColor::Text));
for (std::size_t line = 0; line < wide.below.size(); ++line) requireLineDrawn(layout, text, wide.below[line], OverlayMetrics::Padding + static_cast<std::int32_t>(line) * OverlayMetrics::LinePitch);
for (const auto& rect : layout.rects) require(rect.color != static_cast<std::uint32_t>(AgcDriver::OverlayColor::Graph), "a panel without a graph draws graph bars");
const std::pair<VkExtent2D, std::int32_t> 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<void>(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";