Skip to content

asr: add setFrameRate function#1612

Open
AndreVto wants to merge 3 commits into
utkarshdalal:masterfrom
AndreVto:asr-framerate
Open

asr: add setFrameRate function#1612
AndreVto wants to merge 3 commits into
utkarshdalal:masterfrom
AndreVto:asr-framerate

Conversation

@AndreVto

@AndreVto AndreVto commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

Affects what refresh rate the display is supposed to use with the app

Recording

Tried a recording but the green counter didn't show up in the video
First image is uncapped FPS, 120hz, max refresh rate
Second image is capped at 50 fps, the screen decides to use 48hz
Third image is capped at 30 fps, the screen changes to 30hz

image image image

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Add frame rate control so the app can request a display refresh rate on supported devices. Adds a Java API, applies the setting to current and future windows, and persists it across app resume.

  • New Features

    • Implemented native setFrameRate(frameRate, compatibility, changeStrategy) using ASurfaceTransaction setFrameRate/WithChangeStrategy when available; falls back if needed.
    • Applies to all active surfaces in a single transaction; stores pending settings and applies on window registration.
    • Added JNI nativeSetFrameRate and public ASurfaceRenderer.setFrameRate(float, int, int) in com.winlator.renderer.
    • XServerView.setFrameRateLimit in com.winlator.widget forwards the limit with compatibility=0 (DEFAULT) and changeStrategy=1 (ALWAYS).
  • Bug Fixes

    • Persist and replay frame rate requests on surface re-creation (e.g., after returning from background) to keep the requested refresh rate consistent.
    • Clamp requested frame rate to a minimum of 30 FPS to prevent unusably low display refresh rates on devices that can drop below 30 Hz.

Written for commit 533b1f4. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added per-surface frame rate control with compatibility and change-strategy options, allowing finer frame pacing per display surface.
    • When set to a value between 0 and 30, frame rate is clamped to a minimum of 30.
  • Integration

    • Updated the renderer bridge to support the new frame-rate parameters, including applying pending settings automatically once a surface is created.
    • Enhanced XServerView so its FPS limit now applies to ASurfaceRenderer in addition to Vulkan.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds per-surface frame rate control to ASurfaceRenderer. ASurfaceRendererContext dynamically loads ASurfaceTransaction_setFrameRate and ASurfaceTransaction_setFrameRateWithChangeStrategy from libandroid.so, stores pending frame-rate state, and applies it on surface registration and via a new setFrameRate method exposed through a JNI bridge and Java layer into XServerView.setFrameRateLimit.

Changes

ASurfaceRenderer per-surface frame rate

Layer / File(s) Summary
C++ header contracts and dynamic API loading
app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.h, app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp
Declares setFrameRate public method, pendingFrameRate/Compatibility/ChangeStrategy private fields, and fnSTSetFrameRate/fnSTSetFrameRateWithChangeStrategy function pointer members; adds typedefs and invocation macros; extends loadScanoutApi() to dlsym both symbols.
setFrameRate implementation and pending-state application on registration
app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp
Implements setFrameRate(...) to write pending state and apply frame rate to all windowScMap surfaces in one transaction; updates registerWindowSC to apply non-zero pending frame rate to a newly registered surface control via a one-shot transaction.
JNI bridge and Java wiring
app/src/main/cpp/asurfacerenderer/asurface_jni.cpp, app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java
Adds nativeSetFrameRate(float, byte, byte) JNI export with shared-lock guard; updates Java private state, JNI declaration, and setFrameRate method to store pending values, apply clamping, and forward to native when surface is initialized or immediately if already initialized; adds deferred application in onSurfaceCreated.
XServerView integration
app/src/main/java/com/winlator/widget/XServerView.java
Adds ASurfaceRenderer branch in setFrameRateLimit that calls aRenderer.setFrameRate(frameRateLimit, 0, 1) alongside the existing VulkanRenderer path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • utkarshdalal/GameNative#1332: Modifies the same XServerView.setFrameRateLimit(...) call site; the retrieved PR removes render-throttling logic at that location while this PR adds the ASurfaceRenderer.setFrameRate(...) forwarding there.

Suggested reviewers

  • utkarshdalal

Poem

🐇 A surface awoke at the hop of a rate,
Transactions applied both early and late!
With dlsym in paw and pending in store,
Each frame rate is set at each new surface's door.
— signed, the rabbit who loves a smooth render 🎞️

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR description covers the basic changes with screenshots, but lacks detailed explanation of technical implementation and missing a recording as required by the checklist. Provide a recording demonstrating the frame rate control feature as indicated in the description template. Consider adding more technical details about the implementation approach and rationale.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'asr: add setFrameRate function' clearly and concisely summarizes the main change—adding a new setFrameRate function to the ASurfaceRenderer context, which is the primary focus of this pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

