Bläddra i källkod

more TexMemWatcher features

David Rose 17 år sedan
förälder
incheckning
ac19dd2de0
2 ändrade filer med 337 tillägg och 32 borttagningar
  1. 13 0
      direct/src/showbase/ShowBase.py
  2. 324 32
      direct/src/showutil/TexMemWatcher.py

+ 13 - 0
direct/src/showbase/ShowBase.py

@@ -149,6 +149,7 @@ class ShowBase(DirectObject.DirectObject):
         self.mouseInterface = None
         self.drive = None
         self.trackball = None
+        self.texmem = None
         self.cam = None
         self.cam2d = None
         self.cam2dp = None
@@ -1782,6 +1783,18 @@ class ShowBase(DirectObject.DirectObject):
         if self.trackball:
             self.changeMouseInterface(self.trackball)
 
+    def toggleTexMem(self):
+        """ Toggles a handy texture memory watcher.  See TexMemWatcher
+        for more information. """
+
+        if self.texmem and not self.texmem.cleanedUp:
+            self.texmem.cleanup()
+            self.texmem = None
+            return
+
+        from direct.showutil.TexMemWatcher import TexMemWatcher
+        self.texmem = TexMemWatcher()
+
     def oobe(self):
         """
         Enable a special "out-of-body experience" mouse-interface

+ 324 - 32
direct/src/showutil/TexMemWatcher.py

@@ -20,8 +20,16 @@ class TexMemWatcher(DirectObject):
     memory fragmentation issues.
     """
 
+    NextIndex = 1
+
     def __init__(self, gsg = None, limit = None):
         DirectObject.__init__(self)
+
+        # First, we'll need a name to uniquify the object.
+        self.name = 'tex-mem%s' % (TexMemWatcher.NextIndex)
+        TexMemWatcher.NextIndex += 1
+
+        self.cleanedUp = False
         
         # If no GSG is specified, use the main GSG.
         if gsg is None:
@@ -65,20 +73,15 @@ class TexMemWatcher(DirectObject):
         # We should render at the end of the frame.
         self.win.setSort(10000)
 
-        # Set our GSG to a texture memory limit of 0.  This will force
-        # it to unload any textures as soon as they are offscreen, so
-        # that our GSG's texture memory utilization will match that of
-        # the real GSG.
-        self.win.getGsg().getPreparedObjects().setGraphicsMemoryLimit(0)
-
         # We don't need to clear the color buffer, since we'll be
         # filling it with a texture.  We also don't need to clear the
         # depth buffer, since we won't be using it.
         self.win.setClearColor(False)
         self.win.setClearDepth(False)
 
-        self.win.setWindowEvent('tex-mem-window')
-        self.accept('tex-mem-window', self.windowEvent)
+        eventName = '%s-window' % (self.name)
+        self.win.setWindowEvent(eventName)
+        self.accept(eventName, self.windowEvent)
 
         # Make a render2d in this new window.
         self.render2d = NodePath('render2d')
@@ -96,9 +99,32 @@ class TexMemWatcher(DirectObject):
         self.cam = self.render2d.attachNewNode(cam)
         self.dr.setCamera(self.cam)
 
+        # We'll need a Mouse and a MouseWatcher in the data graph, so
+        # we can interact with the various textures.
+        self.mouse = base.dataRoot.attachNewNode(MouseAndKeyboard(self.win, 0, '%s-mouse' % (self.name)))
+        self.mw = MouseWatcher('%s-watcher' % (self.name))
+        mwnp = self.mouse.attachNewNode(self.mw)
+        bt = ButtonThrower('%s-thrower' % (self.name))
+        mwnp.attachNewNode(bt)
+        bt.setPrefix('button-%s-' % (self.name))
+        self.accept('button-%s-mouse1' % (self.name), self.mouseClick)
+
+        eventName = '%s-enter' % (self.name)
+        self.mw.setEnterPattern(eventName)
+        self.accept(eventName, self.enterRegion)
+
+        eventName = '%s-leave' % (self.name)
+        self.mw.setLeavePattern(eventName)
+        self.accept(eventName, self.leaveRegion)
+
+        # Now start handling up the actual stuff in the scene.
+
         self.canvas = self.render2d.attachNewNode('canvas')
         self.background = None
         self.overflowing = False
