diff --git a/include/runtime/roblox_text_surface_overlay.h b/include/runtime/roblox_text_surface_overlay.h index 6a6b0f4..1ce7879 100644 --- a/include/runtime/roblox_text_surface_overlay.h +++ b/include/runtime/roblox_text_surface_overlay.h @@ -11,6 +11,8 @@ #include "mocktail/status.h" #include "runtime/roblox_text_display_state.h" +struct TTF_Font; + namespace mocktail { namespace runtime { @@ -39,6 +41,8 @@ class RobloxTextSurfaceOverlay final { const RobloxTextDisplayUpdate& update); void ApplyUpdate(const RobloxTextDisplayUpdate& update); Status RasterizeLocked(); + Status OpenFontsLocked(std::int32_t font, float point_size); + void CloseFontsLocked(); void ClearFrameLocked(); void RecordFailureLocked(Status status); @@ -46,10 +50,14 @@ class RobloxTextSurfaceOverlay final { RobloxTextDisplayState state_; RobloxTextOverlayViewport viewport_; std::vector rgba_; + std::vector fonts_; + std::int32_t fonts_font_ = 0; + float fonts_point_size_ = 0.0F; std::uint64_t state_revision_ = 0; std::uint64_t raster_revision_ = 0; Status failure_; bool initialized_ = false; + bool ttf_initialized_ = false; bool failure_logged_ = false; bool ready_logged_ = false; }; diff --git a/src/runtime/roblox_text_surface_overlay.cc b/src/runtime/roblox_text_surface_overlay.cc index 07cd8d5..5982d70 100644 --- a/src/runtime/roblox_text_surface_overlay.cc +++ b/src/runtime/roblox_text_surface_overlay.cc @@ -301,6 +301,7 @@ Status RobloxTextSurfaceOverlay::Shutdown() { } state_.Reset(); ClearFrameLocked(); + CloseFontsLocked(); viewport_ = {}; initialized_ = false; if (owned_active_overlay) { @@ -445,10 +446,6 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() { const bool wrapped_layout = presentation.multiline || presentation.text_wrapped; - if (!TTF_Init()) { - SecureClear(&candidate); - return RasterizationFailure("unable to initialize SDL3_ttf"); - } const RobloxTextFontSelection font_selection = ResolveRobloxTextFont(presentation.font); const float native_point_size = @@ -458,23 +455,12 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() { ? std::clamp(native_point_size * font_selection.size_scale, 1.0F, 512.0F) : std::clamp(static_cast(height) * 0.38F, 12.0F, 28.0F); - const std::vector font_files = ResolveFontFiles(font_selection); - std::vector fonts; - fonts.reserve(font_files.size()); - for (const std::string& file : font_files) { - TTF_Font* font = TTF_OpenFont(file.c_str(), point_size); - if (font != nullptr) { - fonts.push_back(font); - } - } - if (fonts.empty()) { - TTF_Quit(); + if (Status status = OpenFontsLocked(presentation.font, point_size); + !status.ok()) { SecureClear(&candidate); - return RasterizationFailure("unable to open a Roblox or fallback font"); - } - for (std::size_t index = 1; index < fonts.size(); ++index) { - (void)TTF_AddFallbackFont(fonts.front(), fonts[index]); + return status; } + const std::vector& fonts = fonts_; if (wrapped_layout) { TTF_HorizontalAlignment alignment = TTF_HORIZONTAL_ALIGN_LEFT; if (presentation.x_alignment == 1) { @@ -629,11 +615,6 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() { if (layout != nullptr) { TTF_DestroyText(layout); } - TTF_ClearFallbackFonts(fonts.front()); - for (auto iterator = fonts.rbegin(); iterator != fonts.rend(); ++iterator) { - TTF_CloseFont(*iterator); - } - TTF_Quit(); if (!status.ok()) { SecureClear(&candidate); @@ -651,6 +632,58 @@ Status RobloxTextSurfaceOverlay::RasterizeLocked() { return Status::Ok(); } +Status RobloxTextSurfaceOverlay::OpenFontsLocked(std::int32_t font, + float point_size) { + if (!fonts_.empty() && fonts_font_ == font && + fonts_point_size_ == point_size) { + return Status::Ok(); + } + if (!TTF_Init()) { + return RasterizationFailure("unable to initialize SDL3_ttf"); + } + const std::vector files = + ResolveFontFiles(ResolveRobloxTextFont(font)); + std::vector opened; + opened.reserve(files.size()); + for (const std::string& file : files) { + TTF_Font* candidate = TTF_OpenFont(file.c_str(), point_size); + if (candidate != nullptr) { + opened.push_back(candidate); + } + } + // A failed reopen leaves the fonts already rendering in place. + if (opened.empty()) { + TTF_Quit(); + return RasterizationFailure("unable to open a Roblox or fallback font"); + } + for (std::size_t index = 1; index < opened.size(); ++index) { + (void)TTF_AddFallbackFont(opened.front(), opened[index]); + } + CloseFontsLocked(); + fonts_ = std::move(opened); + ttf_initialized_ = true; + fonts_font_ = font; + fonts_point_size_ = point_size; + return Status::Ok(); +} + +void RobloxTextSurfaceOverlay::CloseFontsLocked() { + if (!fonts_.empty()) { + TTF_ClearFallbackFonts(fonts_.front()); + for (auto iterator = fonts_.rbegin(); iterator != fonts_.rend(); + ++iterator) { + TTF_CloseFont(*iterator); + } + fonts_.clear(); + } + fonts_font_ = 0; + fonts_point_size_ = 0.0F; + if (ttf_initialized_) { + TTF_Quit(); + ttf_initialized_ = false; + } +} + void RobloxTextSurfaceOverlay::ClearFrameLocked() { SecureClear(&rgba_); raster_revision_ = 0; diff --git a/tests/roblox_text_display_state_test.cc b/tests/roblox_text_display_state_test.cc index f19b460..46b7c1d 100644 --- a/tests/roblox_text_display_state_test.cc +++ b/tests/roblox_text_display_state_test.cc @@ -594,6 +594,33 @@ TEST(RobloxTextDisplayStateTest, AdvancesCaretForTrailingSpace) { EXPECT_TRUE(overlay.Shutdown().ok()); } +TEST(RobloxTextDisplayStateTest, ReopensFontsAfterPointSizeChange) { + RobloxTextSurfaceOverlay overlay; + ASSERT_TRUE(overlay.Initialize({800, 600}).ok()); + RobloxTextDisplaySink sink = overlay.sink(); + const std::string text = "size"; + const auto text_width = [&](float font_size, RobloxTextDisplayEvent event) { + RobloxTextDisplayUpdate update = Show(1, text, 4); + update.event = event; + update.area_height = 64; + update.font_size = font_size; + sink.update(sink.context, update); + MocktailTextOverlayFrameInfo frame; + EXPECT_TRUE(overlay.QueryFrame(&frame)); + std::vector rgba(frame.rgba_bytes); + EXPECT_TRUE(overlay.CopyFrame(frame.revision, rgba.data(), rgba.size())); + const auto bounds = FindAlphaBounds(frame, rgba); + EXPECT_TRUE(bounds.valid()); + return bounds.valid() ? bounds.maximum_x - bounds.minimum_x : 0; + }; + + const int small = text_width(14.0F, RobloxTextDisplayEvent::kShow); + const int large = text_width(28.0F, RobloxTextDisplayEvent::kUpdate); + + EXPECT_GT(large, small); + EXPECT_TRUE(overlay.Shutdown().ok()); +} + } // namespace } // namespace runtime } // namespace mocktail