on some devices it affects what refresh rate the display is supposed to use with this app
@AndreVto AndreVto marked this pull request as ready for review June 23, 2026 02:16
@AndreVto AndreVto requested a review from utkarshdalal as a code owner June 23, 2026 02:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp`:
- Around line 412-419: The code has thread safety issues where pending state
variables (pendingFrameRate, pendingCompatibility, pendingChangeStrategy) are
read in the oneShot lambda without synchronization while being written
elsewhere, and the sc SurfaceControl pointer snapshots are used after the lock
is released while concurrent operations may release these handles. Fix this by
extending the mutex lock to cover all access to these pending state variables
and ensuring the sc pointer snapshots and their usage in the SurfaceTransaction
remain protected under the same mutex throughout, preventing both data races on
the pending variables and use-after-free on the SurfaceControl handles.

In `@app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java`:
- Around line 651-653: The frame-rate request made before surface initialization
in the conditional check (line 651) is silently dropped, and there is no
mechanism to replay it once the surface is created. Cache the frameRate,
compatibility, and changeStrategy parameters as instance variables whenever they
are set, and then replay the cached values by calling nativeSetFrameRate with
the stored parameters in the onSurfaceCreated method after surfaceInitialized is
set to true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1f07993-b67f-4b19-9314-b0fe45a3e976

📥 Commits

Reviewing files that changed from the base of the PR and between f9cbe32 and 725fa33.

⛔ Files ignored due to path filters (1)
  • app/src/main/jniLibs/arm64-v8a/libasurface_renderer.so is excluded by !**/*.so
📒 Files selected for processing (5)
  • app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp
  • app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.h
  • app/src/main/cpp/asurfacerenderer/asurface_jni.cpp
  • app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java
  • app/src/main/java/com/winlator/widget/XServerView.java

Comment on lines +412 to +419
if (pendingFrameRate != 0.f && (fnSTSetFrameRate || fnSTSetFrameRateWithChangeStrategy)) {
oneShot([&](void* tx) {
if (fnSTSetFrameRateWithChangeStrategy)
ST_SETFRAMERATE_CS(tx, sc, pendingFrameRate, pendingCompatibility, pendingChangeStrategy);
else
ST_SETFRAMERATE(tx, sc, pendingFrameRate, pendingCompatibility);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Strengthen lock coverage for pending state and SurfaceControl handle lifetime.

Line 455–457 writes pendingFrameRate/pendingCompatibility/pendingChangeStrategy without synchronization, while Line 412–417 reads them on another path. Also, Line 459–465 snapshots raw sc pointers and Line 469–474 uses them after unlock, while concurrent registration/unregistration can release handles (e.g., Line 426). This can cause data races and use-after-free.

🔧 Suggested fix (serialize state + handle usage under the same mutex)
 void ASurfaceRendererContext::setFrameRate(float frameRate,
                                            int8_t compatibility, int8_t changeStrategy)
 {
     if (!fnSTSetFrameRate && !fnSTSetFrameRateWithChangeStrategy) return;
-
-    pendingFrameRate      = frameRate;
-    pendingCompatibility  = compatibility;
-    pendingChangeStrategy = changeStrategy;
-
-    std::vector<void*> scs;
-    {
-        std::lock_guard<std::mutex> lk(windowScMutex);
-        scs.reserve(windowScMap.size());
-        for (auto& [id, sc] : windowScMap)
-            scs.push_back(sc);
-    }
-    if (scs.empty()) return;
-
-    void* tx = ST_CREATE();
-    for (void* sc : scs) {
-        if (fnSTSetFrameRateWithChangeStrategy)
-            ST_SETFRAMERATE_CS(tx, sc, frameRate, compatibility, changeStrategy);
-        else
-            ST_SETFRAMERATE(tx, sc, frameRate, compatibility);
-    }
-    ST_APPLY(tx);
-    ST_DELETE(tx);
+    oneShot([&](void* tx) {
+        std::lock_guard<std::mutex> lk(windowScMutex);
+        pendingFrameRate      = frameRate;
+        pendingCompatibility  = compatibility;
+        pendingChangeStrategy = changeStrategy;
+        for (auto& [id, sc] : windowScMap) {
+            if (fnSTSetFrameRateWithChangeStrategy)
+                ST_SETFRAMERATE_CS(tx, sc, frameRate, compatibility, changeStrategy);
+            else
+                ST_SETFRAMERATE(tx, sc, frameRate, compatibility);
+        }
+    });
 }

Also applies to: 455-475

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp` around lines
412 - 419, The code has thread safety issues where pending state variables
(pendingFrameRate, pendingCompatibility, pendingChangeStrategy) are read in the
oneShot lambda without synchronization while being written elsewhere, and the sc
SurfaceControl pointer snapshots are used after the lock is released while
concurrent operations may release these handles. Fix this by extending the mutex
lock to cover all access to these pending state variables and ensuring the sc
pointer snapshots and their usage in the SurfaceTransaction remain protected
under the same mutex throughout, preventing both data races on the pending
variables and use-after-free on the SurfaceControl handles.

