Runtime fullscreen toggle

(copied from other thread, [+] camera restoration, [-] resolution change)

 find the latest code in my last post 

Thanks very much for posting, ynjh_jo! I can really use this.

Yes, thanks ynjh_jo! This is very useful. Appreciate it!

I am trying this out under linux. I run into a few issues when I turn fullscreen off: the window size is now “fullscreen size” and there is no window decoration. This can be fixed the same way as the window position is remembered and restored.

Another point is that my audio3dmanager is no longer using the correct camera as listener. Apparently when opening a new window, you also get a new camera, which is why you do the camera restoration? Is the old camera really gone or could we set the old camera object as the “current camera”?

No go for me … any ideas?

Assertion failed: has_origin() at line 84 of c:\temp\mkpr\panda3d-1.3.2\panda\src\display\windowProperties.I
Traceback (most recent call last):
  File "C:\PHL\Dev\CodeBase\Templates\Panda3D\P3D-SimpleNavWorld.py", line 311, in ?
    run()
  File "C:\Panda3D-1.3.2\direct\src\showbase\ShowBase.py", line 2028, in run
    self.taskMgr.run()
  File "C:\Panda3D-1.3.2\direct\src\task\Task.py", line 839, in run
    self.step()
  File "C:\Panda3D-1.3.2\direct\src\task\Task.py", line 787, in step
    self.__stepThroughList(taskPriList)
  File "C:\Panda3D-1.3.2\direct\src\task\Task.py", line 721, in __stepThroughList
    ret = self.__executeTask(task)
  File "C:\Panda3D-1.3.2\direct\src\task\Task.py", line 644, in __executeTask
    ret = task(task)
  File "C:\Panda3D-1.3.2\direct\src\showbase\EventManager.py", line 38, in eventLoopTask
    self.doEvents()
  File "C:\Panda3D-1.3.2\direct\src\showbase\EventManager.py", line 32, in doEvents
    self.processEvent(self.eventQueue.dequeueEvent())
  File "C:\Panda3D-1.3.2\direct\src\showbase\EventManager.py", line 88, in processEvent
    messenger.send(eventName)
  File "C:\Panda3D-1.3.2\direct\src\showbase\Messenger.py", line 223, in send
    apply(method, (extraArgs + sentArgs))
  File "C:\PHL\Dev\CodeBase\Templates\Panda3D\P3D-SimpleNavWorld.py", line 247, in toggleFullscreen
    self.winOrigin=(props.getXOrigin(),props.getYOrigin()) 
AssertionError: has_origin() at line 84 of c:\temp\mkpr\panda3d-1.3.2\panda\src\display\windowProperties.I
Tool returned code: 1

Strange, that would mean your window has no origin :confused: . You can check for this using props.hasOrigin() .

Are you guys using the CVS version ?
perhaps you’re running into this :
discourse.panda3d.org/viewtopic.php?t=2934

and perhaps this helps :

props = WindowProperties( base.win.getProperties() )

props.hasOrigin() returns 0

No I am using stock 1.3.2 and no, that didn’t help

ah, I see. Config.prc by default doesn’t set the origin, right ? Put “win-origin xsize ysize” there.

Please don’t write, or suggest other people write, code like this:

This is just promoting an incorrect usage of WindowProperties, which I attempted to prevent by making base.win.getProperties() return a const structure. But by wrapping its return value in a copy constructor, you are just working around my attempt to enforce correctness. The correct way to request changes to the window properties is:

As to the reason the window has no origin, well, it must be that no one ever requested a particular origin. On Linux, Panda correctly reports that the window has no origin. On Windows, because of a quirk in the way the Windows messaging works, Panda reports in this case that the window has some arbitrary origin instead, which I consider incorrect behavior, but difficult to avoid.

You could argue that it is incorrect of Panda to behave slightly differently in this regard on different operating systems, and I would have to agree with you. Ideally, we would standardize the way Panda’s windows react to changes in window properties in all cases, across all platforms. Until we have achieved that ideal, though, programmers will have to be sensitive to this potential difference.

