David Rose 17 år sedan
förälder
incheckning
8e6eb98afe
1 ändrade filer med 704 tillägg och 0 borttagningar
  1. 704 0
      direct/src/showutil/TexMemWatcher.py

+ 704 - 0
direct/src/showutil/TexMemWatcher.py

@@ -0,0 +1,704 @@
+from pandac.PandaModules import *
+from direct.showbase.DirectObject import DirectObject
+import math
+import copy
+
+class TexMemWatcher(DirectObject):
+    """
+    This class creates a separate graphics window that displays an
+    approximation of the current texture memory, showing the textures
+    that are resident and/or active, and an approximation of the
+    amount of texture memory consumed by each one.  It's intended as a
+    useful tool to help determine where texture memory is being spent.
+
+    Although it represents the textures visually in a 2-d space, it
+    doesn't actually have any idea how textures are physically laid
+    out in memory--but it has to lay them out somehow, so it makes
+    something up.  It occasionally rearranges the texture display when
+    it feels it needs to, without regard to what the graphics card is
+    actually doing.  This tool can't be used to research texture
+    memory fragmentation issues.
+    """
+
+    def __init__(self, gsg = None, limit = None):
+        DirectObject.__init__(self)
+        
+        # If no GSG is specified, use the main GSG.
+        if gsg is None:
+            gsg = base.win.getGsg()
+        elif isinstance(gsg, GraphicsOutput):
+            # If we were passed a window, use that window's GSG.
+            gsg = gsg.getGsg()
+
+        self.gsg = gsg
+
+        # Now open a new window just to render the output.
+        self.winSize = (300, 300)
+        name = 'Texture Memory'
+        props = WindowProperties()
+        props.setSize(*self.winSize)
+        props.setTitle(name)
+        props.setFullscreen(False)
+
+        fbprops = FrameBufferProperties.getDefault()
+        flags = GraphicsPipe.BFFbPropsOptional | GraphicsPipe.BFRequireWindow
+
+        self.win = base.graphicsEngine.makeOutput(base.pipe, name, 0, fbprops,
+                                                  props, flags)
+        assert self.win
+
+        # We don't need to clear the color buffer, since we'll be
+        # filling it with a texture.  But we can clear the depth
+        # buffer; we use the depth buffer to cut a hole in the matte.
+        self.win.setClearColor(False)
+        self.win.setClearDepth(True)
+
+        self.win.setWindowEvent('tex-mem-window')
+        self.accept('tex-mem-window', self.windowEvent)
+
+        # Make a render2d in this new window.
+        self.render2d = NodePath('render2d')
+        self.render2d.setDepthTest(False)
+        self.render2d.setDepthWrite(False)
+        self.render2d.setTwoSided(True)
+        
+        # And a camera to view it.
+        self.dr = self.win.makeDisplayRegion()
+        cam = Camera('cam2d')
+        self.lens = OrthographicLens()
+        self.lens.setNearFar(-1000, 1000)
+        cam.setLens(self.lens)
+
+        self.cam = self.render2d.attachNewNode(cam)
+        self.dr.setCamera(self.cam)
+
+        self.canvas = self.render2d.attachNewNode('canvas')
+        self.background = None
+        self.overflowing = False
+        
+        self.task = taskMgr.doMethodLater(0.5, self.updateTextures, 'TexMemWatcher')
+
+        self.setLimit(limit)
+
+    def setLimit(self, limit):
+        self.limit = limit
+        self.dynamicLimit = False
+        
+        if limit is None:
+            # If no limit was specified, use the specified graphics
+            # memory limit, if any.
+            lruLimit = self.gsg.getPreparedObjects().getGraphicsMemoryLimit()
+            if lruLimit < 2**32 - 1:
+                # Got a real lruLimit.  Use it.
+                self.limit = lruLimit
+
+            else:
+                # No LRU limit either, so there won't be a practical
+                # limit to the TexMemWatcher.  We'll determine our
+                # limit on-the-fly instead.
+                
+                self.dynamicLimit = True
+
+        # 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.
+        self.top = 1.25
+        if self.dynamicLimit:
+            # Actually, we'll never exceed texture memory, so never mind.
+            self.top = 1
+
+        self.lens.setFilmSize(1, self.top)
+        self.lens.setFilmOffset(0.5, self.top / 2.0)  # lens covers 0..1 in x and y
+
+        self.makeWindowBackground()
+        self.reconfigureWindow()
+
+    def cleanup(self):
+        # Remove the window.
+        if self.win:
+            base.graphicsEngine.removeWindow(self.win)
+            self.win = None
+
+        if self.task:
+            taskMgr.remove(self.task)
+            self.task = None
+            
+        self.ignoreAll()
+
+        self.canvas.getChildren().detach()
+        self.texRecords = {}
+        self.texPlacements = {}
+
+
+    def windowEvent(self, win):
+        if win == self.win:
+            props = win.getProperties()
+            if not props.getOpen():
+                # User closed window.
+                self.cleanup()
+                return
+            
+            size = (props.getXSize(), props.getYSize())
+            if size != self.winSize:
+                self.winSize = size
+                self.reconfigureWindow()
+
+    def reconfigureWindow(self):
+        """ Resets everything for a new window size. """
+
+        self.background.setTexScale(TextureStage.getDefault(),
+                                    self.winSize[0] / 20.0, self.winSize[1] / (20.0 * self.top))
+        self.repack()
+
+    def makeWindowBackground(self):
+        """ Creates a tile to use for coloring the background of the
+        window, so we can tell what empty space looks like. """
+
+        if self.background:
+            self.background.detachNode()
+            self.background = None
+
+        # We start with a simple checkerboard texture image.
+        p = PNMImage(2, 2, 1)
+        p.setGray(0, 0, 0.40)
+        p.setGray(1, 1, 0.40)
+        p.setGray(0, 1, 0.80)
+        p.setGray(1, 0, 0.80)
+
+        tex = Texture('check')
+        tex.load(p)
+        tex.setMagfilter(tex.FTNearest)
+
+        self.background = self.render2d.attachNewNode('background')
+
+        cm = CardMaker('background')
+        cm.setFrame(0, 1, 0, 1)
+        cm.setUvRange((0, 0), (1, 1))
+        self.background.attachNewNode(cm.generate())
+
+        cm.setFrame(0, 1, 1, self.top)
+        cm.setUvRange((0, 1), (1, self.top))
+        bad = self.background.attachNewNode(cm.generate())
+        bad.setColor((0.8, 0.2, 0.2, 1))
+
+        self.background.setBin('fixed', -100)
+        self.background.setTexture(tex)
+
+
+    def updateTextures(self, task):
+        """ Gets the current list of resident textures and adds new
+        textures or removes old ones from the onscreen display, as
+        necessary. """
+
+        pgo = self.gsg.getPreparedObjects()
+        totalSize = 0
+
+        texRecords = []
+        neverVisited = copy.copy(self.texRecords)
+        for tex in self.gsg.getPreparedTextures():
+            # We have visited this texture; remove it from the
+            # neverVisited list.
+            if tex in neverVisited:
+                del neverVisited[tex]
+                
+            size = 0
+            if tex.getResident(pgo):
+                size = tex.getDataSizeBytes(pgo)
+
+            tr = self.texRecords.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)
+                    texRecords.append(tr)
+                else:
+                    tr.setActive(active)
+                    if tr.size != size:
+                        # The size has changed; reapply it.
+                        tr.setSize(size)
+                        self.unplaceTexture(tr)
+                        texRecords.append(tr)
+            else:
+                if tr:
+                    # This texture is no longer resident; need to remove it.
+                    self.unplaceTexture(tr)
+
+        # Now go through and make sure we unplace any textures that we
+        # didn't visit at all this pass.
+        for tr in neverVisited.values():
+            self.unplaceTexture(tr)
+
+        self.totalSize = totalSize
+        if totalSize > self.limit and self.dynamicLimit:
+            # Actually, never mind on the update: we have exceeded the
+            # dynamic limit computed before, and therefore we need to
+            # repack.
+            self.repack()
+
+        else:
+            # Pack in just the newly-loaded textures.
+
+            # Sort the regions from largest to smallest to maximize
+            # packing effectiveness.
+            texRecords.sort(key = lambda tr: (-tr.w, -tr.h))
+
+            self.overflowing = False
+            for tr in texRecords:
+                self.placeTexture(tr)
+                self.texRecords[tr.tex] = tr
+
+        return task.again
+                
+
+    def repack(self):
+        """ Repacks all of the current textures. """
+
+        self.canvas.getChildren().detach()
+        self.texRecords = {}
+        self.texPlacements = {}
+        self.w = 1
+        self.h = 1
+
+        pgo = self.gsg.getPreparedObjects()
+        totalSize = 0
+        
+        for tex in self.gsg.getPreparedTextures():
+            if tex.getResident(pgo):
+                size = tex.getDataSizeBytes(pgo)
+                if size:
+                    active = tex.getActive(pgo)
+                    tr = TexRecord(tex, size, active)
+                    self.texRecords[tex] = tr
+                    totalSize += size
+
+        self.totalSize = totalSize
+        if not self.totalSize:
+            return
+
+        if self.dynamicLimit:
+            # Choose a suitable limit by rounding to the next power of two.
+            self.limit = Texture.upToPower2(self.totalSize)
+
+        # Now make that into a 2-D rectangle of the appropriate shape,
+        # such that w * h == limit.
+
+        # Window size
+        x, y = self.winSize
+
+        # There should be a little buffer on the top so we can see if
+        # we overflow.
+        y /= self.top
+        
+        r = float(y) / float(x)
+
+        # Region size
+        w = math.sqrt(self.limit) / math.sqrt(r)
+        h = w * r
+        self.w = w
+        self.h = h
+
+        self.canvas.setScale(1.0 / w, 1.0, 1.0 / h)
+
+        # Sort the regions from largest to smallest to maximize
+        # packing effectiveness.
+        texRecords = self.texRecords.values()
+        texRecords.sort(key = lambda tr: (-tr.w, -tr.h))
+        
+        self.overflowing = False
+        for tr in texRecords:
+            self.placeTexture(tr)
+
+    def unplaceTexture(self, tr):
+        """ Removes the texture from its place on the canvas. """
+        for tp in tr.placements:
+            del self.texPlacements[tp]
+        tr.placements = []
+
+        if tr.root:
+            tr.root.detachNode()
+            tr.root = None
+
+    def placeTexture(self, tr):
+        """ Places the texture somewhere on the canvas where it will
+        fit. """
+
+        if not self.overflowing:
+            tp = self.findHole(tr.w, tr.h)
+            if tp:
+                tr.placements = [tp]
+                tr.makeCard(self)
+                self.texPlacements[tp] = tr
+                return
+
+            # Couldn't find a hole; can we fit it if we rotate?
+            tp = self.findHole(tr.h, tr.w)
+            if tp:
+                tp.rotated = True
+                tr.placements = [tp]
+                tr.makeCard(self)
+                self.texPlacements[tp] = tr
+                return
+
+            # Couldn't find a hole of the right shape; can we find a
+            # single rectangular hole of the right area, but of any shape?
+            tp = self.findArea(tr.h * tr.w)
+            if tp:
+                texCmp = cmp(tr.w, tr.h)
+                holeCmp = cmp(tp.p[1] - tp.p[0], tp.p[3] - tp.p[2])
+                if texCmp != 0 and holeCmp != 0 and texCmp != holeCmp:
+                    tp.rotated = True
+                tr.placements = [tp]
+                tr.makeCard(self)
+                self.texPlacements[tp] = tr
+                return
+
+            # Couldn't find a single rectangular hole.  We'll have to
+            # divide the texture up into several smaller pieces to cram it
+            # in.
+            tpList = self.findHolePieces(tr.h * tr.w)
+            if tpList:
+                tr.placements = tpList
+                tr.makeCard(self)
+                for tp in tpList:
+                    self.texPlacements[tp] = tr
+                return
+
+        # Just let it overflow.
+        self.overflowing = True
+        tp = self.findHole(tr.w, tr.h, allowOverflow = True)
+        if tp:
+            tr.placements = [tp]
+            tr.makeCard(self)
+            self.texPlacements[tp] = tr
+            return
+
+        # Something went wrong.
+        assert False
+
+    def findHole(self, w, h, allowOverflow = False):
+        """ Searches for a hole large enough for (w, h).  If one is
+        found, returns an appropriate TexPlacement; otherwise, returns
+        None. """
+
+        if w > self.w:
+            # It won't fit within the row at all.
+            if not allowOverflow:
+                return None
+            # Just stack it on the top.
+            y = 0
+            if self.texPlacements:
+                y = max(map(lambda tp: tp.p[3], self.texPlacements.keys()))
+            tp = TexPlacement(0, w, y, y + h)
+            return tp
+            
+        y = 0
+        while y + h <= self.h or allowOverflow:
+            nextY = None
+
+            # Scan along the row at 'y'.
+            x = 0
+            while x + w <= self.w:
+                # Consider the spot at x, y.
+                tp = TexPlacement(x, x + w, y, y + h)
+                overlap = self.findOverlap(tp)
+                if not overlap:
+                    # Hooray!
+                    return tp
+
+                nextX = overlap.p[1]
+                if nextY is None:
+                    nextY = overlap.p[3]
+                else:
+                    nextY = min(nextY, overlap.p[3])
+
+                assert nextX > x
+                x = nextX
+
+            assert nextY > y
+            y = nextY
+
+        # Nope, wouldn't fit anywhere.
+        return None
+        
+
+    def findArea(self, area):
+        """ Searches for a rectangular hole that is at least area
+        square units big, regardless of its shape.  If one is found,
+        returns an appropriate TexPlacement; otherwise, returns
+        None. """
+            
+        y = 0
+        while y < self.h:
+            nextY = self.h
+
+            # Scan along the row at 'y'.
+            x = 0
+            while x < self.w:
+                nextX = self.w
+
+                # Consider the spot at x, y.
+
+                # How wide can we go?  Start by trying to go all the
+                # way to the edge of the region.
+                tpw = self.w - x
+
+                # Now, given this particular width, how tall do we
+                # need to go?
+                tph = area / tpw
+
+                while y + tph < self.h:
+                    tp = TexPlacement(x, x + tpw, y, y + tph)
+                    overlap = self.findOverlap(tp)
+                    if not overlap:
+                        # Hooray!
+                        return tp
+
+                    nextX = min(nextX, overlap.p[1])
+                    nextY = min(nextY, overlap.p[3])
+
+                    # Shorten the available region.
+                    tpw = overlap.p[0] - x
+                    if tpw <= 0.0:
+                        break
+                    tph = area / tpw
+                
+                assert nextX > x
+                x = nextX
+
+            assert nextY > y
+            y = nextY
+
+        # Nope, wouldn't fit anywhere.
+        return None
+
+    def findHolePieces(self, area):
+        """ Returns a list of holes whose net area sums to the given
+        area, or None if there are not enough holes. """
+        
+        # First, save the original value of self.texPlacements, since
+        # we will be modifying that during this search.
+        savedTexPlacements = copy.copy(self.texPlacements)
+
+        result = []
+
+        while area > 0:
+            tp = self.findLargestHole()
+            if not tp:
+                break
+            
+            l, r, b, t = tp.p
+            tpArea = (r - l) * (t - b)
+            if tpArea >= area:
+                # we're done.
+                shorten = (tpArea - area) / (r - l)
+                tp.p = (l, r, b, t - shorten)
+                result.append(tp)
+                self.texPlacements = savedTexPlacements
+                return result
+
+            # Keep going.
+            area -= tpArea
+            result.append(tp)
+            self.texPlacements[tp] = None
+
+        # Huh, not enough room, or no more holes.
+        self.texPlacements = savedTexPlacements
+        return None
+
+    def findLargestHole(self):
+        """ Searches for the largest available hole. """
+
+        holes = []
+            
+        y = 0
+        while y < self.h:
+            nextY = self.h
+
+            # Scan along the row at 'y'.
+            x = 0
+            while x < self.w:
+                nextX = self.w
+
+                # Consider the spot at x, y.
+
+                # How wide can we go?  Start by trying to go all the
+                # way to the edge of the region.
+                tpw = self.w - x
+
+                # And how tall can we go?  Start by trying to go to
+                # the top of the region.
+                tph = self.h - y
+
+                while tpw > 0.0 and tph > 0.0:
+                    tp = TexPlacement(x, x + tpw, y, y + tph)
+                    overlap = self.findOverlap(tp)
+                    if not overlap:
+                        # Here's a hole.
+                        holes.append((tpw * tph, tp))
+                        break
+
+                    nextX = min(nextX, overlap.p[1])
+                    nextY = min(nextY, overlap.p[3])
+
+                    # We've been intersected either on the top or the
+                    # right.  We need to shorten either width or
+                    # height.  Which way results in the largest
+                    # remaining area?
+
+                    tpw0 = overlap.p[0] - x
+                    tph0 = overlap.p[2] - y
+
+                    if tpw0 * tph > tpw * tph0:
+                        # Shortening width results in larger.
+                        tpw = tpw0
+                    else:
+                        # Shortening height results in larger.
+                        tph = tph0
+                
+                assert nextX > x
+                x = nextX
+
+            assert nextY > y
+            y = nextY
+
+        if not holes:
+            # No holes to be found.
+            return None
+
+        # Return the biggest hole
+        return max(holes)[1]
+
+    def findOverlap(self, tp):
+        """ If there is another placement that overlaps the indicated
+        TexPlacement, returns it.  Otherwise, returns None. """
+
+        for other in self.texPlacements.keys():
+            if other.intersects(tp):
+                return other
+
+        return None
+            
+
+
+class TexRecord:
+    def __init__(self, tex, size, active):
+        self.tex = tex
+        self.active = active
+        self.root = None
+        self.placements = []
+
+        self.setSize(size)
+
+    def setSize(self, size):
+        self.size = size
+        x = self.tex.getXSize()
+        y = self.tex.getYSize()
+        r = float(y) / float(x)
+
+        # Card size
+        w = math.sqrt(self.size) / math.sqrt(r)
+        h = w * r
+
+        self.w = w
+        self.h = h
+
+
+    def setActive(self, flag):
+        self.active = flag
+        if self.active:
+            self.matte.clearColor()
+        else:
+            self.matte.setColor((0.4, 0.4, 0.4, 1))
+
+    def makeCard(self, tmw):
+        if self.root:
+            self.root.detachNode()
+
+        root = NodePath('root')
+
+        # A card to display the texture.
+        card = root.attachNewNode('card')
+
+        # A matte to frame the texture and indicate its status.
+        matte = root.attachNewNode('matte')
+
+        # A wire frame to ring the matte and separate the card from
+        # its neighbors.
+        frame = root.attachNewNode('frame')
+
+
+        for p in self.placements:
+            l, r, b, t = p.p
+            cx = (l + r) * 0.5
+            cy = (b + t) * 0.5
+            shrinkMat = Mat4.translateMat(-cx, 0, -cy) * Mat4.scaleMat(0.9) * Mat4.translateMat(cx, 0, cy)
+            
+            cm = CardMaker('card')
+            cm.setFrame(l, r, b, t)
+            if p.rotated:
+                cm.setUvRange((0, 1), (0, 0), (1, 0), (1, 1))
+            c = card.attachNewNode(cm.generate())
+            c.setMat(shrinkMat)
+
+            cm = CardMaker('matte')
+            cm.setFrame(l, r, b, t)
+            matte.attachNewNode(cm.generate())
+
+            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)
+            f1 = frame.attachNewNode(ls.create())
+            f2 = f1.copyTo(frame)
+            f2.setMat(shrinkMat)
+
+        # Instead of enabling transparency, we set a color blend
+        # attrib.  We do this because plain transparency would also
+        # enable an alpha test, which we don't want; we want to draw
+        # every pixel.
+        card.setAttrib(ColorBlendAttrib.make(
+            ColorBlendAttrib.MAdd,
+            ColorBlendAttrib.OIncomingAlpha,
+            ColorBlendAttrib.OOneMinusIncomingAlpha))
+        card.setBin('fixed', 0)
+        card.setTexture(self.tex)
+        card.setY(-1)  # the card gets pulled back, so the matte will z-test it out.
+        card.setDepthWrite(True)
+        card.setDepthTest(True)
+        #card.flattenStrong()
+        self.card = card
+
+        matte.setBin('fixed', 10)
+        matte.setDepthTest(True)
+        #matte.flattenStrong()
+        self.matte = matte
+
+        frame.setBin('fixed', 20)
+        #frame.flattenStrong()
+        self.frame = frame
+
+        root.reparentTo(tmw.canvas)
+
+        self.root = root
+
+class TexPlacement:
+    def __init__(self, l, r, b, t):
+        self.p = (l, r, b, t)
+        self.rotated = False
+
+    def intersects(self, other):
+        """ Returns True if the placements intersect, False
+        otherwise. """
+
+        ml, mr, mb, mt = self.p
+        tl, tr, tb, tt = other.p
+
+        return (tl < mr and tr > ml and
+                tb < mt and tt > mb)
+