Comment thread app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java">

<violation number="1" location="app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java:649">
P2: Frame-rate request is silently dropped when the surface is not initialized; no pending state is stored for later application.</violation>

<violation number="2" location="app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java:649">
P2: Unchecked int-to-byte narrowing may corrupt frame-rate mode parameters passed to native layer</violation>
</file>

<file name="app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp">

<violation number="1" location="app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp:412">
P1: Unsynchronized cross-thread access to pending frame-rate state causes a C++ data race and potentially inconsistent tuples applied to new windows.</violation>

<violation number="2" location="app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp:455">
P2: No validation of frameRate, compatibility, or changeStrategy before persisting and applying them globally to all surfaces. Negative or NaN frame rates and invalid enum values pass through to the Android compositor.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

windowScMap[contentId] = sc;
}

if (pendingFrameRate != 0.f && (fnSTSetFrameRate || fnSTSetFrameRateWithChangeStrategy)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unsynchronized cross-thread access to pending frame-rate state causes a C++ data race and potentially inconsistent tuples applied to new windows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp, line 412:

<comment>Unsynchronized cross-thread access to pending frame-rate state causes a C++ data race and potentially inconsistent tuples applied to new windows.</comment>

<file context>
@@ -402,6 +408,16 @@ void ASurfaceRendererContext::registerWindowSC(int64_t contentId, const char* de
         windowScMap[contentId] = sc;
     }
+
+    if (pendingFrameRate != 0.f && (fnSTSetFrameRate || fnSTSetFrameRateWithChangeStrategy)) {
+        oneShot([&](void* tx) {
+            if (fnSTSetFrameRateWithChangeStrategy)
</file context>

return false;
}

public void setFrameRate(float frameRate, int compatibility, int changeStrategy) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Unchecked int-to-byte narrowing may corrupt frame-rate mode parameters passed to native layer

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java, line 649:

<comment>Unchecked int-to-byte narrowing may corrupt frame-rate mode parameters passed to native layer</comment>

<file context>
@@ -645,6 +646,13 @@ private boolean isFullscreenWindow(Window window) {
         return false;
     }
 
+    public void setFrameRate(float frameRate, int compatibility, int changeStrategy) {
+        Timber.d("setFrameRate frameRate=%f compatibility=%d changeStrategy=%d", frameRate, compatibility, changeStrategy);
+        if (surfaceInitialized) {
</file context>

Comment thread app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java
{
if (!fnSTSetFrameRate && !fnSTSetFrameRateWithChangeStrategy) return;

pendingFrameRate = frameRate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: No validation of frameRate, compatibility, or changeStrategy before persisting and applying them globally to all surfaces. Negative or NaN frame rates and invalid enum values pass through to the Android compositor.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/cpp/asurfacerenderer/ASurfaceRendererContext.cpp, line 455:

<comment>No validation of frameRate, compatibility, or changeStrategy before persisting and applying them globally to all surfaces. Negative or NaN frame rates and invalid enum values pass through to the Android compositor.</comment>

<file context>
@@ -430,3 +446,32 @@ void ASurfaceRendererContext::unregisterWindowSC(int64_t contentId) {
+{
+    if (!fnSTSetFrameRate && !fnSTSetFrameRateWithChangeStrategy) return;
+
+    pendingFrameRate      = frameRate;
+    pendingCompatibility  = compatibility;
+    pendingChangeStrategy = changeStrategy;
</file context>

AndreVto added 2 commits June 23, 2026 02:36
record frame rate changes is necessary to replay it after opening the app back from background
limit it to 30 FPS so the app is not unusable on lower values
example: my phone can go to as low as 10hz, limit it to 30hz at least

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java">

<violation number="1" location="app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java:658">
P2: Hardcoded minimum 30 FPS silently overrides valid lower requested frame-rate limits</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic


public void setFrameRate(float frameRate, int compatibility, int changeStrategy) {
Timber.d("setFrameRate frameRate=%f compatibility=%d changeStrategy=%d", frameRate, compatibility, changeStrategy);
if (frameRate > 0 && frameRate < 30) frameRate = 30;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Hardcoded minimum 30 FPS silently overrides valid lower requested frame-rate limits

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java, line 658:

<comment>Hardcoded minimum 30 FPS silently overrides valid lower requested frame-rate limits</comment>

<file context>
@@ -655,6 +655,7 @@ private boolean isFullscreenWindow(Window window) {
 
     public void setFrameRate(float frameRate, int compatibility, int changeStrategy) {
         Timber.d("setFrameRate frameRate=%f compatibility=%d changeStrategy=%d", frameRate, compatibility, changeStrategy);
+        if (frameRate > 0 && frameRate < 30) frameRate = 30;
         pendingFrameRate = frameRate;
         pendingFrameRateCompatibility = (byte) compatibility;
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant