From cd02ffc190d9a65a9666edd2aaae3654c777cc01 Mon Sep 17 00:00:00 2001 From: Mariana Date: Sat, 1 Nov 2025 20:25:49 +0000 Subject: [PATCH 1/4] Handle overlay config reload on live updates --- src/app/main.cpp | 18 +- src/overlay/overlay.cpp | 477 ++++++++++++++++++++++------------------ 2 files changed, 284 insertions(+), 211 deletions(-) diff --git a/src/app/main.cpp b/src/app/main.cpp index 7ae66dc..3df229a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -276,13 +276,29 @@ int main(int argc, char **argv) { } engine.shutdown(); engine.init(cfg.sound_path(), cfg.volume_percent(), cfg.audio_backend()); + overlay.refresh_from_config(cfg); + bool prev_enabled = tray_state.enabled; + bool prev_muted = tray_state.muted; + bool prev_fullscreen_pause = tray_state.fullscreen_pause; + auto prev_mode = tray_state.fps_mode; + int prev_fixed = tray_state.fps_fixed; tray_state.enabled = cfg.enabled(); tray_state.muted = cfg.mute(); tray_state.fullscreen_pause = cfg.fullscreen_pause(); + auto new_mode = cfg.fps_mode() == "fixed" ? lizard::platform::FpsMode::Fixed + : lizard::platform::FpsMode::Auto; + int new_fixed = cfg.fps_fixed(); + tray_state.fps_mode = new_mode; + tray_state.fps_fixed = new_fixed; enabled = tray_state.enabled; muted = tray_state.muted; fullscreen_pause = tray_state.fullscreen_pause; - lizard::platform::update_tray(tray_state); + bool tray_changed = tray_state.enabled != prev_enabled || tray_state.muted != prev_muted || + tray_state.fullscreen_pause != prev_fullscreen_pause || + prev_mode != new_mode || prev_fixed != new_fixed; + if (tray_changed) { + lizard::platform::update_tray(tray_state); + } update_state(); } }); diff --git a/src/overlay/overlay.cpp b/src/overlay/overlay.cpp index 80fa4bd..f9a0e9b 100644 --- a/src/overlay/overlay.cpp +++ b/src/overlay/overlay.cpp @@ -31,6 +31,7 @@ void stbi_image_free(void *); #include #include #include +#include #include #include @@ -102,6 +103,7 @@ class Overlay { void spawn_badge(float x, float y); void run(std::stop_token st); void stop(); + void refresh_from_config(const app::Config &cfg); void set_paused(bool v) { m_paused = v; } void set_fps_mode(platform::FpsMode mode) { m_fps_mode = mode; @@ -118,6 +120,24 @@ class Overlay { void update(float dt); void render(); void update_frame_interval(); + void apply_pending_config(); + bool load_atlas_from_path(const std::optional &emoji_path); + void build_selector(const std::vector &emoji, + const std::unordered_map &emoji_weighted); + static std::optional + normalize_path(const std::optional &path); + + struct PendingConfig { + std::string spawn_strategy; + int badge_min_px = 60; + int badge_max_px = 108; + int badges_per_second_max = 12; + std::string fps_mode; + int fps_fixed = 60; + std::optional emoji_atlas; + std::vector emoji; + std::unordered_map emoji_weighted; + }; platform::Window m_window{}; std::vector m_badges; @@ -146,6 +166,10 @@ class Overlay { platform::FpsMode m_fps_mode = platform::FpsMode::Auto; int m_fps_fixed = 60; std::atomic m_frame_interval_us{1000000 / 60}; + std::optional m_current_emoji_path; + std::mutex m_pending_mutex; + std::optional m_pending_config; + std::atomic m_has_pending_config{false}; }; void Overlay::update_frame_interval() { @@ -209,107 +233,12 @@ bool Overlay::init(const app::Config &cfg, std::optional m_badge_max_px = cfg.badge_max_px(); m_badges_per_second_max = cfg.badges_per_second_max(); update_frame_interval(); -#ifdef LIZARD_TEST - if (emoji_path && emoji_path->extension() == ".png") { - int w, h, channels; - unsigned char *pixels = stbi_load(emoji_path->string().c_str(), &w, &h, &channels, 4); - if (!pixels) { -#ifdef LIZARD_TEST - g_overlay_log_called = true; -#endif - spdlog::error("Failed to load emoji atlas {}: {}", emoji_path->string(), - stbi_failure_reason()); - return false; - } - stbi_image_free(pixels); - } - - std::ifstream atlas_file; - std::istringstream atlas_default(R"({ - "sprites": { - "🦎": { "u0": 0.0, "v0": 0.0, "u1": 0.5, "v1": 0.5 }, - "🐍": { "u0": 0.5, "v0": 0.0, "u1": 1.0, "v1": 0.5 }, - "🐢": { "u0": 0.0, "v0": 0.5, "u1": 0.5, "v1": 1.0 } - } -})"); - std::istream *atlas = nullptr; - if (emoji_path) { - if (emoji_path->extension() == ".json") { - atlas_file.open(*emoji_path); - if (atlas_file.is_open()) { - atlas = &atlas_file; - } - } else { - std::filesystem::path json_path = *emoji_path; - json_path += ".json"; - if (std::filesystem::exists(json_path)) { - atlas_file.open(json_path); - if (atlas_file.is_open()) { - atlas = &atlas_file; - } - } - if (!atlas) { - json_path = emoji_path->parent_path() / "emoji_atlas.json"; - if (std::filesystem::exists(json_path)) { - atlas_file.open(json_path); - if (atlas_file.is_open()) { - atlas = &atlas_file; - } - } - } - } - } - if (!atlas) { - atlas = &atlas_default; - } - try { - json j; - *atlas >> j; - if (j.contains("sprites") && j["sprites"].is_object()) { - for (const auto &[emoji, s] : j["sprites"].items()) { - Sprite sp{}; - sp.u0 = s.value("u0", 0.0f); - sp.v0 = s.value("v0", 0.0f); - sp.u1 = s.value("u1", 1.0f); - sp.v1 = s.value("v1", 1.0f); - m_sprite_lookup[emoji] = static_cast(m_sprites.size()); - m_sprites.push_back(sp); - } - } - } catch (const std::exception &e) { - spdlog::error("Failed to parse emoji atlas: {}", e.what()); - } - if (m_sprites.empty()) { - m_sprites.push_back({0.0f, 0.0f, 1.0f, 1.0f}); - m_sprite_lookup["🦎"] = 0; - } - std::vector weights; - if (!cfg.emoji_weighted().empty()) { - for (const auto &[emoji, weight] : cfg.emoji_weighted()) { - auto it = m_sprite_lookup.find(emoji); - if (it != m_sprite_lookup.end()) { - m_selector_indices.push_back(it->second); - weights.push_back(weight); - } - } - } else { - for (const auto &emoji : cfg.emoji()) { - auto it = m_sprite_lookup.find(emoji); - if (it != m_sprite_lookup.end()) { - m_selector_indices.push_back(it->second); - weights.push_back(1.0); - } - } - } - if (m_selector_indices.empty()) { - for (int i = 0; i < static_cast(m_sprites.size()); ++i) { - m_selector_indices.push_back(i); - weights.push_back(1.0); - } + auto normalized_path = normalize_path(emoji_path); +#ifdef LIZARD_TEST + if (!load_atlas_from_path(normalized_path)) { + return false; } - m_selector = std::discrete_distribution<>(weights.begin(), weights.end()); - return true; #else platform::WindowDesc desc{}; #ifdef _WIN32 @@ -374,120 +303,12 @@ bool Overlay::init(const app::Config &cfg, std::optional return false; } - // Load atlas - int w, h, channels; - unsigned char *pixels = nullptr; - if (emoji_path && std::filesystem::exists(*emoji_path)) { - pixels = stbi_load(emoji_path->string().c_str(), &w, &h, &channels, 4); - } else { - pixels = stbi_load_from_memory(lizard::assets::lizard_regular_png, - lizard::assets::lizard_regular_png_len, &w, &h, &channels, 4); - } - if (!pixels) { - if (emoji_path && std::filesystem::exists(*emoji_path)) { - spdlog::error("Failed to load emoji atlas {}: {}", emoji_path->string(), - stbi_failure_reason()); - } else { - spdlog::error("Failed to load embedded emoji atlas: {}", stbi_failure_reason()); - } + if (!load_atlas_from_path(normalized_path)) { return false; } +#endif - // Pre-multiply RGB by alpha - for (int i = 0; i < w * h; ++i) { - unsigned char *p = pixels + i * 4; - unsigned char a = p[3]; - p[0] = static_cast(p[0] * a / 255); - p[1] = static_cast(p[1] * a / 255); - p[2] = static_cast(p[2] * a / 255); - } - m_texture.create(); - glBindTexture(GL_TEXTURE_2D, m_texture.id); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - stbi_image_free(pixels); - - // Load sprite UVs from atlas - std::ifstream atlas_file; - std::istringstream atlas_default(R"({ - "sprites": { - "🦎": { "u0": 0.0, "v0": 0.0, "u1": 0.5, "v1": 0.5 }, - "🐍": { "u0": 0.5, "v0": 0.0, "u1": 1.0, "v1": 0.5 }, - "🐢": { "u0": 0.0, "v0": 0.5, "u1": 0.5, "v1": 1.0 } - } -})"); - std::istream *atlas = nullptr; - if (emoji_path) { - std::filesystem::path json_path = *emoji_path; - json_path += ".json"; - if (std::filesystem::exists(json_path)) { - atlas_file.open(json_path); - if (atlas_file.is_open()) { - atlas = &atlas_file; - } - } - if (!atlas) { - json_path = emoji_path->parent_path() / "emoji_atlas.json"; - if (std::filesystem::exists(json_path)) { - atlas_file.open(json_path); - if (atlas_file.is_open()) { - atlas = &atlas_file; - } - } - } - } - if (!atlas) { - atlas = &atlas_default; - } - try { - json j; - *atlas >> j; - if (j.contains("sprites") && j["sprites"].is_object()) { - for (const auto &[emoji, s] : j["sprites"].items()) { - Sprite sp{}; - sp.u0 = s.value("u0", 0.0f); - sp.v0 = s.value("v0", 0.0f); - sp.u1 = s.value("u1", 1.0f); - sp.v1 = s.value("v1", 1.0f); - m_sprite_lookup[emoji] = static_cast(m_sprites.size()); - m_sprites.push_back(sp); - } - } - } catch (const std::exception &e) { - spdlog::error("Failed to parse emoji atlas: {}", e.what()); - } - if (m_sprites.empty()) { - m_sprites.push_back({0.0f, 0.0f, 1.0f, 1.0f}); - m_sprite_lookup["🦎"] = 0; - } - - // Build selector from config - std::vector weights; - if (!cfg.emoji_weighted().empty()) { - for (const auto &[emoji, weight] : cfg.emoji_weighted()) { - auto it = m_sprite_lookup.find(emoji); - if (it != m_sprite_lookup.end()) { - m_selector_indices.push_back(it->second); - weights.push_back(weight); - } - } - } else { - for (const auto &emoji : cfg.emoji()) { - auto it = m_sprite_lookup.find(emoji); - if (it != m_sprite_lookup.end()) { - m_selector_indices.push_back(it->second); - weights.push_back(1.0); - } - } - } - if (m_selector_indices.empty()) { - for (int i = 0; i < static_cast(m_sprites.size()); ++i) { - m_selector_indices.push_back(i); - weights.push_back(1.0); - } - } - m_selector = std::discrete_distribution<>(weights.begin(), weights.end()); + build_selector(cfg.emoji(), cfg.emoji_weighted()); m_badge_capacity = 150; m_badges.reserve(m_badge_capacity); @@ -497,6 +318,7 @@ bool Overlay::init(const app::Config &cfg, std::optional spawn_badge(0.0f, 0.0f); } +#ifndef LIZARD_TEST // Geometry const float verts[] = {-0.5f, -0.5f, 0.0f, 0.0f, 0.5f, -0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 1.0f, 1.0f, -0.5f, 0.5f, 0.0f, 1.0f}; @@ -623,9 +445,241 @@ bool Overlay::init(const app::Config &cfg, std::optional glDeleteShader(vsId); glDeleteShader(fsId); +#endif m_running = true; return true; +} + +std::optional +Overlay::normalize_path(const std::optional &path) { + if (!path || path->empty()) { + return std::nullopt; + } + return path->lexically_normal(); +} + +bool Overlay::load_atlas_from_path(const std::optional &emoji_path) { + auto normalized = normalize_path(emoji_path); + std::unordered_map lookup; + std::vector sprites; + +#ifdef LIZARD_TEST + if (normalized && normalized->extension() == ".png") { + int w = 0; + int h = 0; + int channels = 0; + unsigned char *pixels = stbi_load(normalized->string().c_str(), &w, &h, &channels, 4); + if (!pixels) { + g_overlay_log_called = true; + spdlog::error("Failed to load emoji atlas {}: {}", normalized->string(), + stbi_failure_reason()); + return false; + } + stbi_image_free(pixels); + } +#else + int w = 0; + int h = 0; + int channels = 0; + unsigned char *pixels = nullptr; + if (normalized) { + pixels = stbi_load(normalized->string().c_str(), &w, &h, &channels, 4); + } else { + pixels = stbi_load_from_memory(lizard::assets::lizard_regular_png, + lizard::assets::lizard_regular_png_len, &w, &h, &channels, 4); + } + if (!pixels) { + if (normalized) { + spdlog::error("Failed to load emoji atlas {}: {}", normalized->string(), + stbi_failure_reason()); + } else { + spdlog::error("Failed to load embedded emoji atlas: {}", stbi_failure_reason()); + } + return false; + } + + for (int i = 0; i < w * h; ++i) { + unsigned char *p = pixels + i * 4; + unsigned char a = p[3]; + p[0] = static_cast(p[0] * a / 255); + p[1] = static_cast(p[1] * a / 255); + p[2] = static_cast(p[2] * a / 255); + } + if (!m_texture.id) { + m_texture.create(); + } + glBindTexture(GL_TEXTURE_2D, m_texture.id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + stbi_image_free(pixels); #endif + + std::ifstream atlas_file; + std::istringstream atlas_default(R"({ + "sprites": { + "🦎": { "u0": 0.0, "v0": 0.0, "u1": 0.5, "v1": 0.5 }, + "🐍": { "u0": 0.5, "v0": 0.0, "u1": 1.0, "v1": 0.5 }, + "🐢": { "u0": 0.0, "v0": 0.5, "u1": 0.5, "v1": 1.0 } + } +})"); + std::istream *atlas = nullptr; + if (normalized) { + if (normalized->extension() == ".json") { + atlas_file.open(*normalized); + if (atlas_file.is_open()) { + atlas = &atlas_file; + } + } else { + std::filesystem::path json_path = *normalized; + json_path += ".json"; + if (std::filesystem::exists(json_path)) { + atlas_file.open(json_path); + if (atlas_file.is_open()) { + atlas = &atlas_file; + } + } + if (!atlas) { + json_path = normalized->parent_path() / "emoji_atlas.json"; + if (std::filesystem::exists(json_path)) { + atlas_file.open(json_path); + if (atlas_file.is_open()) { + atlas = &atlas_file; + } + } + } + } + } + if (!atlas) { + atlas = &atlas_default; + } + try { + json j; + *atlas >> j; + if (j.contains("sprites") && j["sprites"].is_object()) { + for (const auto &[emoji, s] : j["sprites"].items()) { + Sprite sp{}; + sp.u0 = s.value("u0", 0.0f); + sp.v0 = s.value("v0", 0.0f); + sp.u1 = s.value("u1", 1.0f); + sp.v1 = s.value("v1", 1.0f); + lookup[emoji] = static_cast(sprites.size()); + sprites.push_back(sp); + } + } + } catch (const std::exception &e) { + spdlog::error("Failed to parse emoji atlas: {}", e.what()); + } + if (sprites.empty()) { + sprites.push_back({0.0f, 0.0f, 1.0f, 1.0f}); + lookup["🦎"] = 0; + } + + m_sprite_lookup = std::move(lookup); + m_sprites = std::move(sprites); + m_current_emoji_path = normalized; + return true; +} + +void Overlay::build_selector(const std::vector &emoji, + const std::unordered_map &emoji_weighted) { + m_selector_indices.clear(); + std::vector weights; + + if (!emoji_weighted.empty()) { + for (const auto &[symbol, weight] : emoji_weighted) { + auto it = m_sprite_lookup.find(symbol); + if (it != m_sprite_lookup.end()) { + m_selector_indices.push_back(it->second); + weights.push_back(weight); + } + } + } else if (!emoji.empty()) { + for (const auto &symbol : emoji) { + auto it = m_sprite_lookup.find(symbol); + if (it != m_sprite_lookup.end()) { + m_selector_indices.push_back(it->second); + weights.push_back(1.0); + } + } + } + + if (m_selector_indices.empty()) { + for (int i = 0; i < static_cast(m_sprites.size()); ++i) { + m_selector_indices.push_back(i); + weights.push_back(1.0); + } + } + + if (weights.empty()) { + weights.push_back(1.0); + if (m_selector_indices.empty()) { + m_selector_indices.push_back(0); + } + } + + m_selector = std::discrete_distribution<>(weights.begin(), weights.end()); +} + +void Overlay::refresh_from_config(const app::Config &cfg) { + PendingConfig pending; + pending.spawn_strategy = cfg.badge_spawn_strategy(); + pending.badge_min_px = cfg.badge_min_px(); + pending.badge_max_px = cfg.badge_max_px(); + pending.badges_per_second_max = cfg.badges_per_second_max(); + pending.fps_mode = cfg.fps_mode(); + pending.fps_fixed = cfg.fps_fixed(); + pending.emoji_atlas = normalize_path(cfg.emoji_atlas()); + pending.emoji = cfg.emoji(); + pending.emoji_weighted = cfg.emoji_weighted(); + + { + std::lock_guard lock(m_pending_mutex); + m_pending_config = std::move(pending); + } + m_has_pending_config.store(true, std::memory_order_release); +} + +void Overlay::apply_pending_config() { + PendingConfig pending; + { + std::lock_guard lock(m_pending_mutex); + if (!m_pending_config) { + return; + } + pending = *m_pending_config; + m_pending_config.reset(); + } + + if (pending.spawn_strategy == "cursor_follow") { + m_spawn_strategy = BadgeSpawnStrategy::CursorFollow; + } else { + m_spawn_strategy = BadgeSpawnStrategy::RandomScreen; + } + + m_badge_min_px = pending.badge_min_px; + m_badge_max_px = pending.badge_max_px; + m_badges_per_second_max = pending.badges_per_second_max; + + if (pending.fps_mode == "fixed") { + set_fps_fixed(pending.fps_fixed); + set_fps_mode(platform::FpsMode::Fixed); + } else { + set_fps_mode(platform::FpsMode::Auto); + } + + auto normalized = normalize_path(pending.emoji_atlas); + bool atlas_changed = normalized != m_current_emoji_path; + if (atlas_changed) { + if (load_atlas_from_path(normalized)) { + m_badges.clear(); + m_spawn_times.clear(); + } else { + normalized = m_current_emoji_path; + } + } + + build_selector(pending.emoji, pending.emoji_weighted); } void Overlay::shutdown() { @@ -798,6 +852,9 @@ void Overlay::run(std::stop_token st) { using clock = std::chrono::steady_clock; auto last = clock::now(); while (m_running && !st.stop_requested()) { + if (m_has_pending_config.exchange(false, std::memory_order_acq_rel)) { + apply_pending_config(); + } auto frame = std::chrono::microseconds(m_frame_interval_us.load()); if (m_paused.load()) { std::this_thread::sleep_for(frame); From 9c792af883174de4c7c93fbad7639a9c89ddad03 Mon Sep 17 00:00:00 2001 From: Mariana Date: Sat, 1 Nov 2025 20:52:20 +0000 Subject: [PATCH 2/4] Guard overlay config refresh against concurrent spawns --- src/overlay/overlay.cpp | 98 ++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/src/overlay/overlay.cpp b/src/overlay/overlay.cpp index f9a0e9b..4569163 100644 --- a/src/overlay/overlay.cpp +++ b/src/overlay/overlay.cpp @@ -117,6 +117,7 @@ class Overlay { private: friend struct ::OverlayTestAccess; int select_sprite(); + int select_sprite_locked(); void update(float dt); void render(); void update_frame_interval(); @@ -124,6 +125,8 @@ class Overlay { bool load_atlas_from_path(const std::optional &emoji_path); void build_selector(const std::vector &emoji, const std::unordered_map &emoji_weighted); + void spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrategy strategy, + int badge_min_px, int badge_max_px, int badges_per_second_max); static std::optional normalize_path(const std::optional &path); @@ -170,6 +173,7 @@ class Overlay { std::mutex m_pending_mutex; std::optional m_pending_config; std::atomic m_has_pending_config{false}; + std::mutex m_spawn_config_mutex; }; void Overlay::update_frame_interval() { @@ -651,23 +655,6 @@ void Overlay::apply_pending_config() { m_pending_config.reset(); } - if (pending.spawn_strategy == "cursor_follow") { - m_spawn_strategy = BadgeSpawnStrategy::CursorFollow; - } else { - m_spawn_strategy = BadgeSpawnStrategy::RandomScreen; - } - - m_badge_min_px = pending.badge_min_px; - m_badge_max_px = pending.badge_max_px; - m_badges_per_second_max = pending.badges_per_second_max; - - if (pending.fps_mode == "fixed") { - set_fps_fixed(pending.fps_fixed); - set_fps_mode(platform::FpsMode::Fixed); - } else { - set_fps_mode(platform::FpsMode::Auto); - } - auto normalized = normalize_path(pending.emoji_atlas); bool atlas_changed = normalized != m_current_emoji_path; if (atlas_changed) { @@ -679,7 +666,27 @@ void Overlay::apply_pending_config() { } } - build_selector(pending.emoji, pending.emoji_weighted); + { + std::lock_guard lock(m_spawn_config_mutex); + if (pending.spawn_strategy == "cursor_follow") { + m_spawn_strategy = BadgeSpawnStrategy::CursorFollow; + } else { + m_spawn_strategy = BadgeSpawnStrategy::RandomScreen; + } + + m_badge_min_px = pending.badge_min_px; + m_badge_max_px = pending.badge_max_px; + m_badges_per_second_max = pending.badges_per_second_max; + + build_selector(pending.emoji, pending.emoji_weighted); + } + + if (pending.fps_mode == "fixed") { + set_fps_fixed(pending.fps_fixed); + set_fps_mode(platform::FpsMode::Fixed); + } else { + set_fps_mode(platform::FpsMode::Auto); + } } void Overlay::shutdown() { @@ -713,6 +720,45 @@ void Overlay::shutdown() { } int Overlay::select_sprite() { + std::lock_guard lock(m_spawn_config_mutex); + return select_sprite_locked(); +} + +void Overlay::spawn_badge(float x, float y) { + int sprite = 0; + BadgeSpawnStrategy strategy = BadgeSpawnStrategy::RandomScreen; + int badge_min_px = m_badge_min_px; + int badge_max_px = m_badge_max_px; + int badges_per_second_max = m_badges_per_second_max; + { + std::lock_guard lock(m_spawn_config_mutex); + sprite = select_sprite_locked(); + strategy = m_spawn_strategy; + badge_min_px = m_badge_min_px; + badge_max_px = m_badge_max_px; + badges_per_second_max = m_badges_per_second_max; + } + spawn_badge_internal(sprite, x, y, strategy, badge_min_px, badge_max_px, + badges_per_second_max); +} + +void Overlay::spawn_badge(int sprite, float x, float y) { + BadgeSpawnStrategy strategy = BadgeSpawnStrategy::RandomScreen; + int badge_min_px = m_badge_min_px; + int badge_max_px = m_badge_max_px; + int badges_per_second_max = m_badges_per_second_max; + { + std::lock_guard lock(m_spawn_config_mutex); + strategy = m_spawn_strategy; + badge_min_px = m_badge_min_px; + badge_max_px = m_badge_max_px; + badges_per_second_max = m_badges_per_second_max; + } + spawn_badge_internal(sprite, x, y, strategy, badge_min_px, badge_max_px, + badges_per_second_max); +} + +int Overlay::select_sprite_locked() { if (m_selector_indices.empty()) { return 0; } @@ -720,9 +766,9 @@ int Overlay::select_sprite() { return m_selector_indices[idx]; } -void Overlay::spawn_badge(float x, float y) { spawn_badge(select_sprite(), x, y); } - -void Overlay::spawn_badge(int sprite, float x, float y) { +void Overlay::spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrategy strategy, + int badge_min_px, int badge_max_px, + int badges_per_second_max) { if (m_badge_suppressed) { if (m_badges.size() < static_cast(m_badge_capacity * 0.8f)) { m_badge_suppressed = false; @@ -739,14 +785,14 @@ void Overlay::spawn_badge(int sprite, float x, float y) { while (!m_spawn_times.empty() && now - m_spawn_times.front() > std::chrono::seconds(1)) { m_spawn_times.pop_front(); } - if (m_badges_per_second_max > 0 && - static_cast(m_spawn_times.size()) >= m_badges_per_second_max) { + if (badges_per_second_max > 0 && + static_cast(m_spawn_times.size()) >= badges_per_second_max) { return; } float px = x; float py = y; - if (m_spawn_strategy == BadgeSpawnStrategy::RandomScreen) { + if (strategy == BadgeSpawnStrategy::RandomScreen) { std::uniform_real_distribution dist(0.0f, 1.0f); px = dist(m_rng); py = dist(m_rng); @@ -762,8 +808,8 @@ void Overlay::spawn_badge(int sprite, float x, float y) { std::uniform_real_distribution phaseDist(0.0f, 6.2831853f); float phase = phaseDist(m_rng); - std::uniform_real_distribution diaDist(static_cast(m_badge_min_px), - static_cast(m_badge_max_px)); + std::uniform_real_distribution diaDist(static_cast(badge_min_px), + static_cast(badge_max_px)); float diameter = diaDist(m_rng); float scale = (diameter * 2.0f) / m_view_height; From e1d56491f333435cd0e7dfa53b982a1e83d92ffa Mon Sep 17 00:00:00 2001 From: Mariana Date: Sat, 1 Nov 2025 21:18:50 +0000 Subject: [PATCH 3/4] Protect atlas refresh with spawn mutex --- src/overlay/overlay.cpp | 50 +++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/overlay/overlay.cpp b/src/overlay/overlay.cpp index 4569163..3cce8a9 100644 --- a/src/overlay/overlay.cpp +++ b/src/overlay/overlay.cpp @@ -122,7 +122,12 @@ class Overlay { void render(); void update_frame_interval(); void apply_pending_config(); - bool load_atlas_from_path(const std::optional &emoji_path); + struct AtlasData { + std::vector sprites; + std::unordered_map lookup; + std::optional normalized_path; + }; + std::optional load_atlas_from_path(const std::optional &emoji_path); void build_selector(const std::vector &emoji, const std::unordered_map &emoji_weighted); void spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrategy strategy, @@ -238,9 +243,13 @@ bool Overlay::init(const app::Config &cfg, std::optional m_badges_per_second_max = cfg.badges_per_second_max(); update_frame_interval(); + auto emoji = cfg.emoji(); + auto emoji_weighted = cfg.emoji_weighted(); auto normalized_path = normalize_path(emoji_path); + std::optional atlas; #ifdef LIZARD_TEST - if (!load_atlas_from_path(normalized_path)) { + atlas = load_atlas_from_path(normalized_path); + if (!atlas) { return false; } #else @@ -307,12 +316,19 @@ bool Overlay::init(const app::Config &cfg, std::optional return false; } - if (!load_atlas_from_path(normalized_path)) { + atlas = load_atlas_from_path(normalized_path); + if (!atlas) { return false; } #endif - build_selector(cfg.emoji(), cfg.emoji_weighted()); + { + std::lock_guard lock(m_spawn_config_mutex); + m_sprite_lookup = std::move(atlas->lookup); + m_sprites = std::move(atlas->sprites); + m_current_emoji_path = std::move(atlas->normalized_path); + build_selector(emoji, emoji_weighted); + } m_badge_capacity = 150; m_badges.reserve(m_badge_capacity); @@ -462,7 +478,8 @@ Overlay::normalize_path(const std::optional &path) { return path->lexically_normal(); } -bool Overlay::load_atlas_from_path(const std::optional &emoji_path) { +std::optional +Overlay::load_atlas_from_path(const std::optional &emoji_path) { auto normalized = normalize_path(emoji_path); std::unordered_map lookup; std::vector sprites; @@ -477,7 +494,7 @@ bool Overlay::load_atlas_from_path(const std::optional &e g_overlay_log_called = true; spdlog::error("Failed to load emoji atlas {}: {}", normalized->string(), stbi_failure_reason()); - return false; + return std::nullopt; } stbi_image_free(pixels); } @@ -499,7 +516,7 @@ bool Overlay::load_atlas_from_path(const std::optional &e } else { spdlog::error("Failed to load embedded emoji atlas: {}", stbi_failure_reason()); } - return false; + return std::nullopt; } for (int i = 0; i < w * h; ++i) { @@ -579,10 +596,11 @@ bool Overlay::load_atlas_from_path(const std::optional &e lookup["🦎"] = 0; } - m_sprite_lookup = std::move(lookup); - m_sprites = std::move(sprites); - m_current_emoji_path = normalized; - return true; + AtlasData data; + data.sprites = std::move(sprites); + data.lookup = std::move(lookup); + data.normalized_path = normalized; + return data; } void Overlay::build_selector(const std::vector &emoji, @@ -657,17 +675,25 @@ void Overlay::apply_pending_config() { auto normalized = normalize_path(pending.emoji_atlas); bool atlas_changed = normalized != m_current_emoji_path; + std::optional atlas; if (atlas_changed) { - if (load_atlas_from_path(normalized)) { + atlas = load_atlas_from_path(normalized); + if (atlas) { m_badges.clear(); m_spawn_times.clear(); } else { normalized = m_current_emoji_path; + atlas.reset(); } } { std::lock_guard lock(m_spawn_config_mutex); + if (atlas) { + m_sprite_lookup = std::move(atlas->lookup); + m_sprites = std::move(atlas->sprites); + m_current_emoji_path = std::move(atlas->normalized_path); + } if (pending.spawn_strategy == "cursor_follow") { m_spawn_strategy = BadgeSpawnStrategy::CursorFollow; } else { From 2b1c3cbcb70bfdedaa7bcd1fb7bb6fd77b760fa2 Mon Sep 17 00:00:00 2001 From: Mariana Date: Sat, 1 Nov 2025 21:40:49 +0000 Subject: [PATCH 4/4] Guard badge container reset with spawn mutex --- src/overlay/overlay.cpp | 57 +++++++++++------------------------------ 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/src/overlay/overlay.cpp b/src/overlay/overlay.cpp index 3cce8a9..0883915 100644 --- a/src/overlay/overlay.cpp +++ b/src/overlay/overlay.cpp @@ -130,8 +130,7 @@ class Overlay { std::optional load_atlas_from_path(const std::optional &emoji_path); void build_selector(const std::vector &emoji, const std::unordered_map &emoji_weighted); - void spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrategy strategy, - int badge_min_px, int badge_max_px, int badges_per_second_max); + void spawn_badge_locked(int sprite, float x, float y); static std::optional normalize_path(const std::optional &path); @@ -678,10 +677,7 @@ void Overlay::apply_pending_config() { std::optional atlas; if (atlas_changed) { atlas = load_atlas_from_path(normalized); - if (atlas) { - m_badges.clear(); - m_spawn_times.clear(); - } else { + if (!atlas) { normalized = m_current_emoji_path; atlas.reset(); } @@ -690,6 +686,8 @@ void Overlay::apply_pending_config() { { std::lock_guard lock(m_spawn_config_mutex); if (atlas) { + m_badges.clear(); + m_spawn_times.clear(); m_sprite_lookup = std::move(atlas->lookup); m_sprites = std::move(atlas->sprites); m_current_emoji_path = std::move(atlas->normalized_path); @@ -751,37 +749,14 @@ int Overlay::select_sprite() { } void Overlay::spawn_badge(float x, float y) { - int sprite = 0; - BadgeSpawnStrategy strategy = BadgeSpawnStrategy::RandomScreen; - int badge_min_px = m_badge_min_px; - int badge_max_px = m_badge_max_px; - int badges_per_second_max = m_badges_per_second_max; - { - std::lock_guard lock(m_spawn_config_mutex); - sprite = select_sprite_locked(); - strategy = m_spawn_strategy; - badge_min_px = m_badge_min_px; - badge_max_px = m_badge_max_px; - badges_per_second_max = m_badges_per_second_max; - } - spawn_badge_internal(sprite, x, y, strategy, badge_min_px, badge_max_px, - badges_per_second_max); + std::lock_guard lock(m_spawn_config_mutex); + int sprite = select_sprite_locked(); + spawn_badge_locked(sprite, x, y); } void Overlay::spawn_badge(int sprite, float x, float y) { - BadgeSpawnStrategy strategy = BadgeSpawnStrategy::RandomScreen; - int badge_min_px = m_badge_min_px; - int badge_max_px = m_badge_max_px; - int badges_per_second_max = m_badges_per_second_max; - { - std::lock_guard lock(m_spawn_config_mutex); - strategy = m_spawn_strategy; - badge_min_px = m_badge_min_px; - badge_max_px = m_badge_max_px; - badges_per_second_max = m_badges_per_second_max; - } - spawn_badge_internal(sprite, x, y, strategy, badge_min_px, badge_max_px, - badges_per_second_max); + std::lock_guard lock(m_spawn_config_mutex); + spawn_badge_locked(sprite, x, y); } int Overlay::select_sprite_locked() { @@ -792,9 +767,7 @@ int Overlay::select_sprite_locked() { return m_selector_indices[idx]; } -void Overlay::spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrategy strategy, - int badge_min_px, int badge_max_px, - int badges_per_second_max) { +void Overlay::spawn_badge_locked(int sprite, float x, float y) { if (m_badge_suppressed) { if (m_badges.size() < static_cast(m_badge_capacity * 0.8f)) { m_badge_suppressed = false; @@ -811,14 +784,14 @@ void Overlay::spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrat while (!m_spawn_times.empty() && now - m_spawn_times.front() > std::chrono::seconds(1)) { m_spawn_times.pop_front(); } - if (badges_per_second_max > 0 && - static_cast(m_spawn_times.size()) >= badges_per_second_max) { + if (m_badges_per_second_max > 0 && + static_cast(m_spawn_times.size()) >= m_badges_per_second_max) { return; } float px = x; float py = y; - if (strategy == BadgeSpawnStrategy::RandomScreen) { + if (m_spawn_strategy == BadgeSpawnStrategy::RandomScreen) { std::uniform_real_distribution dist(0.0f, 1.0f); px = dist(m_rng); py = dist(m_rng); @@ -834,8 +807,8 @@ void Overlay::spawn_badge_internal(int sprite, float x, float y, BadgeSpawnStrat std::uniform_real_distribution phaseDist(0.0f, 6.2831853f); float phase = phaseDist(m_rng); - std::uniform_real_distribution diaDist(static_cast(badge_min_px), - static_cast(badge_max_px)); + std::uniform_real_distribution diaDist(static_cast(m_badge_min_px), + static_cast(m_badge_max_px)); float diameter = diaDist(m_rng); float scale = (diameter * 2.0f) / m_view_height;