The correct way to write code that queries the window’s origin (or, for that matter, any element of WindowProperties) is always to query props.hasOrigin() first, and only call props.getXOrigin() or getYOrigin() if hasOrigin has returned true.

David

This fixes it:

loadPrcFileData("", "win-origin 10 10")

I’m working on a workaround that will follow David’s comments/recommendations above … will post it when complete.

Thanks,
Paul

Try this :

# Standard imports

import sys
import os

# Panda imports

from pandac.PandaModules import *

from direct.gui.OnscreenText import OnscreenText 
from direct.showbase import DirectObject
from direct.showbase.DirectObject import DirectObject
import direct.directbase.DirectStart
from direct.task import Task

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Function to put instructions on the screen.

def addInstructions(pos, msg):
	return OnscreenText(
		text=msg,
		style=1,
		fg=(1,1,1,1),
		pos=(-1.3, pos),
		align=TextNode.ALeft,
		scale = .05
	)

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Function to put title on the screen.

def addTitle(text):
	return OnscreenText(
		text=text,
		style=1,
		fg=(1,1,1,1),
		pos=(1.3,-0.95),
		align=TextNode.ARight,
		scale = .07
	)

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 

class World(DirectObject):

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# Constructor
	def __init__(self):

		# *** Setup basics
		title = 'Simple Panda3D Template'
		addTitle(title)
		pos = 0.85
		addInstructions(pos,"ESC: Quit")
		pos = pos - 0.05
		addInstructions(pos,"=: Toggle Fullscreen")

		# *** Setup title
		wp = WindowProperties()
		wp.setTitle(title)
		base.win.requestProperties(wp)
		base.setBackgroundColor(0,0.0,0.3,1)

		# *** Setup window origin memory
		props = base.win.getProperties()
		props = base.win.getProperties()
		if props.hasOrigin():
			self.mOrigin = (props.getXOrigin(),props.getYOrigin())
		else:
			self.mOrigin = (50,50)

		# *** Setup key bindings
		self.accept('=',self.toggleFullscreen)
		self.accept('escape',sys.exit)

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	# Full screen toggle
	def toggleFullscreen(self):

		fullscreen = not base.win.isFullscreen()
		props = base.win.getProperties()
		props = base.win.getProperties()

		cameraParent = camera.getParent()
		cameraMat = Mat4(camera.getMat())
		camParent = base.cam.getParent()
		camMat = Mat4(base.cam.getMat())

		if (not fullscreen):
			props.setFullscreen(False)
			props.setOrigin(*self.mOrigin)
		else:
			if props.hasOrigin():
				self.mOrigin = (props.getXOrigin(),props.getYOrigin())
			else:
				self.mOrigin = (50,50)
				props.setOrigin(self.mOrigin[0],self.mOrigin[1])
			props.setFullscreen(True)

		base.openMainWindow(props=props, gsg=base.win.getGsg()) 
		camera.reparentTo(cameraParent) 
		camera.setMat(cameraMat) 
		base.cam.reparentTo(camParent) 
		base.cam.setMat(camMat)

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 

world = World()

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Run the program

run()

Yes, that’s the exact point of the idea.
We need to close and re-open the window, which definitely need a modified FULL properties BEFORE re-opening it.
I’m sure that’s the only way to alter the properties before re-opening the window, …or… is there any other way without altering ShowBase.openWindow ?

Paul, your frozen props obviously won’t work with the next official release.

oh, 1 simple question :
Is there a way to detect the desktop resolution ?
Setting the fullscreen size to match the desktop gives a lot faster switch.

Ah, you’re absolutely right. My apologies. In this case, then, using the WindowProperties copy constructor is absolutely the right thing to do. Still, I’m a little sensitive about it, since so many people seem to have gotten the idea that this is also the way to make little changes to an existing window without reopening it; so maybe it would be good to precede this with a comment explaining why it is necessary to make a complete copy of the WindowProperties in this instance.

