diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index bf67a7e71b..28cde77b3c 100644 --- a/direct/src/showbase/ShowBase.py +++ b/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 diff --git a/direct/src/showutil/TexMemWatcher.py b/direct/src/showutil/TexMemWatcher.py index 33400cc045..4519f22ee7 100644 --- a/direct/src/showutil/TexMemWatcher.py +++ b/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)