Technical Report: Firestorm Viewer FPS Halving Bug on Startup - ApertureViewer/Aperture-Viewer-Website GitHub Wiki

Date: 2025-04-14

Author: William Weaver, Aperture Viewer Project

Abstract: This report details the identification, investigation, and mitigation of a severe performance bug within the Firestorm Viewer codebase. The bug manifests as a persistent halving of the potential maximum framerate (FPS) when the viewer is started under specific, common configuration states involving the RenderVSyncEnable (VSync) and FSLimitFramerate (FPS Limiter) settings. The root cause appears to be a timing-sensitive interaction during the viewer's initialization sequence, specific to the Firestorm codebase where VSync functionality was merged into a system already containing an FPS limiter. This issue is not present in the base Linden Lab viewer. A workaround patch has been developed and implemented in the Aperture Viewer fork, which effectively prevents the performance degradation by modifying the initial VSync state under the problematic startup condition. This report provides reproduction steps, detailed analysis, code references, and the implementation details of the current workaround.

1. Introduction

The Firestorm Viewer, a fork of the official Second Life viewer, incorporates both Vertical Synchronization (VSync) functionality (merged from upstream Linden Lab code) and a pre-existing, Firestorm-specific application-level FPS limiter. This report documents a critical performance degradation issue arising from the interaction of these systems, specifically during the application startup phase. The issue results in a user's maximum achievable FPS being persistently halved for their session if certain settings are configured at launch. This report aims to provide Firestorm developers and QA teams with the necessary technical details to understand, reproduce, and address this bug.

2. Problem Description

When Firestorm Viewer is launched with the RenderVSyncEnable setting configured to false AND the FSLimitFramerate setting configured to true, a persistent performance cap is engaged for the duration of the viewer session. If the user later disables the FSLimitFramerate setting during runtime (attempting to achieve maximum uncapped FPS), the resulting frame rate is observed to be approximately half of the true maximum frame rate achievable by their hardware under normal (non-bugged) conditions. This halving is consistent across different hardware and potential FPS levels (e.g., a potential 240 FPS becomes ~120 FPS, a potential 180 FPS becomes ~90 FPS).

3. Affected Versions & Platforms

Testing confirms this issue is present in current Firestorm release, beta, and development branches as of the date of this report. The bug has been specifically confirmed on Windows builds utilizing AVX instructions. Initial testing indicates the issue also exists on AVX2 Windows builds, although comprehensive testing across multiple AVX2 platforms has not been feasible due to resource constraints. Testing has not yet been performed on Linux or macOS platforms, so the bug's presence on those operating systems is currently unknown.

4. Reproduction Steps & Testing Methodology

4.1. Test Objective: To systematically identify the conditions under which the FPS halving bug manifests and establish the true Baseline Maximum FPS. This methodology accounts for initial startup configurations, the critical impact of subsequent viewer launches involving disk cache reads, the sequence of scenario transitions within a testing phase, and the order of runtime setting changes.

Note

The exhaustive procedure described below reflects the rigorous QA process undertaken to fully characterize the bug's behavior and triggers. End-users may experience this bug without performing these extensive steps, as it can manifest simply during subsequent viewer launches (when caches are active) or during general use cases involving disabling the FPS limiter or VSync at runtime after starting the viewer in an affected initial configuration.

4.2. Prerequisites:

  • Environment: Use a consistent low-lag environment for all tests.
  • FPS Monitoring: Use the standard on-screen FPS counter (not the full Statistics Floater) for all observations. Record peak stable FPS values.
  • VSync Initialization Point: Document the location of the initial toggleVSync call in the specific Firestorm build being tested, as this influences results, particularly on first runs (See Section 6.5).

