Skip to content

Commit b9651a4

Browse files
committed
perf: reduce redundant work and memory allocations across capture pipeline
- config: eliminate double hash lookup in Config::GetValue by reusing the iterator from find() instead of calling at() twice per override - capture: remove file-scope cap_portal global; merge its fields into portal_state_t so portal capture state is stack-local and the function becomes re-entrant - tool: cache resolved font path in inputs_results_t to avoid recursive filesystem scan on every rendered frame per text annotation; invalidate on StartWindow() and when the user edits the font field - tool: eliminate per-frame heap allocation in draw_pencil by casting point_t* directly to ImVec2* (layout-compatible structs); guarded by static_assert - main: fix fake move of trivially-copyable uint8_t in IPC image handler; replace move_iterator range-construction with erase + move of the existing vector, avoiding an unnecessary ~8MB copy for 1080p captures - util/clipboard: deduplicate svpng encoding logic into a shared encode_to_png() helper, removing duplicated macro boilerplate from clipboard.cpp
1 parent accbd75 commit b9651a4

9 files changed

Lines changed: 90 additions & 97 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ execute_process(
100100
)
101101

102102
target_compile_definitions(${PROJECT_NAME} PRIVATE $<$<CONFIG:Debug>:DEBUG=1>)
103+
target_compile_definitions(${PROJECT_NAME} PRIVATE
104+
FMT_HEADER_ONLY=0
105+
)
103106
target_compile_options(
104107
${PROJECT_NAME}
105108
PRIVATE

include/config.hpp

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,19 +132,19 @@ class Config
132132
{
133133
const auto& overridePos = m_overrides.find(value.data());
134134

135-
// user wants a bool (overridable), we found an override matching the name, and the override is a bool.
136-
if constexpr (std::is_same<T, bool>())
137-
if (overridePos != m_overrides.end() && m_overrides.at(value.data()).value_type == ValueType::kBool)
138-
return m_overrides.at(value.data()).bool_value;
139-
140-
// user wants a str (overridable), we found an override matching the name, and the override is a str.
141-
if constexpr (std::is_same<T, std::string>())
142-
if (overridePos != m_overrides.end() && m_overrides.at(value.data()).value_type == ValueType::kString)
143-
return m_overrides.at(value.data()).string_value;
144-
145-
if constexpr (std::is_same<T, int>())
146-
if (overridePos != m_overrides.end() && m_overrides.at(value.data()).value_type == ValueType::kInt)
147-
return m_overrides.at(value.data()).int_value;
135+
if (overridePos != m_overrides.end())
136+
{
137+
const auto& ov = overridePos->second;
138+
if constexpr (std::is_same<T, bool>())
139+
if (ov.value_type == ValueType::kBool)
140+
return ov.bool_value;
141+
if constexpr (std::is_same<T, std::string>())
142+
if (ov.value_type == ValueType::kString)
143+
return ov.string_value;
144+
if constexpr (std::is_same<T, int>())
145+
if (ov.value_type == ValueType::kInt)
146+
return ov.int_value;
147+
}
148148

149149
const std::optional<T>& ret = this->m_tbl.at_path(value).value<T>();
150150
if constexpr (toml::is_string<T>)

include/screenshot_tool.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ struct annotation_t
109109
class ScreenshotTool
110110
{
111111
public:
112-
ScreenshotTool() : m_io(dummy), m_inputs{ g_config->File.ocr_path, g_config->File.ocr_model, {}, "", {}, "" } {}
112+
ScreenshotTool() : m_io(dummy), m_inputs{ g_config->File.ocr_path, g_config->File.ocr_model, {}, "", {}, "", "" } {}
113113

114114
Result<> Start();
115115
Result<> StartWindow();
@@ -175,6 +175,7 @@ class ScreenshotTool
175175
zbar_result_t zbar_scan_result;
176176

177177
std::string ann_font;
178+
std::string resolved_ann_font_path;
178179
};
179180

180181
ImGuiIO& m_io;

include/util.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ static inline const std::string version_infos = fmt::format(
160160
#ifdef __linux__
161161
std::vector<uint8_t> ximage_to_rgba(XImage* image, int width, int height);
162162
#endif
163+
std::vector<uint8_t> encode_to_png(const capture_result_t& cap);
163164

164165
std::string replace_str(std::string& str, const std::string_view from, const std::string_view to);
165166
std::string select_image();

src/clipboard.cpp

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@
77
#include "clip/clip.h"
88
#include "socket.hpp"
99

10-
#define SVPNG_LINKAGE inline
11-
#define SVPNG_OUTPUT std::vector<uint8_t>* output
12-
#define SVPNG_PUT(u) output->push_back(static_cast<uint8_t>(u))
13-
#pragma GCC diagnostic push
14-
#pragma GCC diagnostic ignored "-Wfree-nonheap-object"
15-
#include "svpng.h"
16-
#pragma GCC diagnostic pop
17-
1810
// used to track the wl-copy process
1911
static int wlcopy_pid = -1;
2012

@@ -113,10 +105,7 @@ Result<> Clipboard::CopyImage(const capture_result_t& cap)
113105

114106
if (m_session == SessionType::Wayland)
115107
{
116-
std::vector<uint8_t> png;
117-
png.reserve(static_cast<size_t>(cap.w) * cap.h * 4);
118-
119-
svpng(&png, cap.w, cap.h, cap.view().data(), 1);
108+
const std::vector<uint8_t>& png = encode_to_png(cap);
120109

121110
const Result<int>& res = start_wlcopy("image/png");
122111
if (!res.ok())

src/main.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,9 +488,8 @@ int main(int argc, char* argv[])
488488
continue;
489489

490490
// Strip the 8-byte header
491-
std::vector<uint8_t> pixel_data(std::make_move_iterator(payload.begin() + 8),
492-
std::make_move_iterator(payload.end()));
493-
capture_result_t cap{ std::move(pixel_data), w, h };
491+
payload.erase(payload.begin(), payload.begin() + 8);
492+
capture_result_t cap{ std::move(payload), w, h };
494493
{
495494
std::lock_guard lk(mtx);
496495
pending_image = std::move(cap);

src/screen_capture.cpp

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -284,25 +284,22 @@ Result<capture_result_t> capture_full_screen_wayland()
284284
return Ok(std::move(result));
285285
}
286286

287-
struct portal_capture_t
288-
{
289-
std::string png_path;
290-
std::string error_msg;
291-
capture_result_t cap;
292-
} cap_portal;
293-
294287
struct portal_state_t
295288
{
296289
GMainLoop* loop;
297290
guint subscription_id;
298291
guint timeout_id; // tracked so we can cancel it before the stack frame is gone
292+
293+
std::string png_path;
294+
std::string error_msg;
295+
capture_result_t cap;
299296
};
300297

301298
static gboolean on_timeout(gpointer user_data)
302299
{
303-
portal_state_t* st = reinterpret_cast<portal_state_t*>(user_data);
304-
cap_portal.error_msg = "Timed out waiting for portal response (is xdg-desktop-portal running?)";
305-
st->timeout_id = 0; // GLib will remove the source; mark it gone so cleanup skips it
300+
portal_state_t* st = reinterpret_cast<portal_state_t*>(user_data);
301+
st->error_msg = "Timed out waiting for portal response (is xdg-desktop-portal running?)";
302+
st->timeout_id = 0; // GLib will remove the source; mark it gone so cleanup skips it
306303
g_main_loop_quit(st->loop);
307304
return G_SOURCE_REMOVE;
308305
}
@@ -343,9 +340,9 @@ static void on_response(GDBusConnection* conn,
343340
g_variant_get(parameters, "(u@a{sv})", &response, &results);
344341

345342
if (response != 0)
346-
cap_portal.error_msg = fmt::format("Cancelled or failed (response={})", (unsigned)response);
343+
st->error_msg = fmt::format("Cancelled or failed (response={})", (unsigned)response);
347344
else if (!(g_variant_lookup(results, "uri", "&s", &uri) && uri))
348-
cap_portal.error_msg = "Success, but portal returned no uri";
345+
st->error_msg = "Success, but portal returned no uri";
349346

350347
if (results)
351348
g_variant_unref(results);
@@ -359,31 +356,30 @@ static void on_response(GDBusConnection* conn,
359356
const Result<std::string>& res = uri_to_path(uri);
360357
if (res.ok())
361358
{
362-
cap_portal.png_path = res.get();
363-
cap_portal.error_msg = "";
359+
st->png_path = res.get();
360+
st->error_msg = "";
364361
}
365362
else
366363
{
367-
cap_portal.png_path = "";
368-
cap_portal.error_msg = res.error_v();
364+
st->png_path = "";
365+
st->error_msg = res.error_v();
369366
}
370367

371368
g_main_loop_quit(st->loop);
372369
}
373370

374371
Result<capture_result_t> capture_full_screen_portal()
375372
{
376-
cap_portal = {};
373+
portal_state_t st{};
377374
warn("Fallback to portal capture");
378375

379376
GError* error = nullptr;
380377
GDBusConnection* bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
381378
if (!bus)
382379
{
383-
cap_portal.error_msg =
384-
"Failed to connect to session bus: " + std::string(error->message ? error->message : "Unknown");
380+
st.error_msg = "Failed to connect to session bus: " + std::string(error->message ? error->message : "Unknown");
385381
g_error_free(error);
386-
return Err(cap_portal.error_msg);
382+
return Err(st.error_msg);
387383
}
388384

389385
// Call Screenshot portal synchronously so we can fail loudly if the service isn't there.
@@ -402,10 +398,10 @@ Result<capture_result_t> capture_full_screen_portal()
402398

403399
if (!reply)
404400
{
405-
cap_portal.error_msg = "Portal call failed: " + std::string(error->message ? error->message : "Unknown");
401+
st.error_msg = "Portal call failed: " + std::string(error->message ? error->message : "Unknown");
406402
g_error_free(error);
407403
g_object_unref(bus);
408-
return Err(cap_portal.error_msg);
404+
return Err(st.error_msg);
409405
}
410406

411407
const char* request_path = nullptr;
@@ -418,7 +414,6 @@ Result<capture_result_t> capture_full_screen_portal()
418414
return Err("Portal returned an empty request handle");
419415
}
420416

421-
portal_state_t st;
422417
st.loop = g_main_loop_new(nullptr, FALSE);
423418
st.subscription_id = 0;
424419
st.timeout_id = 0;
@@ -457,73 +452,66 @@ Result<capture_result_t> capture_full_screen_portal()
457452
g_main_loop_unref(st.loop);
458453
g_object_unref(bus);
459454

460-
if (cap_portal.png_path.empty())
455+
if (st.png_path.empty())
461456
{
462-
if (cap_portal.error_msg.empty())
457+
if (st.error_msg.empty())
463458
return Err("Failed to retrieve uri path to screenshot PNG");
464-
return Err(cap_portal.error_msg);
459+
return Err(st.error_msg);
465460
}
466461

467462
int w = 0, h = 0, comp = 0;
468-
uint8_t* rgba = stbi_load(cap_portal.png_path.c_str(), &w, &h, &comp, STBI_rgb_alpha);
463+
uint8_t* rgba = stbi_load(st.png_path.c_str(), &w, &h, &comp, STBI_rgb_alpha);
469464

470465
if (!rgba)
471466
{
472467
const char* reason = stbi_failure_reason();
473-
unlink(cap_portal.png_path.c_str());
468+
unlink(st.png_path.c_str());
474469
return Err("Failed to read PNG data: " + std::string(reason ? reason : "Unknown"));
475470
}
476471

477-
cap_portal.cap.w = w;
478-
cap_portal.cap.h = h;
479-
cap_portal.cap.data.assign(rgba, rgba + (static_cast<size_t>(w) * h * 4));
472+
st.cap.w = w;
473+
st.cap.h = h;
474+
st.cap.data.assign(rgba, rgba + (static_cast<size_t>(w) * h * 4));
480475
stbi_image_free(rgba);
481476

482477
// The portal backend (on KDE mostly) writes a permanent file to ~/Pictures named "Screenshot_*.png".
483478
// Delete it now that we have the pixels in memory.
484-
unlink(cap_portal.png_path.c_str());
479+
unlink(st.png_path.c_str());
485480

486481
// The portal always captures the full virtual desktop on multi-monitor
487482
// setups. Crop down to the monitor that contains the cursor.
488483
// XRandR works on native X11 and on KDE/GNOME Wayland via XWayland.
489484
{
490485
int mx = 0, my = 0, mw = 0, mh = 0;
491-
if (get_cursor_monitor_xrandr(nullptr, mx, my, mw, mh) && (cap_portal.cap.w > mw || cap_portal.cap.h > mh))
486+
if (get_cursor_monitor_xrandr(nullptr, mx, my, mw, mh) && (st.cap.w > mw || st.cap.h > mh))
492487
{
493-
debug("Portal: cropping {}x{} capture to monitor {}x{}+{}+{}",
494-
cap_portal.cap.w,
495-
cap_portal.cap.h,
496-
mw,
497-
mh,
498-
mx,
499-
my);
488+
debug("Portal: cropping {}x{} capture to monitor {}x{}+{}+{}", st.cap.w, st.cap.h, mw, mh, mx, my);
500489

501490
const int x0 = std::max(0, mx);
502491
const int y0 = std::max(0, my);
503-
const int x1 = std::min(cap_portal.cap.w, mx + mw);
504-
const int y1 = std::min(cap_portal.cap.h, my + mh);
492+
const int x1 = std::min(st.cap.w, mx + mw);
493+
const int y1 = std::min(st.cap.h, my + mh);
505494
const int new_w = x1 - x0;
506495
const int new_h = y1 - y0;
507496

508497
if (new_w > 0 && new_h > 0)
509498
{
510-
const int src_stride = cap_portal.cap.w;
499+
const int src_stride = st.cap.w;
511500
std::vector<uint8_t> cropped(static_cast<size_t>(new_w) * new_h * 4);
512501
for (int row = 0; row < new_h; ++row)
513502
{
514-
const uint8_t* src =
515-
cap_portal.cap.data.data() + (static_cast<size_t>(y0 + row) * src_stride + x0) * 4;
516-
uint8_t* dst = cropped.data() + static_cast<size_t>(row) * new_w * 4;
503+
const uint8_t* src = st.cap.data.data() + (static_cast<size_t>(y0 + row) * src_stride + x0) * 4;
504+
uint8_t* dst = cropped.data() + static_cast<size_t>(row) * new_w * 4;
517505
std::memcpy(dst, src, static_cast<size_t>(new_w) * 4);
518506
}
519-
cap_portal.cap.data = std::move(cropped);
520-
cap_portal.cap.w = new_w;
521-
cap_portal.cap.h = new_h;
507+
st.cap.data = std::move(cropped);
508+
st.cap.w = new_w;
509+
st.cap.h = new_h;
522510
}
523511
}
524512
}
525513

526-
return Ok(std::move(cap_portal.cap));
514+
return Ok(std::move(st.cap));
527515
}
528516

529517
#else

src/screenshot_tool.cpp

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ Result<> ScreenshotTool::StartWindow()
230230
m_io = ImGui::GetIO();
231231
m_state = ToolState::Selecting;
232232

233-
m_inputs.ann_font = g_config->File.fonts.size() > 0 ? g_config->File.fonts[0] : "";
234-
m_show_text_tools = g_config->File.show_text_tools;
233+
m_inputs.ann_font = g_config->File.fonts.size() > 0 ? g_config->File.fonts[0] : "";
234+
m_show_text_tools = g_config->File.show_text_tools;
235+
m_inputs.resolved_ann_font_path = get_font_path(m_inputs.ann_font).string();
235236

236237
fit_to_screen(m_screenshot);
237238
RefreshOcrModels();
@@ -526,8 +527,7 @@ void ScreenshotTool::HandleAnnotationInput()
526527
if (ImGui::IsWindowAppearing())
527528
ImGui::SetKeyboardFocusHere();
528529

529-
ImFont* ann_font =
530-
CacheAndGetFont(get_font_path(m_inputs.ann_font).string(), m_current_annotation.thickness);
530+
ImFont* ann_font = CacheAndGetFont(m_inputs.resolved_ann_font_path, m_current_annotation.thickness);
531531
ImGui::PushFont(ann_font);
532532

533533
ImGui::PushStyleColor(ImGuiCol_Text, m_current_annotation.color);
@@ -1308,7 +1308,12 @@ void ScreenshotTool::DrawAnnotationToolbar()
13081308
ImGui::TextUnformatted("Font Size");
13091309
static const char* font_filters[] = { "*.ttf", "*.otf", "*.woff", "*.woff2" };
13101310
draw_input_text_file(
1311-
"Font name/path", "##font_path_ann_settings", font_filters, 4, [] {}, m_inputs.ann_font);
1311+
"Font name/path",
1312+
"##font_path_ann_settings",
1313+
font_filters,
1314+
4,
1315+
[&] { m_inputs.resolved_ann_font_path = get_font_path(m_inputs.ann_font).string(); },
1316+
m_inputs.ann_font);
13121317
}
13131318
else
13141319
{
@@ -1376,6 +1381,9 @@ void ScreenshotTool::DrawAnnotationToolbar()
13761381

13771382
void ScreenshotTool::DrawAnnotations()
13781383
{
1384+
static_assert(sizeof(point_t) == sizeof(ImVec2) && alignof(point_t) == alignof(ImVec2),
1385+
"point_t and ImVec2 layout mismatch");
1386+
13791387
ImDrawList* draw_list = ImGui::GetBackgroundDrawList();
13801388
const float dpi = m_io.DisplayFramebufferScale.x;
13811389

@@ -1385,7 +1393,7 @@ void ScreenshotTool::DrawAnnotations()
13851393

13861394
auto draw_text = [&](const annotation_t& ann, const ImVec2& p1) {
13871395
const float font_size = ann.thickness > 8.0f ? ann.thickness : ImGui::GetFontSize();
1388-
ImFont* font = CacheAndGetFont(get_font_path(m_inputs.ann_font).string(), font_size);
1396+
ImFont* font = CacheAndGetFont(m_inputs.resolved_ann_font_path, font_size);
13891397
draw_list->AddText(font, font_size, p1, ann.color, ann.text.c_str());
13901398
};
13911399

@@ -1409,11 +1417,11 @@ void ScreenshotTool::DrawAnnotations()
14091417
auto draw_pencil = [&](const annotation_t& ann, const float t) {
14101418
if (ann.points.size() > 1)
14111419
{
1412-
std::vector<ImVec2> pts;
1413-
pts.reserve(ann.points.size());
1414-
for (const auto& p : ann.points)
1415-
pts.emplace_back(p.x, p.y);
1416-
draw_list->AddPolyline(pts.data(), static_cast<int>(pts.size()), ann.color, ImDrawFlags_None, t);
1420+
draw_list->AddPolyline(reinterpret_cast<const ImVec2*>(ann.points.data()),
1421+
static_cast<int>(ann.points.size()),
1422+
ann.color,
1423+
ImDrawFlags_None,
1424+
t);
14171425
}
14181426
};
14191427

@@ -1706,7 +1714,7 @@ capture_result_t ScreenshotTool::GetFinalImage(bool is_text_tools)
17061714
break;
17071715

17081716
const float font_size = ann.thickness > 8.0f ? ann.thickness : ImGui::GetFontSize();
1709-
ImFont* font = CacheAndGetFont(get_font_path(m_inputs.ann_font).string(), font_size);
1717+
ImFont* font = CacheAndGetFont(m_inputs.resolved_ann_font_path, font_size);
17101718
if (!font || !font->OwnerAtlas)
17111719
break;
17121720

0 commit comments

Comments
 (0)