Skip to content

Conversation

@hntirgeam
Copy link

Summary

Implements configurable OSD refresh rate via osd_framerate_hz CLI command.

Fixes mentioned here behaviour.

Problem

INAVs element drawing updates only one element per frame. With many enabled elements (e.g., 30), each element updates at ~2Hz (62.5Hz / 30) which doesn't help when flying in harsh conditions. Only artificial horizon and telemetry updates as fast as possible (each frame)

Solution

  • Added osd_framerate_hz setting (range: -1 to 60 Hz)
    • -1 (default): Legacy (current) mode
    • 1-60: all elements update at specified Hz
    • 0: all elements update each frame

Video showing default behaviour, osd_framerate_hz=12 (default in betaflight) and osd_framerate_hz=0 (each frame)

osd_framerate_hz_hd.mp4

@github-actions
Copy link

Branch Targeting Suggestion

You've targeted the master branch with this PR. Please consider if a version branch might be more appropriate:

  • maintenance-9.x - If your change is backward-compatible and won't create compatibility issues between INAV firmware and Configurator 9.x versions. This will allow your PR to be included in the next 9.x release.

  • maintenance-10.x - If your change introduces compatibility requirements between firmware and configurator that would break 9.x compatibility. This is for PRs which will be included in INAV 10.x

If master is the correct target for this change, no action is needed.


This is an automated suggestion to help route contributions to the appropriate branch.

@hntirgeam hntirgeam changed the base branch from master to maintenance-9.x December 24, 2025 22:05
@hntirgeam
Copy link
Author

Can also be ported to 8.x.x (which I tested on). I can open another PR if needed.

@MrD-RC
Copy link
Member

MrD-RC commented Dec 24, 2025

There won't be any more updates for 8.x. This would need to go in to 9.1.

@MrD-RC
Copy link
Member

MrD-RC commented Dec 24, 2025

I'm not sure updating all elements every cycle is a good idea. If there are a lot of elements. This could cause issues.

Maybe instead of a timeout where all elements are updated. Maybe just set the number of elements to be updated each cycle. Maybe with a range of 1 to 10.

1 would be the current way, with a single element updated each cycle.

10 would be 10 elements per cycle. I think that would be plenty fast enough.

With a 30 element OSD and this set to 10. All elements would be updating at ~21Hz. Thats more than fast enough. Setting to 5 and them updating at ~10Hz would be fast enough too.

@sensei-hacker
Copy link
Member

sensei-hacker commented Dec 25, 2025

Maybe instead of a timeout where all elements are updated. Maybe just set the number of elements to be updated each cycle. Maybe with a range of 1 to 10.

Yeah. This is the way. After a certain number of microseconds, it's going to be time for the PID task to run. Rather than the OSD task occasionally trying to take far more than the available time, it should consistently do what it can in the time available. Consistently do two or three elements each cycle.

Same as you might work 8 hours per day. Not try to work 40 hours one day per week.

@hntirgeam
Copy link
Author

hntirgeam commented Dec 25, 2025

I've tested this change on my SPEEDYBEEF405WING (9.0.0) using a layout populated with nearly all available OSD elements:

image

For testing purposes I used this code:

static void osdDrawAllElements(void)
{
    uint32_t startUs = micros();
    uint8_t count = 0;

    for (uint8_t element = 0; element < OSD_ITEM_COUNT; element++) {
        if (osdDrawSingleElement(element)) count++;
    }

    osdDrawSingleElement(OSD_ARTIFICIAL_HORIZON);
    if (osdConfig()->telemetry>0){
        osdDisplayTelemetry();
    }

    uint32_t elapsedUs = micros() - startUs;
    osdPerf.lastTimeUs = elapsedUs;
    osdPerf.elementsDrawn = count;
    if (elapsedUs > osdPerf.maxTimeUs) {
        osdPerf.maxTimeUs = elapsedUs;
    }

    DEBUG_SET(DEBUG_OSD_PERF, 0, osdPerf.lastTimeUs);
    DEBUG_SET(DEBUG_OSD_PERF, 1, osdPerf.maxTimeUs);
}

Results I got:

debugging

With an intentionally overloaded layout, a full OSD frame render takes approximately 1-1.4ms, using 6-9% of the 16ms budget available at 62.5Hz (250Hz drawScreen() / DRAW_FREQ_DENOM)


With a more realistic, moderate OSD setup, render time is ~0.4ms (2.5% of available time), which is effectively nothing from a performance perspective.

osd_setup_moderate

