Panda3D issues with Tkinter, FPS, multiple windows, RenderPipeline, Fatal thread errors

Hi all,

I’m new to Panda3D, and trying to get it to work properly with my existing Tkinter-based application. I’ve searched the community but can’t seem to find the right answers except some of them seem to have been asked many years ago, but are not addressed.

Some of these questions may be general, I appreciate any help.

Panda Version: Panda3D-1.10.4.1-x64; Python 3.7.4
Windows 10, AlienWare laptop Intel-i9 w/GTX 1080.

I am able to get a Panda3D window running inside a Tkinter frame, using the standard rendering and the RenderPipeline PBR materials.

  1. Is it normal for my FPS to dramatically drop in this configuration? I can get 350+ FPS in a 500x500 RenderPipeline Panda window without Tkinter. Putting the same window into a Tkinter frame in a very simple Tkinter toplevel drops to 40 FPS.

  2. PyEval_RestoreThread: NULL tstate errors. I’ve seen many posts about this dating back years but no clear answer. They happen seemingly randomly, but maybe not. They don’t seem to happen on Tkinter button pressing, or bound Leave/Enter widget events. They happen when a new Toplevel is opened from another Toplevel when that underlying Toplevel has the Panda frame. Yet sometimes they work fine, other times the error hits. I can open/close the new Toplevel several times and it’ll be good, then it will Fatal error, other times I can’t open it once without the error. This issue is independent of whether the Panda frame is using RenderPipeline PBR or not. The error can also be triggered if the new Toplevel opens and I try to move it too much.

  3. Resizing/Moving a Tkinter Toplevel or root with an embedded Panda frame. Again, I’ve seen this in the forums dating back many years but I don’t know if there’s a solution. I feel like I once in experimenting did find the solution to this, but now something I’ve done has reintroduced it. But basically any resizing or moving of a Tkinter window with a Panda frame seems like it loses control over whether mouse_L is pressed, causing it to jump back to the original position/size. Sometimes it works. This is also independent on whether it’s the PBR RenderPipe or not.

  4. EDIT: Tkinter window with embedded Panda frame doesn’t return focus to entry/text widgets after a mouse click on the Panda frame. Using the default window with camera controls, a mouse click event there requires a focus_force() on entry/text widgets, but not other widgets? ie: after rotating the camera in the TK Panda frame, a click event in the rest of the TK toplevel is caught such as a button widget, but a click into an entry/text widget isn’t, and requires manually using focus_force() back into the widget to enable it?

  5. Multiple Panda windows inside multiple Tkinter frames. I feel like I had this working without the PBR RenderPipeline… except the default mouse commands on one window affect all the other windows? Is it possible to have multiple independent Panda windows operating in multiple Tkinter toplevels simultaneously where the default mouse bindings on window1 do not affect window2 (ie: the zoom, rotate, etc.). I was never able to get multiple Panda windows running with RenderPipeline at all. After getting the first Panda window using PBR and initializing the RenderPipeline, all subsequent windows revert to non-PBR. But I can’t initialize/create a new RenderPipeline after the first one or it throws Windows font initialization errors?

  6. I feel like some of these issues could be solved by catching the window events and pausing the Panda render. Is there a way to freeze the Panda render frame as an image and then manually unpause the rendering?

Thanks for any help. I’m still experimenting over here.

Steve

1 - Your configuration is too complex. You should check the RP demos and look how many FPS you can get. Something like 40-50 FPS will be fine on default RP plugin’s set, that’s because of the heavy shaders.

5 - I think you will need to modify RP to make it work. RP attaches itself to default window and camera, and it also registers them as global variables.

For your second and third issues, it might relate to this issue on the Panda3D GitHub which was subsequently “fixed” in this commit. You’ll need to enable disable-message-loop in your configuration file for this to work.

The fix that @frozenstranger references requires Panda3D 1.10.5, by the way, so I recommend upgrading.

Hello, I’m wondering if there have been any developments in solving these issues? I’m primarily interested in issues 3 and 4, as I have not (to this point) come across the others listed here.

Regarding issue 3, the resizing of the panda window when embedded in a tkinter frame. I attempted to utilize the resizing code I came across elsewhere:

self.frame.bind("<Configure>",self.resized)
def resized(self,event):
		self.frame.update()
		self.frame.update_idletasks() 
		w = self.frame.winfo_width()
		h = self.frame.winfo_height()
		
		props = WindowProperties() 
		props.setSize(w, h) 
		
		self.base.win.requestProperties(props)

However I get the following error:

AttributeError: 'NoneType' object has no attribute 'requestProperties'

It seems to be when using windowtype=“none” this method of sizing isn’t a viable option. Throughout my research into this problem I have not come across another solution.