Unfortunately, not universally. On Linux, this works with base.pipe.getDisplayWidth() / base.pipe.getDisplayHeight(). But this hasn’t yet been implemented on Windows.

Yes, it does, and especially if you set the window to “undecorated”, it looks a lot like a fullscreen window. But it’s not the same as fullscreen mode; it’s just a window that covers the desktop–many drivers run faster in true fullscreen mode, even with today’s graphics cards.

Oh, wait–I misunderstood again. You mean to make a true fullscreen window that is the same size as the existing desktop. Yes, you’re right; that is very often faster than switching to a different fullscreen size, especially if the auto-selected refresh rate happens to be the same as the one that was used for the desktop mode. But whether it is faster or not really depends on your monitor.

David

that’s correct, true fullscreen.
So, desktop res query on Windows is not supported yet, no wonder it always throws 0.

trash
scroll down

Back here again.
I just noticed that there is already a way to preserve camera’s scenegraph and transform. I just need to pass keepCamera=1 to openMainWindow() call. So, upon closing the main window, the default camera isn’t removed. This way, any reference to the camera isn’t lost, e.g. 3d sound manager as Laurens wrote.

New issue :
None of main window’s user-made display regions survives during fullscreen switch. So basically, I have to re-create every non-default display regions.

This is how I do it (plus the latest fullscreen toggle) :

from pandac.PandaModules import *
loadPrcFileData('','win-size 640 480')
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
import direct.directbase.DirectStart
import sys


class DisplayRegionsManager:
  def __init__(self):
      self.DRCollection=[]

  def make(self,name,win,cam,
           l=0,r=1,b=0,t=1, sort=0, active=True, stereoChannel=0,
           drawCallback=None, incompleteRender=True, textureReloadPriority=0,
           cubeMapIndex=-1, cullTrav=None):
      """ creates a display region """
      dr=win.makeDisplayRegion(l,r,b,t)
      dr.setCamera(cam)
      cam.setSx(base.getAspectRatio()*(r-l)/(t-b))
      dr.setActive(active)
      dr.setCubeMapIndex(cubeMapIndex)
      dr.setIncompleteRender(incompleteRender)
      dr.setSort(sort)
      dr.setStereoChannel(stereoChannel)
      dr.setTextureReloadPriority(textureReloadPriority)
      if drawCallback is not None:
         dr.setDrawCallback(drawCallback)
      if cullTrav is not None:
         dr.setCullTraverser(cullTrav)
      self.DRCollection.append([name,win,dr])
#      print 'DR:',name,win,cam,l,r,b,t

  def get(self,name):
      """ returns display region with the given name """
      matches=filter(lambda dri: dri[0]==name, self.DRCollection)
      return matches[0][-1] if matches else None

  def remove(self,name):
      """ removes display region with the given name """
      matches=filter(lambda dri: dri[0]==name, self.DRCollection)
      for dri in matches:
          name,win,dr = dri
          if not win.isActive():
             win=base.win
          win.removeDisplayRegion(dr)
          self.DRCollection.remove(dri)

  def save(self):
      """ saves all user-made display regions, be sure to call this before
          calling rebuild()
      """
      self.DRStates=[]
      for name,win,dr in self.DRCollection:
          props=[ dr.getCamera(),
                  dr.getLeft(),dr.getRight(),dr.getBottom(),dr.getTop(),
                  dr.getSort(),
                  dr.isActive(),
                  dr.getStereoChannel(),
                  dr.getDrawCallback(),
                  dr.getIncompleteRender(),
                  dr.getTextureReloadPriority(),
                  dr.getCubeMapIndex(),
                  dr.getCullTraverser()
                ]
          self.DRStates.append([name,win]+props)

  def rebuild(self):
      """ re-creates all user-made display regions, call this
          after closing and re-opening the main window. Be sure to call save()
          before closing the main window.
      """
      self.DRCollection=[]
      for name,win,cam,l,r,b,t, sort, active, stereoChannel,\
            drawCallback, incompleteRender, textureReloadPriority,\
            cubeMapIndex, cullTrav in self.DRStates:
          if not win.isActive():
             win=base.win
          self.make(name,win,cam,l,r,b,t, sort, active, stereoChannel,
            drawCallback, incompleteRender, textureReloadPriority,
            cubeMapIndex, cullTrav)