+        self.nextTexRecordKey = 0
+        self.rollover = None
+        self.isolate = None
         
         self.task = taskMgr.doMethodLater(0.5, self.updateTextures, 'TexMemWatcher')
 
@@ -123,6 +149,12 @@ class TexMemWatcher(DirectObject):
                 
                 self.dynamicLimit = True
 
+        if not self.dynamicLimit:
+            # Set our GSG to limit itself to no more textures than we
+            # expect to display onscreen, so we don't go crazy with
+            # texture memory.
+            self.win.getGsg().getPreparedObjects().setGraphicsMemoryLimit(self.limit)
+
         # The actual height of the canvas, including the overflow
         # area.  The texture memory itself is restricted to (0..1)
         # vertically; anything higher than 1 is overflow.
@@ -138,20 +170,25 @@ class TexMemWatcher(DirectObject):
         self.reconfigureWindow()
 
     def cleanup(self):
-        # Remove the window.
-        if self.win:
+        if not self.cleanedUp:
+            self.cleanedUp = True
+
+            # Remove the window.
             base.graphicsEngine.removeWindow(self.win)
             self.win = None
+            self.gsg = None
+            self.pipe = None
+
+            # Remove the mouse.
+            self.mouse.detachNode()
 
-        if self.task:
             taskMgr.remove(self.task)
-            self.task = None
-            
-        self.ignoreAll()
+            self.ignoreAll()
 
-        self.canvas.getChildren().detach()
-        self.texRecords = {}
-        self.texPlacements = {}
+            self.canvas.getChildren().detach()
+            self.texRecordsByTex = {}
+            self.texRecordsByKey = {}
+            self.texPlacements = {}
 
 
     def windowEvent(self, win):
@@ -167,6 +204,131 @@ class TexMemWatcher(DirectObject):
                 self.winSize = size
                 self.reconfigureWindow()
 
