DearPyGui icon indicating copy to clipboard operation
DearPyGui copied to clipboard

get_text_size() is unstable

Open v-ein opened this issue 2 years ago • 1 comments
trafficstars

Version of Dear PyGui

Version: 1.9.1 Operating System: Windows 10

My Issue/Question

Got one more race condition: get_text_size sometimes returns [0, 0] even though other calls from the same place return text size as expected.

The issue occurs when:

  • the font parameter is not specified (i.e. get_text_size is supposed to use the default font), and
  • get_text_size is called right at the start of a new frame, between ImGui::NewFrame and Render().

The latter is usually hard to achieve, which makes the issue quite rare in a typical DPG project. However, if you overload the handlers thread (e.g. do a heavy initialization in a callback), or if you call DPG from a user thread (threading.Thread), then the chances to get into this condition are much higher.

To Reproduce

Steps to reproduce the behavior:

  1. Run the sample below
  2. Look at the console and wait for messages to appear. The script prints a message each time get_text_size returns [0, 0].

A typical output (confirming the bug) might look like this:

frame=554, sz=[0.0, 0.0]
frame=1692, sz=[0.0, 0.0]
frame=1719, sz=[0.0, 0.0]
frame=1722, sz=[0.0, 0.0]

In the example code, I overload the handlers thread by doing a time.sleep in each handler call. In real projects, there's no need for sleep: a typical heavy initialization may fill the entire gap between two consecutive frames, thus putting get_text_size call into the unlucky moment between ImGui::NewFrame and Render().

Expected behavior

Always return valid text size, consistent between calls in identical conditions.

Screenshots/Video

None.

Standalone, minimal, complete and verifiable example

import time
import dearpygui.dearpygui as dpg

dpg.create_context()
dpg.setup_dearpygui()
dpg.create_viewport(title="Test", width=800, height=800)

def on_visible(s, a):
    # Emulating hard work
    time.sleep(0.001)

    sz = dpg.get_text_size('test text')
    frame = dpg.get_frame_count()
    if sz is not None and sz[0] == 0:
        print(f"frame={frame}, sz={sz}")

with dpg.item_handler_registry() as event_handlers:
    dpg.add_item_visible_handler(callback=on_visible)

with dpg.window(pos=(0, 0), width=800, height=800, label="Hey") as wnd:
    # The handlers queue can only keep up to 50 events, so it doesn't make sense
    # to generate more than 50 events per frame.
    with dpg.group(horizontal=True):
        for i in range(50):
            dpg.add_text(".")
            dpg.bind_item_handler_registry(dpg.last_item(), event_handlers)

dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

v-ein avatar Aug 27 '23 21:08 v-ein

Here's how it typically works:

  • If font is not specified, get_text_size relies on the default font - or, rather, the current font in ImGui at the time get_text_size is called. Otherwise it temporarily sets current font to whatever is passed in, measures text size, and restores the previous "current font".
  • Now, the "current font" depends on when get_text_size gets called. Since get_text_size locks the mutex before doing anything, its call typically occurs after the entire frame has been rendered, and the current font happens to be the default font (which can be changed with dpg.bind_font).
  • Font size of the current font is stored in a separate variable, g.FontSize, and changed in a couple of places along the rendering process. Luckily (and it's just pure luck!) the last window to be rendered is a hidden ImGui window named Debug##Default, and it sets font size to the size of the default font. If this window were not present, the size picked up by get_text_size would depend on which window renders last.
    • We can probably ignore this and hope for the best.
  • g.FontSize is used to compute a scaling factor that affects calculations (scale in ImFont::CalcTextSizeA).
    • As said above, g.FontSize typically contains the size of the default font by the time get_text_size is called.

And here's how it breaks:

  • ImGui::NewFrame calls ImGui::SetCurrentFont to reset the font.
  • SetCurrentFont attempts to obtain font size from the window being rendered (g.CurrentWindow), and store it in g.FontSize.
    • Since we're only initializing a new frame, there's no current window, and font size gets reset to zero.
  • Upon return from ImGui::NewFrame, DPG backend calls Render, which locks the mutex, thus preventing get_text_size from picking up random font size values during rendering.
    • While the rendering thread is still in ImGui::NewFrame, the mutex is not locked yet, and get_text_size can pick up zero font size set by ImGui::SetCurrentFont!
    • The zero font size effectively sets the scaling factor to 0.0, zeroing the resulting measurements.

One can see that there's little chance for get_text_size to break since g.FontSize is being zero for a very short time span. Also, since this function is typically called from callbacks and handlers, there's little chance to hit that time interval: the handlers are usually fast, and complete before the new frame begins. However, with too much content to render, or with heavily-loaded handlers thread, a handler can be delayed until the beginning of the new frame, thus exposing this issue. Another way to recreate it is to call DPG API from a user thread, which is not bound to how frames are rendered, and can call get_text_size at any moment, including the start of a new frame.

v-ein avatar Aug 28 '23 07:08 v-ein