class World(DirectObject):
  def __init__(self):
      camera.setPos(7.61, -12.01, 3.52)
      camera.setHpr(36.70, -4.25, 2.87)
      mat=Mat4(camera.getMat())
      mat.invertInPlace()
      base.mouseInterfaceNode.setMat(mat)

      self.accept('escape',sys.exit)
      self.accept('f',self.toggleFullscreen)
      self.accept('r',self.toggleFullscreen,[0])
      self.accept('space',self.toggleDR3active)
      self.accept('c',camera.printTransform)
      self.acceptOnce('delete',self.delDR2)

      OnscreenText( """[ F ] : toggle fullscreen (use desktop resolution)
[ R ] : toggle fullscreen (same resolution)
[ SPACE ] : toggle teapot's display region
[ DEL ] : remove right-smiley's display region
[ ESC ] : exit""", parent=base.a2dTopLeft, fg=(1,1,1,1),
        pos=(.02,-.06),scale=.055,align=TextNode.ALeft)

      p4=loader.loadModel('panda-model')
      p4.setScale(.01)
      p4.reparentTo(render)

      scene1=NodePath('SCENE 1')
      panda=loader.loadModel('panda')
      panda.reparentTo(scene1)
      panda.hprInterval(3,Vec3(360,0,0)).loop()
      self.VPcam1 = Camera('vpcam1')
      self.VPcam1.setScene(scene1)
      self.VPcamNP1 = scene1.attachNewNode(self.VPcam1)
      self.VPcamNP1.setPos(panda.getBounds().getCenter())
      self.VPcamNP1.setY(-panda.getBounds().getRadius()*3.5)
      DRMgr.make('dr1',base.win,self.VPcamNP1,.7,1,0,.33,sort=100)

      scene2=NodePath('SCENE 2')
      smiley=loader.loadModel('smiley')
      smiley.reparentTo(scene2)
      smiley.hprInterval(3,Vec3(360,0,0)).loop()
      self.VPcam2 = Camera('vpcam2')
      self.VPcam2.setScene(scene2)
      self.VPcamNP2 = scene2.attachNewNode(self.VPcam2)
      self.VPcamNP2.setPos(smiley.getBounds().getCenter())
      self.VPcamNP2.setY(-smiley.getBounds().getRadius()*3.5)
      DRMgr.make('dr2-right',base.win,self.VPcamNP2,.7,1,.33,.66,sort=101)
      # just a duplicate, same scene rendered in different display region
      DRMgr.make('dr2-left',base.win,self.VPcamNP2,.4,.7,.33,.66,sort=102)

      scene3=NodePath('SCENE 3')
      teapot=loader.loadModel('teapot')
      teapot.reparentTo(scene3)
      teapot.hprInterval(3,Vec3(360,0,0)).loop()
      self.VPcam3 = Camera('vpcam3')
      self.VPcam3.setScene(scene3)
      self.VPcamNP3 = scene3.attachNewNode(self.VPcam3)
      self.VPcamNP3.setPos(teapot.getBounds().getCenter())
      self.VPcamNP3.setY(-teapot.getBounds().getRadius()*3.5)
      DRMgr.make('dr3',base.win,self.VPcamNP3,.7,1,.66,1,sort=103)

  def delDR2(self):
      DRMgr.remove('dr2-right')

  def toggleDR3active(self):
      dr3=DRMgr.get('dr3')
      dr3.setActive(not dr3.isActive())

  def toggleFullscreen(self, matchDesktopSize=1):
      '''
         NOTE :
         Due to closing and re-opening the window, you will get new mouseWatcher,
         mouseWatcherNode and buttonThrower of the new window.
         If you used them directly (not getting it from base.*),
         you must grab the new ones, because the old ones are already dead.
      '''
      fullscreen = not base.win.isFullscreen()
      # To get the real fullscreen, we have to close the window and re-open it.
      # It means we need the modified FULL WindowProperties before re-opening
      # the window. But the new CVS version of "window.getProperties()"
      # returns a constant WindowProperties object, then it would be
      # impossible to change the properties before passing it to openMainWindow().
      #_________________________________________________________________________
      # Q : do we have any way around this ?
      # Yes, the only possible answer is to "thaw" the constant WindowProperties
      # by wrapping it with a new WindowProperties object,
      # so all of the old properties get copied to the new one.
      # By doing so, we have a new alterable WindowProperties. :D
      # WARNING : do NOT attempt to do this if you don't need to close and
      # re-open your window. You might cause a havoc to your window.
      #_________________________________________________________________________
      # NOTE : the properties change which need
      # closing and re-opening the window are :
      # 1. fullscreen
      # 2. framebuffer properties
      # For other properties change, you only need to create a blank WindowProperties.
      props = WindowProperties( base.win.getProperties() )
      if not fullscreen:  # go windowed
         if not hasattr(self,'winUndecorated'):
            self.winUndecorated=False
         if not hasattr(self,'winOrigin'):
            self.winOrigin=(100,10)
         if not hasattr(self,'winSize'):
            self.winSize=(800,600)
         props.setFullscreen(False)
         props.setUndecorated(self.winUndecorated)
         props.setOrigin(*self.winOrigin)
         props.setSize(*self.winSize)
      else:  # go fullscreen
         props.setFullscreen(True)
         # save the undecorated status
         self.winUndecorated=props.getUndecorated()
         # once switched to fullscreen, the origin will be (0,0), so save it
         if props.hasOrigin():
            self.winOrigin=(props.getXOrigin(),props.getYOrigin())
         else:
            self.winOrigin=(100,10)
         # save the size too
         self.winSize=(props.getXSize(),props.getYSize())
         # match to desktop size ?
         if matchDesktopSize:
            w=base.pipe.getDisplayWidth()
            h=base.pipe.getDisplayHeight()
            if w and h:
               props.setSize(w,h)
            else:
               print 'INVALID DESKTOP SIZE'

      mouse2camParent=base.mouse2cam.getParent()
      # STEP [ 1 ] : save all display regions current states
      DRMgr.save()
      # STEP [ 2 ] : close and re-open the main window
      base.openMainWindow(props=props, gsg=base.win.getGsg(), keepCamera=1)
      # STEP [ 3 ] : user-made display regions are gone
      # due to closing the main window, so re-create them again
      DRMgr.rebuild()
      # restore mouse interface
      if mouse2camParent==base.mouseInterface and not base.mouseInterface.isStashed():
         base.changeMouseInterface(base.mouseInterface)
      # who knows the aspect ratio is changed ? let showbase take care of it
      messenger.send('window-event',[base.win])


if __name__=='__main__':
   DRMgr=DisplayRegionsManager()
   World()
   run()

If you’re on Windows and not using the latest winGraphicsPipe.cxx, you wouldn’t be able to get the desktop resolution using panda. Find other way around.

[EDIT] :
it didn’t work correctly on Linux, so I had to use the entirely customized display region creation and removal to maintain cross-platform consistency.

[EDIT] :
WARNING : don’t keep any reference of any display region object, because once the main window is closed, that display region will be gone and the reference you keep will be outdated. Let my DisplayRegionsManager handle them all.

i tried this but stuff just gets missing from the backdrop, that’s located on different bins.

edit —

hmm i actually figured that part out but there seem to bay way more stuff that needs to be fixed with, shaders, post processing and picking - they all go way off.

Post-processing shaders :
all RTT must be rebuilt, since upon closing main window, all of them are cleared. You have to localize its setup, so it can be called again.
If you’re not using filter manager, you have to remove the cards yourself.

Picking :
I tried it with chessboard sample, it works.

Here are my samples mod :
ynjh.panda3dprojects.com/other/f … amples.zip