CPU-friendly Panda/wxPython

Background:

The work I’m doing requires a mostly static 3d world, with changes happening solely at the request of the user. I have wrapped Panda inside a wx window, using code similar to [url]integrating a wxPython GUI into a Panda app].

Goal:

The CPU should not be burning out an entire core just rendering the scene at 60fps or whatever.

Attempt #1:

Tried following suggestions for decreasing Panda3d CPU usage, such as mentioned at [url]Panda use a lot of CPU even if nothing is displayed].

loadPrcFileData('yield-timeslice', '#t')
loadPrcFileData('sync-video', '#f')

It does not help. I believe this is because Panda does not own the main loop, since wx does. Wx’s main loop is as follows. Note that it is perfectly capable of maxing out a core since it is in effect an infinite loop.

def wx_loop(self, task):
    while self.evtloop.Pending():
        self.evtloop.Dispatch()
    self.ProcessIdle()
    sleep(0) # to be discussed soon
    return Task.cont

Attempt #2:

If I can’t get Panda to chill, the next logical step is to convince wxPython to give the rat race a break. Searching on Google for “wxpython yield timeslice” brought me to this Allegro thread that described a function in their framework that yields the remainder of its timeslice to the next process. The thread implied that using that function would decrease CPU usage. Unfortunately, there does not seem to be an implementation of yield-timeslice() in Python, Panda3D, and wxPython.

Attempt #3:

Following up on the yield-timeslice() idea, I looked for a way to yield a thread in Python. I found a discussion about “Threading unfairness” that states:

I was already using the line from [url]Panda use a lot of CPU even if nothing is displayed] that calls time.sleep(0.01), but now I changed it to the following. No luck.

time.sleep(0)

Call for Help:

Has anybody wanted to minimize CPU usage when running Panda in a wx frame? Does anyone have more suggestions to try in addition to the list above?

Thanks!

But in the code you’re posting, it appears that Panda does indeed own the main loop. The wx loop you post is a Panda task. When it finishes, it returns task.cont, which allows Panda to service the rest of its tasks and then cycle the main loop.

In any case, putting a task that calls time.sleep(0.01) or any nonzero value should certainly stop the CPU from running away. If it doesn’t, something is very wrong.

David

Here is the almost-minimum amount of code to get Panda and wx to work together as they do in my case. I can’t get even this code to run at less than 100% of a core.

import sys
import wx
import direct.directbase.DirectStart
from direct.task import Task
from direct.showbase import DirectObject
from pandac.PandaModules import *
import time

class PandaGui(DirectObject.DirectObject):

    def __init__(self):
        # set up Panda low-level configuration
        loadPrcFileData('startup', 'window-type none')
        loadPrcFileData('startup', 'aux-display tinydisplay')
        loadPrcFileData('yield-timeslice', '#t')
        loadPrcFileData('sync-video', '#f')

        # replace wx event loop with custom one
        self.wxgui = wx.PySimpleApp()
        self.evtloop = wx.EventLoop()
        self.oldWxEventLoop = wx.EventLoop.GetActive()
        wx.EventLoop.SetActive(self.evtloop)
        taskMgr.add(self.wx_loop, 'Custom wx event loop')

        # link panda to the wx frame and set some properties
        frame = wx.Frame(None, wx.ID_ANY, 'Title')
        frame.SetClientSize((600, 400))
        windowProps = WindowProperties.getDefault()
        windowProps.setOrigin(0, 0)
        windowProps.setForeground(False)
        windowProps.setParentWindow(frame.GetHandle())
        base.openDefaultWindow(props=windowProps)

        frame.Bind(wx.EVT_CLOSE, lambda x:sys.exit(0))
        frame.Show(True)

    def wx_loop(self, task):
        while self.evtloop.Pending():
            self.evtloop.Dispatch()
        self.wxgui.ProcessIdle()
        time.sleep(0)
        return Task.cont

if __name__ == '__main__':
    gui = PandaGui()
    run()

What if you follow the advice given by drwr, and change

time.sleep(0)

to

time.sleep(0.01)

Larger values should reduce the CPU load some more, but your GUI might become unresponsive.

Exactly. It “works”, but only by slowing down the entire program. What I’d like is the equivalent of what wxPython does by default. When you run the basic hello world example (see below), the CPU doesn’t even twitch. I’d like Panda to do the same, even if it means disabling the entire Panda event queue and only redrawing when I want.

import wx

app = wx.App(False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show(True)
app.MainLoop()

Does it actually slow down the program, so that you can perceive that it is running slower, or do you just know that it’s slowing down the program so you don’t want to do it? Because a sleep of 1/100th of a second per frame should not have any perceptible effect on program responsiveness.

There is a difference in philosophy between 2-d toolkits like wx, and 3-d toolkits like Panda. In the former, applications are almost entirely event-driven, and typically spend large amounts of time doing nothing while waiting for user input. In the latter, applications are usually rich and heavily animated, and user input is used to guide the experience, not control it. Furthermore, the 3-d hardware won’t refresh the buffer contents to the window automatically, so if the window is covered and then uncovered, an immediate redraw is necessary. Also, a 3-d window is usually the user’s primary interface while it is running.

So Panda, in keeping with this typical philosophy of 3-d engines, focuses more on rendering every frame, than on rendering only when something changes.

If you only want to render the scene when something changes, then you can remove the igloop task and call base.graphicsEngine.renderFrame() only when you know the window needs redrawing. There are a few subtleties with this, though–you may want to move the clock ticking into the task manager, for instance, and a few other things that I’d have to think about a little longer.

But at the end of the day, are you really sure you need to go through all this trouble? If your goal is reducing CPU utilization without impacting responsiveness, a sleep should be sufficient, and if it isn’t I’d like to know more about it. But if your goal is bending Panda to follow the typical 2-d event philosophy for some more abstract reason, then you do need to do all this.

David

I’m finding that up to 0.005 secs of sleep is unnoticeable, and drops my CPU from 48% to 28%. Any more sleep than that and strafing etc is a little jittery, and the wx menubar is slower to respond.

This issue seems like a limitation of Panda’s implementation rather than a philosophy. As you mentioned, it would be manageable to fix Panda or work around it, but my code is just a prototype so I’ll live with the CPU load. Thanks for your tip on where to start, though. If I end up trying to solve it that way, I’ll post again with any news.