4.3. Overall Test Structure: The investigation involved four Major Phases. Each Major Phase began with a mandatory clean environment setup (clearing Firestorm's user-specific settings and cache directories). Within each Major Phase, a specific sequence of Test Runs was performed consecutively without clearing the environment between runs. This allowed observation of behaviour changes between the first clean run and subsequent runs involving disk cache loading, as well as the impact of transitioning between different runtime states. (This detailed structure was part of the QA process; users may encounter the bug on simple subsequent launches).

4.4. Major Phases Definition: The four Major Phases were defined by the initial startup scenario used immediately after the clean environment setup:

  • Phase D-Start: First run starts as Scenario D (VSync=ON, Limiter=ON).
  • Phase C-Start: First run starts as Scenario C (VSync=OFF, Limiter=OFF).
  • Phase B-Start: First run starts as Scenario B (VSync=ON, Limiter=OFF).
  • Phase A-Start: First run starts as Scenario A (VSync=OFF, Limiter=ON).

4.5. Test Run Procedure Pattern (Sequence within each Major Phase): Within each Major Phase, a sequence involving multiple consecutive runs starting in different configurations (e.g., D -> C -> C -> C -> B -> B -> etc.) was executed to observe transitions and the impact of cache loading. Runtime uncapping was performed using different toggle orders (See 4.6) to check for state dependencies. (Detailed logs documenting the exact sequences and FPS results are available separately; the core finding is the increased bug manifestation on runs involving cache reads).

5. Expected vs. Actual Behavior (Revised)

  • Expected Behavior: In all scenarios, after disabling both VSync and the FPS Limiter at runtime (or starting with them disabled in Scenario C), the observed FPS should reach the Baseline Maximum FPS.
  • Actual Behavior: Depending on the VSync initialization location (See 6.5), starting in Scenario A and/or Scenario C results in a persistent state where the maximum achievable runtime FPS is capped at approximately half the Baseline Maximum FPS.

6. Technical Investigation & Analysis

The investigation followed several paths, eliminating potential causes:

6.1. Initial Scenarios & Observations: Four primary startup scenarios were tested:

  • Scenario D (VSync=T, Limiter=T): Starts VSync capped (~60 FPS). Runtime uncapping works correctly -> Baseline Max FPS. (OK)
  • Scenario B (VSync=T, Limiter=F): Starts VSync capped (~60 FPS). Runtime VSync disable works correctly -> Baseline Max FPS. (OK)
  • Scenario C (VSync=F, Limiter=F): Initial tests with standard code showed halved FPS (~120 FPS). Moving VSync init later fixed this specific scenario. (Initial State Problematic)
  • Scenario A (VSync=F, Limiter=T): Starts limiter capped (~60 FPS). Runtime limiter disable results in halved FPS (~120 FPS). (Problematic Scenario)

6.2. Code Analysis - Key Components:

  • Settings: RenderVSyncEnable (bool), FSLimitFramerate (bool), FramePerSecondLimit (U32, accessed as max_fps).

  • VSync Control: LLWindow::toggleVSync(bool) implemented platform-specifically (e.g., LLWindowWin32::toggleVSync using wglSwapIntervalEXT). Called during initial context setup (LLWindowWin32::switchContext) and via signal listener (handleVSyncChanged in llviewercontrol.cpp).

  • FPS Limiter Logic: Located in LLAppViewer::doFrame. Uses LLCachedControl for settings access. Calculates sleep duration based on frameTimer.getElapsedTimeF64() and max_fps. Uses ms_sleep (or replacement) to yield time.

      ```c++
      // llappviewer.cpp - Limiter Block
      static LLCachedControl<U32> max_fps(gSavedSettings, "FramePerSecondLimit");
      static LLCachedControl<bool> fsLimitFramerate(gSavedSettings, "FSLimitFramerate");
      if (fsLimitFramerate && LLStartUp::getStartupState() == STATE_STARTED /*...other conditions...*/)
      {
          F32 min_frame_time = 1.f / (F32)max_fps;
          S32 milliseconds_to_sleep = llclamp((S32)((min_frame_time - frameTimer.getElapsedTimeF64()) * 1000.f), 0, 1000);
          if (milliseconds_to_sleep > 0) { ms_sleep(milliseconds_to_sleep); } // Or std::this_thread::sleep_for
      }
      frameTimer.reset();
      ```
    
  • Performance Stats: LLPerfStats::Tunables::initialiseFromSettings reads settings. LLPerfStats::StatsRecorder::updateAvatarParams contains Autotune logic which uses tunables.vsyncEnabled and tunables.userTargetFPS.

6.3. Hypotheses Tested & Refuted:

  • Simple VSync Cap: Refuted because the halving occurs even when potential FPS is much higher than monitor refresh (e.g., 240 -> 120 on a 60Hz monitor).
  • frameTimer Inaccuracy: Refuted by log analysis. frameTimer.getElapsedTimeF64() consistently reported values accurate to the observed (halved or full) FPS. The application is running slower.
  • OS VSync State Mismatch: Initially suspected, but code review confirmed toggleVSync is called during initialization (LLWindowWin32::switchContext) with the correct value based on the setting. Refuted.
  • ms_sleep vs. std::this_thread::sleep_for: Replacing the sleep function did not resolve the halving in Scenario A. Refuted as the sole cause.
  • Autotune Interference: Refuted because the issue occurs even when Autotune features are disabled by the user. Autotune is not automatically adjusting performance settings in these scenarios.

6.4. Supported Hypothesis - Initialization Timing Interaction: The evidence points strongly towards a timing-sensitive interaction specific to the Firestorm codebase, triggered only when the initialization sequence occurs with the initial state of RenderVSyncEnable=false and FSLimitFramerate=true (Scenario A).

  • Trigger: Calling toggleVSync(false) (via switchContext) early in initialization.
  • Condition: The LLAppViewer::doFrame limiter code block must be active (FSLimitFramerate=true) during these initial frames.
  • Result: This specific combination appears to place the driver, Windows DWM compositor, or the viewer's presentation pipeline into a persistent, degraded state where the maximum frame presentation rate is halved for the session.
  • Firestorm Specificity: This likely occurs because the Firestorm doFrame loop's timing characteristics (influenced by the presence of the limiter code path, even if sleep time is 0) interact negatively with the driver/DWM state established by the early VSync disable. The base LL viewer lacks the limiter code path and thus doesn't trigger the same interaction. Concurrent tasks like texture cache loading may exacerbate this timing sensitivity.

6.5. Sensitivity to VSync Initialization Timing: A critical factor identified during the investigation is the timing and location within the startup code where the initial VSync state (based on RenderVSyncEnable) is applied via the platform-specific toggleVSync call.

  • Early Initialization (e.g., in LLWindowWin32::switchContext): Calling toggleVSync(false) during this early phase, concurrently with other context and resource setup, appears to be the primary trigger for the bug state, especially when FSLimitFramerate is also initially true (Scenario A). It may also trigger the bug in Scenario C.
  • Late Initialization (e.g., end of LLAppViewer::init): Delaying the toggleVSync call until later in the startup sequence was observed to prevent the bug in Scenario C. However, this alone did not prevent the bug from occurring in Scenario A.
  • Implication for Testing: Reproducing this bug requires awareness of where toggleVSync is called in the specific Firestorm build under test. Changes to this initialization point will alter which startup scenarios (A and/or C) manifest the halved FPS behavior. This confirms the issue is rooted in a timing conflict during initialization related to the VSync state request.

6.6. Cache Loading as a Primary Trigger for Manifestation: While specific startup settings (particularly RenderVSyncEnable=false) are preconditions, the comprehensive testing methodology (Section 4) definitively identified the loading of the texture cache from disk during startup on subsequent runs as a primary trigger or exacerbating factor for the bug. Users are significantly more likely to encounter the FPS halving not necessarily on their very first launch after installation/cleaning, but on the second or subsequent launches when the cache is active. This strongly suggests the root cause involves a critical race condition or resource conflict between the rendering/VSync initialization pathway and the disk I/O / cache processing pathway during the viewer's startup phase. This conflict appears specific to the Firestorm codebase, potentially due to how the FPS limiter's code path influences overall loop timing, even if inactive.

7. Mitigation Attempts

Moving VSync Initialization Later: The call to toggleVSync was moved from LLWindowWin32::switchContext to the end of LLAppViewer::init.

  • Result: This successfully fixed Scenario C (VSync=F, Limiter=F startup), allowing it to reach full uncapped FPS. However, it did not fix Scenario A (VSync=F, Limiter=T startup), which still resulted in halved FPS at runtime when the limiter was disabled.

Replacing ms_sleep: ms_sleep was replaced with std::this_thread::sleep_for.

  • Result: No change. Scenario A still resulted in halved FPS at runtime.

Temporarily Disabling Limiter Block Execution: Modifying the if condition to prevent the limiter block from executing for the first ~120 frames if VSync started OFF.

  • Result: Conflicting/inconclusive initial results, but did not reliably fix Scenario A, suggesting the mere potential execution path during startup under VSync OFF might be part of the trigger.

8. Current Workaround Patch (Forced State / Disabled Persistence)

Due to the failure of targeted fixes and the clear link to initialization state and cache loading, the only configuration currently known to reliably prevent the FPS halving bug across all scenarios and subsequent runs (including those involving cache reads) involves forcing specific default states and disabling persistence for key related settings. This is achieved via modifications to settings.xml defaults, as shown in the provided diff and clarified below.

8.1. Diff Implementation: (The diff file itself remains the same as previously shown)

8.2. Explanation of Changes (Corrected): This patch modifies the default values and persistence of several key settings: RenderVSyncEnable:

  • Before: Default OFF (Value=0), Persisted (Persist=1)
  • After: Default ON (Value=1), Persistence Removed (Persist=0). This forces the viewer to start with VSync ON every time, regardless of the user's previous setting.

FSLimitFramerate:

  • Before: Default ON (Value=1), Persisted (Persist=1)
  • After (from Diff): Default ON (Value=1), Persistence Removed (Persist=0). This forces the viewer to start with the Limiter ON every time, regardless of the user's previous setting.

LocalCacheVersion:

  • Before: (Implied) Persisted.
  • After: Default 1, Persistence Removed (Persist=0). This likely forces the cache system into a specific known state on each startup, potentially avoiding issues related to version checks or loading logic on subsequent runs.

RenderShaderCacheEnabled:

  • Before: Default ON (Value=1), Persisted (Persist=1)
  • After: Default OFF (Value=0), Persistence Removed (Persist=0). This disables the shader cache by default on every startup.

8.3. Status & Caveats (Corrected):

  • This diff prevents the bug by forcing the viewer into the "Scenario D" startup state (VSync=ON, Limiter=ON) every time, disabling the shader cache, and potentially resetting the disk cache state (LocalCacheVersion). This avoids the problematic initialization pathways.
  • This is NOT considered a proper fix. It removes user control over the default startup state for VSync and the FPS Limiter, forcing them ON. It also disables the shader cache by default. While users can still change VSync/Limiter during a session, their chosen state will not persist to the next launch.
  • It serves only as a diagnostic workaround demonstrating that avoiding specific initialization pathways and configurations prevents the bug. It strongly implies the root cause lies within those initialization conflicts, particularly when RenderVSyncEnable attempts to start as false.

9. Impact Assessment (Revised)

  • Performance: Causes a direct, unavoidable 50% reduction in maximum potential FPS for affected users on tested platforms.
  • User Base & Platform: Estimated to affect 70-90% of users based on the likelihood of problematic startup configurations. Confirmed on Windows AVX builds, with initial tests indicating presence on Windows AVX2 builds as well. The impact on Linux and macOS is currently untested.
  • Scope: Affects all hardware levels and is independent of graphics quality settings on the confirmed platforms. Present in current Firestorm release, beta, and development branches.

10. Recommendations (Revised)

  1. Acknowledge & Prioritize: Recognize the severity and widespread nature of this bug, particularly its manifestation during common cache-loading scenarios on subsequent runs. Prioritize finding a true fix.
  2. Focus on Initialization Conflict: Direct investigation towards the timing conflicts during startup between VSync/pacing state initialization, FPS limiter logic presence, and concurrent disk I/O for texture cache loading. Analyze resource contention and scheduler interactions during this critical phase.
  3. Reject Workaround as Fix: The provided settings.xml diff should be viewed as diagnostic, proving the issue can be bypassed by forcing state, but it is not an acceptable solution. A proper fix must allow users to reliably start the viewer with any combination of VSync/Limiter settings ON or OFF, with caches enabled, without encountering the FPS halving bug.
  4. Consider Hotfix (Post-True Fix): Once a proper fix is developed that addresses the initialization conflict without removing user choice or disabling caches, consider its deployment in a hotfix due to the significant performance implications.

11. Conclusion (Revised)

The persistent FPS halving bug in Firestorm is a critical issue rooted in initialization timing conflicts specific to its codebase. The interaction between how VSync state and the FPS limiter are handled during startup, especially when combined with texture cache loading from disk on subsequent runs, leads to a severe and persistent performance degradation under common configurations. While a workaround exists by forcing specific startup states and disabling cache persistence, it is not a viable long-term solution. A proper fix addressing the underlying timing conflict during initialization is necessary to restore predictable performance and full configuration flexibility for Firestorm users on Windows, and potentially other platforms where it may also occur.


⚠️ **GitHub.com Fallback** ⚠️