+    def enterRegion(self, region, buttonName):
+        """ the mouse has rolled over a texture. """
+        key, pi = map(int, region.getName().split(':'))
+        tr = self.texRecordsByKey.get(key)
+        if not tr:
+            return
+
+        self.setRollover(tr, pi)
+
+    def leaveRegion(self, region, buttonName):
+        """ the mouse is no longer over a texture. """
+        key, pi = map(int, region.getName().split(':'))
+        tr = self.texRecordsByKey.get(key)
+        if tr != self.rollover:
+            return
+
+        self.setRollover(None, None)
+
+    def mouseClick(self):
+        """ Received a mouse-click within the window.  This isolates
+        the currently-highlighted texture into a full-window
+        presentation. """
+
+        if self.isolate:
+            # We're already isolating a texture; the click undoes this.
+            self.isolateTexture(None)
+            return
+
+        if self.rollover:
+            self.isolateTexture(self.rollover)
+
+    def setRollover(self, tr, pi):
+        """ Sets the highlighted texture (due to mouse rollover) to
+        the indicated texture, or None to clear it. """
+
+        if self.rollover:
+            self.rollover.clearRollover()
+        
+        self.rollover = tr
+
+        if self.rollover:
+            self.rollover.showRollover(pi, self)
+
+    def isolateTexture(self, tr):
+        """ Isolates the indicated texture onscreen, or None to
+        restore normal mode. """
+
+        if self.isolate:
+            self.isolate.removeNode()
+            self.isolate = None
+
+        self.canvas.show()
+        self.background.clearColor()
+        self.win.getGsg().setTextureQualityOverride(Texture.QLDefault)
+
+        if not tr:
+            return
+
+        self.canvas.hide()
+        # Disable the red bar at the top.
+        self.background.setColor(1, 1, 1, 1, 1)
+
+        # Show the texture in all its filtered glory.
+        self.win.getGsg().setTextureQualityOverride(Texture.QLBest)
+
+        self.isolate = self.render2d.attachNewNode('isolate')
+        self.isolate.setBin('fixed', 0)
+
+        # Put a label on the bottom of the screen.
+        tn = TextNode('tn')
+        tn.setText('%s\n%s x %s\n%s bytes' % (
+            tr.tex.getName(), tr.tex.getXSize(), tr.tex.getYSize(),
+            tr.size))
+        tn.setAlign(tn.ACenter)
+        tn.setCardAsMargin(100.0, 100.0, 0.1, 0.1)
+        tn.setCardColor(0.1, 0.2, 0.4, 1)
+        tnp = self.isolate.attachNewNode(tn)
+        scale = 15.0 / self.winSize[1]
+        tnp.setScale(scale * self.winSize[1] / self.winSize[0], scale, scale)
+        tnp.setPos(0.5, 0, -tn.getBottom() * scale)
+
+        labelTop = tn.getHeight() * scale
+
+        # Make a card that shows the texture in actual pixel size, but
+        # don't let it exceed the screen size.
+        tw = tr.tex.getXSize()
+        th = tr.tex.getYSize()
+
+        wx = self.winSize[0] * 0.9
+        wy = self.winSize[1] * (1.0 - labelTop) * 0.9
+
+        w = min(tw, wx)
+        h = min(th, wy)
+
+        sx = w / tw
+        sy = h / th
+        s = min(sx, sy)
+
+        w = tw * s / float(self.winSize[0])
+        h = th * s / float(self.winSize[1])
+
+        cx = 0.5
+        cy = 1.0 - (1.0 - labelTop) * 0.5
+
+        l = cx - w * 0.5
+        r = cx + w * 0.5
+        b = cy - h * 0.5
+        t = cy + h * 0.5
+
+        cm = CardMaker('card')
+        cm.setFrame(l, r, b, t)
+        c = self.isolate.attachNewNode(cm.generate())
+        c.setTexture(tr.tex)
+        c.setTransparency(TransparencyAttrib.MAlpha)
+
+        ls = LineSegs('frame')
+        ls.setColor(0, 0, 0, 1)
+        ls.moveTo(l, 0, b)
+        ls.drawTo(r, 0, b)
+        ls.drawTo(r, 0, t)
+        ls.drawTo(l, 0, t)
+        ls.drawTo(l, 0, b)
+        self.isolate.attachNewNode(ls.create())
+
+
     def reconfigureWindow(self):
         """ Resets everything for a new window size. """
 
@@ -218,7 +380,7 @@ class TexMemWatcher(DirectObject):
         totalSize = 0
 
         texRecords = []
-        neverVisited = copy.copy(self.texRecords)
+        neverVisited = copy.copy(self.texRecordsByTex)
         for tex in self.gsg.getPreparedTextures():
             # We have visited this texture; remove it from the
             # neverVisited list.
@@ -229,14 +391,16 @@ class TexMemWatcher(DirectObject):
             if tex.getResident(pgo):
                 size = tex.getDataSizeBytes(pgo)
 
-            tr = self.texRecords.get(tex, None)
+            tr = self.texRecordsByTex.get(tex, None)
 
             if size:
                 totalSize += size
                 active = tex.getActive(pgo)
                 if not tr:
                     # This is a new texture; need to record it.
-                    tr = TexRecord(tex, size, active)
+                    key = self.nextTexRecordKey
+                    self.nextTexRecordKey += 1
+                    tr = TexRecord(key, tex, size, active)
                     texRecords.append(tr)
                 else:
                     tr.setActive(active)
@@ -254,7 +418,8 @@ class TexMemWatcher(DirectObject):
         # textures that we didn't visit at all this pass.
         for tex, tr in neverVisited.items():
             self.unplaceTexture(tr)
