DearPyGui
DearPyGui copied to clipboard
get_text_size() is unstable
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
fontparameter is not specified (i.e.get_text_sizeis supposed to use the default font), and get_text_sizeis called right at the start of a new frame, betweenImGui::NewFrameandRender().
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:
- Run the sample below
- Look at the console and wait for messages to appear. The script prints a message each time
get_text_sizereturns[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()
Here's how it typically works:
- If
fontis not specified,get_text_sizerelies on the default font - or, rather, the current font in ImGui at the timeget_text_sizeis 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_sizegets called. Sinceget_text_sizelocks 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 withdpg.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 namedDebug##Default, and it sets font size to the size of the default font. If this window were not present, the size picked up byget_text_sizewould depend on which window renders last.- We can probably ignore this and hope for the best.
g.FontSizeis used to compute a scaling factor that affects calculations (scaleinImFont::CalcTextSizeA).- As said above,
g.FontSizetypically contains the size of the default font by the timeget_text_sizeis called.
- As said above,
And here's how it breaks:
ImGui::NewFramecallsImGui::SetCurrentFontto reset the font.SetCurrentFontattempts to obtain font size from the window being rendered (g.CurrentWindow), and store it ing.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 callsRender, which locks the mutex, thus preventingget_text_sizefrom picking up random font size values during rendering.- While the rendering thread is still in
ImGui::NewFrame, the mutex is not locked yet, andget_text_sizecan pick up zero font size set byImGui::SetCurrentFont! - The zero font size effectively sets the scaling factor to 0.0, zeroing the resulting measurements.
- While the rendering thread is still in
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.