Regarding issue 4, I attempted to use the force_set() in conjunction with a binded mouse click to the entry widget - this was unsuccessful.

Any help in overcoming these issues would be much appreciated.

The strangest thing is that resizing the window works.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties

class Tkinter_window(ShowBase):
    def __init__(self):
        ShowBase.__init__(self, windowType='none')
        self.startTk()
        
        self.frame = self.tkRoot
        self.frame.bind("<Configure>", self.resized)
        self.frame.update()
        
        props = WindowProperties()
        props.setParentWindow(self.frame.winfo_id())
        props.setOrigin(0, 0)
        props.setSize(self.frame.winfo_width(), self.frame.winfo_height())

        self.win = base.makeDefaultPipe()
        base.openDefaultWindow(props = props)

        scene = self.loader.loadModel("environment")
        scene.reparentTo(render)

    def resized(self, event):
    
        self.frame.update()
        self.frame.update_idletasks()
        
        props = WindowProperties()
        props.setOrigin(0, 0)
        props.setSize(self.frame.winfo_width(), self.frame.winfo_height())
        
        self.win.requestProperties(props)

tkinter_window = Tkinter_window()
tkinter_window.run()

I found a strange solution.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties

class Tkinter_window(ShowBase):
    def __init__(self):
        ShowBase.__init__(self, windowType='none')
        self.startTk()
        
        self.frame = self.tkRoot
        self.frame.bind("<Configure>", self.resized)
        self.frame.update()
        
        props = WindowProperties()
        props.setParentWindow(self.frame.winfo_id())
        props.setOrigin(0, 0)
        props.setSize(self.frame.winfo_width(), self.frame.winfo_height())

        self.win = base.makeDefaultPipe()
        base.openDefaultWindow(props = props)

        scene = self.loader.loadModel("environment")
        scene.reparentTo(render)

    def resized(self, event):

        self.frame.update()
        self.frame.update_idletasks()
        
        props = WindowProperties()
        props.setOrigin(0, 0)
        props.setSize(self.frame.winfo_width(), self.frame.winfo_height())

        if self.win is not None:
            self.win.requestProperties(props)

tkinter_window = Tkinter_window()
tkinter_window.run()

The correct solution is to create a binding when the Panda3D context has been created.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties

class Tkinter_window(ShowBase):
    def __init__(self):
        ShowBase.__init__(self, windowType='none')
        self.startTk()
        
        self.frame = self.tkRoot
        self.frame.update()
        
        props = WindowProperties()
        props.setParentWindow(self.frame.winfo_id())
        props.setOrigin(0, 0)
        props.setSize(self.frame.winfo_width(), self.frame.winfo_height())

        self.win = base.makeDefaultPipe()
        base.openDefaultWindow(props = props)
        
        self.frame.bind("<Configure>", self.resized)

        scene = self.loader.loadModel("environment")
        scene.reparentTo(render)

    def resized(self, event):

        self.frame.update()
        self.frame.update_idletasks()
        
        props = WindowProperties()
        props.setOrigin(0, 0)
        props.setSize(self.frame.winfo_width(), self.frame.winfo_height())
        
        self.win.requestProperties(props)

tkinter_window = Tkinter_window()
tkinter_window.run()

Yes! Your solution works, thanks!

Any ideas about issue 4? Why I cannot edit tkinter entry widget when panda3d is running?

No ideas, but the workaround with forced focus setting works.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties
import tkinter

class Tkinter_window(ShowBase):
    def __init__(self):
        ShowBase.__init__(self, windowType='none')
        self.startTk()
        
        self.root = self.tkRoot
        self.root.geometry("600x300")
        self.root
        
        self.frame_panda3d = tkinter.Frame(self.root)
        self.frame_panda3d.place(x = 0, y = 0, height = 300, width = 400)
        
        frame_panel = tkinter.Frame(self.root)
        frame_panel.place(x = 400, y = 0, height = 300, width = 200)
        
        self.text = tkinter.Text(frame_panel, width = 25, height = 5, bg = "darkgreen", fg = "white", wrap = tkinter.WORD)
        self.text.bind("<Button-1>", self.set_focus)
        self.text.pack()
        
        self.root.update()
        
        props = WindowProperties()
        props.setParentWindow(self.frame_panda3d.winfo_id())
        props.setOrigin(0, 0)
        props.setSize(self.frame_panda3d.winfo_width(), self.frame_panda3d.winfo_height())

        self.win = base.makeDefaultPipe()
        base.openDefaultWindow(props = props)

        scene = self.loader.loadModel("environment")
        scene.reparentTo(render)

    def set_focus(self, event):
        self.text.focus_force()

tkinter_window = Tkinter_window()
tkinter_window.run()