-            del self.texRecords[tex]
+            del self.texRecordsByTex[tex]
+            del self.texRecordsByKey[tr.key]
 
         self.totalSize = totalSize
         if totalSize > self.limit and self.dynamicLimit:
@@ -279,7 +444,8 @@ class TexMemWatcher(DirectObject):
                 self.overflowing = False
                 for tr in texRecords:
                     self.placeTexture(tr)
-                    self.texRecords[tr.tex] = tr
+                    self.texRecordsByTex[tr.tex] = tr
+                    self.texRecordsByKey[tr.key] = tr
 
         return task.again
                 
@@ -288,8 +454,11 @@ class TexMemWatcher(DirectObject):
         """ Repacks all of the current textures. """
 
         self.canvas.getChildren().detach()
-        self.texRecords = {}
+        self.texRecordsByTex = {}
+        self.texRecordsByKey = {}
         self.texPlacements = {}
+        self.mw.clearRegions()
+        self.setRollover(None, None)
         self.w = 1
         self.h = 1
 
@@ -301,8 +470,11 @@ class TexMemWatcher(DirectObject):
                 size = tex.getDataSizeBytes(pgo)
                 if size:
                     active = tex.getActive(pgo)
-                    tr = TexRecord(tex, size, active)
-                    self.texRecords[tex] = tr
+                    key = self.nextTexRecordKey
+                    self.nextTexRecordKey += 1
+                    tr = TexRecord(key, tex, size, active)
+                    self.texRecordsByTex[tr.tex] = tr
+                    self.texRecordsByKey[tr.key] = tr
                     totalSize += size
 
         self.totalSize = totalSize
@@ -313,6 +485,11 @@ class TexMemWatcher(DirectObject):
             # Choose a suitable limit by rounding to the next power of two.
             self.limit = Texture.upToPower2(self.totalSize)
 
+            # Set our GSG to limit itself to no more textures than we
+            # expect to display onscreen, so we don't go crazy with
+            # texture memory.
+            self.win.getGsg().getPreparedObjects().setGraphicsMemoryLimit(self.limit)
+
         # Now make that into a 2-D rectangle of the appropriate shape,
         # such that w * h == limit.
 
@@ -332,10 +509,11 @@ class TexMemWatcher(DirectObject):
         self.h = h
 
         self.canvas.setScale(1.0 / w, 1.0, 1.0 / h)
+        self.mw.setFrame(0, w, 0, h)
 
         # Sort the regions from largest to smallest to maximize
         # packing effectiveness.
-        texRecords = self.texRecords.values()
+        texRecords = self.texRecordsByTex.values()
         texRecords.sort(key = lambda tr: (-tr.w, -tr.h))
         
         self.overflowing = False
@@ -347,10 +525,7 @@ class TexMemWatcher(DirectObject):
         for tp in tr.placements:
             del self.texPlacements[tp]
         tr.placements = []
