ui: frame the settings menu to the letterboxed game image

This commit is contained in:
mchughalex 2026-07-17 06:01:59 +01:00
parent 6c311391fa
commit 9565cddfbb
4 changed files with 64 additions and 7 deletions

View file

@ -61,6 +61,8 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
void AddDialog(ImGuiDialog* dialog); void AddDialog(ImGuiDialog* dialog);
void RemoveDialog(ImGuiDialog* dialog); void RemoveDialog(ImGuiDialog* dialog);
Presenter* presenter() const { return presenter_; }
// SetPresenter may be called from the destructor. // SetPresenter may be called from the destructor.
void SetPresenter(Presenter* new_presenter); void SetPresenter(Presenter* new_presenter);
void SetImmediateDrawer(ImmediateDrawer* new_immediate_drawer); void SetImmediateDrawer(ImmediateDrawer* new_immediate_drawer);

View file

@ -729,6 +729,27 @@ class Presenter {
return width_out && height_out; return width_out && height_out;
} }
// Final letterboxed rectangle (render-target pixels) the guest output was
// last painted into, cached by GetGuestOutputPaintFlow. width/height are 0
// until the first guest output paint. For UI-thread consumers that want to
// frame themselves to the game image rather than the whole window (e.g. the
// settings overlay on non-16:9 displays).
struct GuestOutputPaintRect {
int32_t x = 0;
int32_t y = 0;
uint32_t width = 0;
uint32_t height = 0;
};
GuestOutputPaintRect GetLastGuestOutputPaintRect() const {
const uint64_t packed = last_guest_output_paint_rect_.load(std::memory_order_relaxed);
GuestOutputPaintRect rect;
rect.x = int32_t(int16_t(packed & 0xFFFF));
rect.y = int32_t(int16_t((packed >> 16) & 0xFFFF));
rect.width = uint32_t(uint16_t((packed >> 32) & 0xFFFF));
rect.height = uint32_t(uint16_t((packed >> 48) & 0xFFFF));
return rect;
}
protected: protected:
// is_8bpc_out_ref is where to write whether the source actually has no more // is_8bpc_out_ref is where to write whether the source actually has no more
// than 8 bits of precision per channel (though the image provided by the // than 8 bits of precision per channel (though the image provided by the
@ -1035,6 +1056,10 @@ class Presenter {
// high frame rates. // high frame rates.
static constexpr size_t kGuestFrameTimestampCount = 512; static constexpr size_t kGuestFrameTimestampCount = 512;
std::atomic<bool> guest_frame_stats_enabled_{false}; std::atomic<bool> guest_frame_stats_enabled_{false};
// Packed x/y/width/height as 4x16-bit, written by GetGuestOutputPaintFlow
// (paint thread, const method - hence mutable), read from the UI thread via
// GetLastGuestOutputPaintRect.
mutable std::atomic<uint64_t> last_guest_output_paint_rect_{0};
mutable std::mutex guest_frame_stats_mutex_; mutable std::mutex guest_frame_stats_mutex_;
std::array<std::chrono::steady_clock::time_point, kGuestFrameTimestampCount> std::array<std::chrono::steady_clock::time_point, kGuestFrameTimestampCount>
guest_frame_timestamps_{}; guest_frame_timestamps_{};

View file

@ -26,6 +26,7 @@
#include <imgui.h> #include <imgui.h>
#include <rex/cvar.h> #include <rex/cvar.h>
#include <rex/logging.h> #include <rex/logging.h>
#include <rex/ui/presenter.h>
#include <toml++/toml.hpp> #include <toml++/toml.hpp>
namespace rex::ui { namespace rex::ui {
@ -1683,8 +1684,26 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
? imgui_drawer()->ui_font_semibold_on_light() ? imgui_drawer()->ui_font_semibold_on_light()
: bold; : bold;
// Layout frame: the letterboxed game image when the presenter reports one,
// the full window otherwise (boot, no guest output yet). On non-16:9
// displays the game letterboxes to 16:9 while the window is taller/wider -
// scaling and anchoring the menu to the window made it read visibly larger
// than on a 16:9 screen (user report, 16:10 Mac). On 16:9 displays the
// rectangle equals the window and nothing changes. The blur/backdrop wash
// still covers the whole window.
ImVec2 frame_pos(0.0f, 0.0f);
ImVec2 frame_size = io.DisplaySize;
if (Presenter* presenter = imgui_drawer()->presenter()) {
const Presenter::GuestOutputPaintRect rect = presenter->GetLastGuestOutputPaintRect();
if (rect.width > 0 && rect.height > 0) {
frame_pos = ImVec2(float(rect.x), float(rect.y));
frame_size = ImVec2(float(rect.width), float(rect.height));
}
}
const float frame_bottom = frame_pos.y + frame_size.y;
// Uniform scale: design metrics are authored for 1080p. // Uniform scale: design metrics are authored for 1080p.
const float s = std::clamp(io.DisplaySize.y / 1080.0f, 0.6f, 3.0f); const float s = std::clamp(frame_size.y / 1080.0f, 0.6f, 3.0f);
// Quantize font sizes so the EM (size * upm / (hhea ascent - descent) - // Quantize font sizes so the EM (size * upm / (hhea ascent - descent) -
// the browser's font-size) lands on whole pixels. A fractional em renders // the browser's font-size) lands on whole pixels. A fractional em renders
@ -1898,8 +1917,8 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
// ---- Layout ---- // ---- Layout ----
// Positional metrics are snapped to whole pixels (see Snap). // Positional metrics are snapped to whole pixels (see Snap).
const float margin_x = Snap(std::max(56.0f * s, io.DisplaySize.x * 0.05f)); const float margin_x = Snap(std::max(56.0f * s, frame_size.x * 0.05f));
const float title_y = Snap(io.DisplaySize.y * 0.13f); const float title_y = Snap(frame_pos.y + frame_size.y * 0.13f);
const float title_size = font_px(42.0f * s); const float title_size = font_px(42.0f * s);
const float columns_y = Snap(title_y + 74.0f * s); const float columns_y = Snap(title_y + 74.0f * s);
// Rail and description columns are equal-width. // Rail and description columns are equal-width.
@ -1907,13 +1926,14 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
const float desc_w = Snap(300.0f * s); const float desc_w = Snap(300.0f * s);
const float col_gap = Snap(14.0f * s); const float col_gap = Snap(14.0f * s);
const float footer_h = Snap(96.0f * s); const float footer_h = Snap(96.0f * s);
const float content_bottom = Snap(io.DisplaySize.y - footer_h - 18.0f * s); const float content_bottom = Snap(frame_bottom - footer_h - 18.0f * s);
float content_w = io.DisplaySize.x - 2.0f * margin_x - rail_w - desc_w - 2.0f * col_gap; float content_w = frame_size.x - 2.0f * margin_x - rail_w - desc_w - 2.0f * col_gap;
content_w = Snap(std::clamp(content_w, 300.0f * s, 800.0f * s)); content_w = Snap(std::clamp(content_w, 300.0f * s, 800.0f * s));
// The column widths are capped, so on wide displays the block would hug the // The column widths are capped, so on wide displays the block would hug the
// left margin - center the whole menu instead. // left margin - center the whole menu instead.
const float menu_total_w = rail_w + content_w + desc_w + 2.0f * col_gap; const float menu_total_w = rail_w + content_w + desc_w + 2.0f * col_gap;
const float rail_x = Snap(std::max(margin_x, (io.DisplaySize.x - menu_total_w) * 0.5f)); const float rail_x =
Snap(frame_pos.x + std::max(margin_x, (frame_size.x - menu_total_w) * 0.5f));
const float content_x = rail_x + rail_w + col_gap; const float content_x = rail_x + rail_w + col_gap;
const float desc_x = content_x + content_w + col_gap; const float desc_x = content_x + content_w + col_gap;
const float menu_right = desc_x + desc_w; const float menu_right = desc_x + desc_w;
@ -2483,7 +2503,7 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
// ---- Footer button legend ---- // ---- Footer button legend ----
{ {
float legend_y = Snap(io.DisplaySize.y - footer_h + 14.0f * s); float legend_y = Snap(frame_bottom - footer_h + 14.0f * s);
std::vector<LegendGlyph> glyphs; std::vector<LegendGlyph> glyphs;
if (pad_active_) { if (pad_active_) {
glyphs.push_back({"A", "Select", true}); glyphs.push_back({"A", "Select", true});

View file

@ -1397,6 +1397,16 @@ Presenter::GuestOutputPaintFlow Presenter::GetGuestOutputPaintFlow(
} }
} }
// Cache the final letterboxed rectangle for UI-thread consumers
// (GetLastGuestOutputPaintRect). Values fit 16 bits comfortably (8K wide is
// 7680); reached only on the successful path, so degenerate flows keep the
// previous rectangle.
last_guest_output_paint_rect_.store(
uint64_t(uint16_t(int16_t(flow.output_x))) |
(uint64_t(uint16_t(int16_t(flow.output_y))) << 16) |
(uint64_t(uint16_t(output_width)) << 32) | (uint64_t(uint16_t(output_height)) << 48),
std::memory_order_relaxed);
return flow; return flow;
} }