Based on these measurements, I believe that adding this CLI option will make OSD behavior clearer and easier to configure, reducing cases where users have to guess why the OSD seems to update slowly (as in already mentioned #9907).

Setting its default value to -1 (which preserves the current INAV rendering behaviour) will also ensures that it does not affect the existing userbase.

The data shows that even with a large number of active elements, rendering time is not the limiting factor: even in the worst-case layout it is around 1 ms per frame


Same as you might work 8 hours per day. Not try to work 40 hours one day per week.

Fortunately, I’m not a microcontroller 😅

@Jetrell
Copy link

Jetrell commented Dec 25, 2025

I agree with @sensei-hacker I've experienced some odd lag while in flight at times, when the OSD is highly populated and a number of other features are also active. It can ranging from the OSD itself just lagging, to other more critical systems being slowed. I'm not saying this isn't related to a problem somewhere else. But caution should be taken here.

@hntirgeam Can I assume your testing was done on the bench, and not inflight ?
INAV can also run other features that Betaflight doesn't. Like the Programming framework. And being able to switch between up to 3 different profiles. Which can vary considerably in what operations they may call.

@hntirgeam
Copy link
Author

hntirgeam commented Dec 25, 2025

@Jetrell, Testing was done on bench in armed state with GPS/PID/telemetry and active blackbox logging enabled. I understand the concern about CPU load with other features active. However, there's an issue with the batch approach:

It increases CPU usage rather than reducing it.

CPU Usage Comparison

Bench measurements with 40 active OSD elements:

Hz-based approach at 12Hz:

  • Per-frame time: 1400μs maximum (1000μs average)
  • Drawing calls: 12 times/second
  • Total CPU time: 16800μs/sec = 1.68%
  • Update rate: 12Hz for all elements

UPD: calculations above are NOT for 40 elements, but for MUCH MORE amount of elements mentioned in previous comment. For 40 elements, as said, its around 410μs per full frame.

Batch approach with 5 elements/cycle (as suggested here #11207):

  • Per-cycle time: ~350μs (based on measurements that a single element update took 60-73 µs)
  • Drawing calls: 62.5 times/second (every OSD cycle)
  • Total CPU time: 21875μs/sec = 2.19%
  • Update rate: 10.4Hz for 30 elements (slower than Hz-based)

Batch approach with 10 elements/cycle:

  • Per-cycle time: 700μs
  • Drawing calls: 62.5 times/second (every OSD cycle)
  • Total CPU time: 43750μs/sec = 4.38%
  • Update rate: 15.6Hz for 30 elements (faster than Hz-based but at cost of CPU time)

The batch approach uses 30% more CPU (2.19% vs 1.68%) while providing a slower update rate (10.4Hz vs 12Hz).

The batch approach calls the drawing function 62.5 times/second regardless of settings. Each call has overhead:
Function entry/exit, loop iteration and state tracking, conditional checks for element count, etc.

The Hz-based approach calls the drawing function only as often as configured (12 times/second). Total time spent rendering is similar, but the batch approach adds 5x more function call overhead.

User Configuration

Batch approach: set elements_per_cycle = 5

  • With 25 elements: 12.5Hz update rate
  • With 30 elements: 10.4Hz update rate
  • User must calculate actual refresh rate based on element count (Adding/removing 1 element changes refresh rate) -- not so obvious behaviour, huh?

Hz-based approach: set osd_framerate_hz = 12

  • Always 12Hz regardless of element count
  • User directly controls refresh rate (almost every person can understand what "12 times per second" means)

The batch approach doesn't reduce total CPU time - it spreads the same work across more calls while adding overhead
At 12Hz with 1.68% CPU usage, there's 98.32% available for programming, profile switching, and other features

If a 1.4ms operation at 12Hz causes issues, a 350μs operation at 62.5Hz won't help - the total CPU time is higher. The safety mechanism is the -1 default value, which preserves current behavior. Users opt-in to higher refresh rates based on their needs.

@MrD-RC
Copy link
Member

MrD-RC commented Dec 25, 2025

There is a big issue with your method though. It is not using the osdIncElementIndex function. This ensures that the correct next element is drawn. You are trying to draw everything, whether it is valid or not.

@sensei-hacker
Copy link
Member

sensei-hacker commented Dec 25, 2025

~0.4ms (2.5% of available time), which is effectively nothing

The PID loop needs to run every 0.5 ms. The PID loop itself takes some time, so figure there is maybe 0.3ms - 0.4ms in between for everything else to run. 0.4ms is more than total time available for everything but PID in that loop.

"Total average CPU usage" is not really a measurement of interest. That's just the amount of time the CPU is sitting idle, doing nothing. It is not helpful to block the PID loop for a while, then have the CPU idle for a while.

What matters is that every task completes in time, every time.

@sensei-hacker
Copy link
Member

sensei-hacker commented Dec 25, 2025

@hntirgeam

Fortunately, I’m not a microcontroller

There are not 40 hours in a day, regardless of whether you're a microcontroller or a pineapple. Nothing can work for 40 hours within a 24 hour day.

Similarly, you can't work for 1,400 us within the 500us interval of PID starts. (And the PID loop itself takes time, so there is about 300us - 400us between PID task runs).

You can't lock up the MCU and block the PID loop three times in a row. "But later on I do nothing while the MCU is idle" doesn't make it okay to block essential flight functions.

That's like saying "I wasn't speeding because after I drove 150 MPH, I parked the car for a while". Doing nothing later doesn't undo the damage.

@hntirgeam
Copy link
Author

Yes, I realize that. I incorrectly assumed that INAV takes a slightly different approach to task scheduling.

I am currently thinking about a proper solution to this problem.

The batch processing approach seems more appealing now, but still less user friendly than specifying the OSD refresh rate as in Betaflight.

@sensei-hacker
Copy link
Member

sensei-hacker commented Dec 25, 2025

If you think setting refresh rate is more user friendly, you could do that. Then the number of elements to update each time is just activeElements / (1/ rate ) or whatever.

I wonder if there is a good way to communicate to users that you don't necessarily want to max this out. 🤔

I haven't closely at either code, but something else to be aware of:
Consider that your task may not run at the requested rate, if there are higher priority tasks. You don't want to end up in a situation where the first 8 elements get updated each time, and the last 4 never get updated. So you have to consider which elements are overdue for an update.

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.

4 participants