-
-        if tr.root:
-            tr.root.detachNode()
-            tr.root = None
+        tr.clearCard(self)
 
     def placeTexture(self, tr):
         """ Places the texture somewhere on the canvas where it will
@@ -625,11 +800,14 @@ class TexMemWatcher(DirectObject):
 
 
 class TexRecord:
-    def __init__(self, tex, size, active):
+    def __init__(self, key, tex, size, active):
+        self.key = key
         self.tex = tex
         self.active = active
         self.root = None
+        self.regions = []
         self.placements = []
+        self.rollover = None
 
         self.setSize(size)
 
@@ -658,10 +836,17 @@ class TexRecord:
             self.matte.setColor((0.2, 0.2, 0.2, 1), 2)
             self.card.setColor((0.4, 0.4, 0.4, 1), 2)
 
-    def makeCard(self, tmw):
+    def clearCard(self, tmw):
         if self.root:
             self.root.detachNode()
+            self.root = None
+
+        for r in self.regions:
+            tmw.mw.removeRegion(r)
+        self.regions = []
 
+    def makeCard(self, tmw):
+        self.clearCard(tmw)
         root = NodePath('root')
 
         # A backing to put behind the card.
@@ -734,6 +919,113 @@ class TexRecord:
 
         self.root = root
 
+        # Also, make one or more clickable MouseWatcherRegions.
+        assert self.regions == []
+        for pi in range(len(self.placements)):
+            p = self.placements[pi]
+            r = MouseWatcherRegion('%s:%s' % (self.key, pi), *p.p)
+            tmw.mw.addRegion(r)
+            self.regions.append(p)
+
+    def showRollover(self, pi, tmw):
+        self.clearRollover()
+        try:
+            p = self.placements[pi]
+        except IndexError:
+            return
+
+        # Center the rollover rectangle over the placement
+        l, r, b, t = p.p
+        cx0 = (l + r) * 0.5
+        cy0 = (b + t) * 0.5
+
+        # Exaggerate its size a bit
+        w = self.w
+        h = self.h
+
+        cx = cx0
+        if cx + w > tmw.w:
+            cx = tmw.w - w
+        if cx - w < 0:
+            cx = w
+
+        cy = cy0
+        if cy + h > tmw.h:
+            cy = tmw.h - h
+        if cy - h < 0:
+            cy = h
+
+        # But keep it within the window
+        l = max(cx - w, 0)
+        r = min(cx + w, tmw.w)
+        b = max(cy - h, 0)
+        t = min(cy + h, tmw.h)
+
+        # If it needs to be shrunk to fit within the window, keep it
+        # the same aspect ratio.
+        sx = float(r - l) / float(w)
+        sy = float(t - b) / float(h)
+        if sx != sy:
+            s = min(sx, sy)
+            w *= s / sx
+            h *= s / sy
+
+            cx = cx0
+            if cx + w > tmw.w:
+                cx = tmw.w - w
+            if cx - w < 0:
+                cx = w
+
+            cy = cy0
+            if cy + h > tmw.h:
+                cy = tmw.h - h
+            if cy - h < 0:
+                cy = h
+
+            l = max(cx - w, 0)
+            r = min(cx + w, tmw.w)
+            b = max(cy - h, 0)
+            t = min(cy + h, tmw.h)
+
+        self.rollover = tmw.canvas.attachNewNode('rollover')
+
+        cm = CardMaker('backing')
+        cm.setFrame(l, r, b, t)
+        cm.setColor(0.1, 0.3, 0.5, 1)
+        c = self.rollover.attachNewNode(cm.generate())
+        c.setBin('fixed', 100)
+            
+        cm = CardMaker('card')
+        cm.setFrame(l, r, b, t)
+        c = self.rollover.attachNewNode(cm.generate())
+        c.setTexture(self.tex)
+        c.setBin('fixed', 110)
+        c.setTransparency(TransparencyAttrib.MAlpha)
+
+        # Label the font too.
+        tn = TextNode('tn')
+        tn.setText('%s\n%s x %s' % (self.tex.getName(), self.tex.getXSize(), self.tex.getYSize()))
+        tn.setAlign(tn.ACenter)
+        tn.setShadow(0.05, 0.05)
+        tnp = self.rollover.attachNewNode(tn)
+        scale = 20.0 / tmw.winSize[1] * tmw.h
+        tnp.setScale(scale)
+        tx = (l + r) * 0.5
+        ty = b + scale * 2
+        tnp.setPos(tx, 0, ty)
+        if tx + tn.getWidth() * scale * 0.5 > tmw.w:
+            tn.setAlign(tn.ARight)
+            tnp.setX(r)
+        elif tx - tn.getWidth() * scale * 0.5 < 0:
+            tn.setAlign(tn.ALeft)
+            tnp.setX(l)
+        tnp.setBin('fixed', 120)
+
+    def clearRollover(self):
+        if self.rollover:
+            self.rollover.removeNode()
+            self.rollover = None
+
 class TexPlacement:
     def __init__(self, l, r, b, t):
         self.p = (l, r, b, t)