Browse Source

Merge branch 'master' into webgl-port

rdb 3 years ago
parent
commit
902e8a4a76
100 changed files with 3722 additions and 1066 deletions
  1. 12 0
      cmake/modules/FindGTK3.cmake
  2. 1 107
      direct/src/showbase/PythonUtil.py
  3. 1 1
      doc/INSTALL
  4. 42 5
      dtool/Config.cmake
  5. 8 7
      dtool/Package.cmake
  6. 5 1
      dtool/dtool_config.h.in
  7. 1 1
      dtool/src/dtoolbase/CMakeLists.txt
  8. 1 1
      dtool/src/dtoolbase/deletedBufferChain.I
  9. 13 11
      dtool/src/dtoolbase/deletedBufferChain.cxx
  10. 3 2
      dtool/src/dtoolbase/deletedBufferChain.h
  11. 11 1
      dtool/src/dtoolbase/dtoolbase.h
  12. 0 5
      dtool/src/dtoolbase/dtoolbase_cc.h
  13. 12 0
      dtool/src/dtoolbase/memoryHook.cxx
  14. 0 2
      dtool/src/dtoolbase/mutexSpinlockImpl.h
  15. 267 0
      dtool/src/dtoolbase/patomic.I
  16. 108 0
      dtool/src/dtoolbase/patomic.h
  17. 14 16
      dtool/src/dtoolbase/typeHandle.cxx
  18. 1 2
      dtool/src/dtoolbase/typeRegistryNode.cxx
  19. 2 1
      dtool/src/dtoolbase/typeRegistryNode.h
  20. 2 2
      dtool/src/interrogatedb/py_panda.cxx
  21. 2 5
      dtool/src/prc/notify.cxx
  22. 66 29
      makepanda/makepanda.py
  23. 5 2
      makepanda/makepandacore.py
  24. 6 3
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  25. 2 0
      panda/src/cocoadisplay/config_cocoadisplay.h
  26. 5 0
      panda/src/cocoadisplay/config_cocoadisplay.mm
  27. 23 4
      panda/src/device/ioKitInputDeviceManager.cxx
  28. 1 0
      panda/src/event/asyncFuture.cxx
  29. 49 32
      panda/src/express/referenceCount.I
  30. 11 8
      panda/src/express/referenceCount.cxx
  31. 3 2
      panda/src/express/referenceCount.h
  32. 7 0
      panda/src/express/weakPointerToBase.I
  33. 3 3
      panda/src/express/weakReferenceList.I
  34. 2 2
      panda/src/express/weakReferenceList.cxx
  35. 3 2
      panda/src/express/weakReferenceList.h
  36. 14 0
      panda/src/mathutil/plane_src.cxx
  37. 1 0
      panda/src/mathutil/plane_src.h
  38. 1 1
      panda/src/pgraph/cacheStats.I
  39. 3 2
      panda/src/pgraph/cacheStats.cxx
  40. 2 1
      panda/src/pgraph/cacheStats.h
  41. 0 2
      panda/src/pgraph/renderState.cxx
  42. 2 5
      panda/src/pgui/pgScrollFrame.h
  43. 2 5
      panda/src/physics/physicalNode.cxx
  44. 0 16
      panda/src/pipeline/conditionVarDummyImpl.I
  45. 17 0
      panda/src/pipeline/conditionVarDummyImpl.cxx
  46. 2 3
      panda/src/pipeline/conditionVarDummyImpl.h
  47. 2 1
      panda/src/pipeline/thread.h
  48. 2 20
      panda/src/pipeline/threadPosixImpl.I
  49. 28 27
      panda/src/pipeline/threadPosixImpl.cxx
  50. 3 4
      panda/src/pipeline/threadPosixImpl.h
  51. 0 24
      panda/src/pipeline/threadWin32Impl.I
  52. 44 32
      panda/src/pipeline/threadWin32Impl.cxx
  53. 2 6
      panda/src/pipeline/threadWin32Impl.h
  54. 8 10
      panda/src/pstatclient/pStatClient.I
  55. 77 83
      panda/src/pstatclient/pStatClient.cxx
  56. 7 8
      panda/src/pstatclient/pStatClient.h
  57. 6 0
      panda/src/pstatclient/pStatClientControlMessage.cxx
  58. 1 0
      panda/src/pstatclient/pStatClientControlMessage.h
  59. 7 1
      panda/src/pstatclient/pStatClientImpl.cxx
  60. 1 0
      panda/src/pstatclient/pStatProperties.cxx
  61. 4 2
      pandatool/src/gtk-stats/CMakeLists.txt
  62. 1 1
      pandatool/src/gtk-stats/gtkStats.cxx
  63. 32 14
      pandatool/src/gtk-stats/gtkStatsChartMenu.cxx
  64. 551 0
      pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx
  65. 81 0
      pandatool/src/gtk-stats/gtkStatsFlameGraph.h
  66. 105 83
      pandatool/src/gtk-stats/gtkStatsGraph.cxx
  67. 27 28
      pandatool/src/gtk-stats/gtkStatsGraph.h
  68. 97 49
      pandatool/src/gtk-stats/gtkStatsLabel.cxx
  69. 18 14
      pandatool/src/gtk-stats/gtkStatsLabel.h
  70. 6 5
      pandatool/src/gtk-stats/gtkStatsLabelStack.cxx
  71. 145 92
      pandatool/src/gtk-stats/gtkStatsMonitor.cxx
  72. 6 6
      pandatool/src/gtk-stats/gtkStatsMonitor.h
  73. 59 41
      pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx
  74. 7 7
      pandatool/src/gtk-stats/gtkStatsPianoRoll.h
  75. 106 83
      pandatool/src/gtk-stats/gtkStatsStripChart.cxx
  76. 8 9
      pandatool/src/gtk-stats/gtkStatsStripChart.h
  77. 1 0
      pandatool/src/gtk-stats/gtkstats_composite1.cxx
  78. 4 1
      pandatool/src/pstatserver/CMakeLists.txt
  79. 1 0
      pandatool/src/pstatserver/p3pstatserver_composite1.cxx
  80. 89 0
      pandatool/src/pstatserver/pStatFlameGraph.I
  81. 297 0
      pandatool/src/pstatserver/pStatFlameGraph.cxx
  82. 107 0
      pandatool/src/pstatserver/pStatFlameGraph.h
  83. 8 0
      pandatool/src/pstatserver/pStatMonitor.I
  84. 4 2
      pandatool/src/pstatserver/pStatMonitor.cxx
  85. 5 1
      pandatool/src/pstatserver/pStatMonitor.h
  86. 3 1
      pandatool/src/pstatserver/pStatReader.cxx
  87. 9 0
      pandatool/src/pstatserver/pStatStripChart.I
  88. 64 5
      pandatool/src/pstatserver/pStatStripChart.cxx
  89. 3 1
      pandatool/src/pstatserver/pStatStripChart.h
  90. 50 7
      pandatool/src/text-stats/textMonitor.cxx
  91. 4 1
      pandatool/src/text-stats/textMonitor.h
  92. 17 2
      pandatool/src/text-stats/textStats.cxx
  93. 1 0
      pandatool/src/text-stats/textStats.h
  94. 4 2
      pandatool/src/win-stats/CMakeLists.txt
  95. 14 0
      pandatool/src/win-stats/winStats.cxx
  96. 21 8
      pandatool/src/win-stats/winStatsChartMenu.cxx
  97. 639 0
      pandatool/src/win-stats/winStatsFlameGraph.cxx
  98. 80 0
      pandatool/src/win-stats/winStatsFlameGraph.h
  99. 107 84
      pandatool/src/win-stats/winStatsGraph.cxx
  100. 13 9
      pandatool/src/win-stats/winStatsGraph.h

+ 12 - 0
cmake/modules/FindGTK3.cmake

@@ -0,0 +1,12 @@
+find_package(PkgConfig QUIET)
+
+set(__gtk3_required_version "${${CMAKE_FIND_PACKAGE_NAME}_FIND_VERSION}")
+if(__gtk3_required_version)
+    set(__gtk3_required_version " >= ${__gtk3_required_version}")
+endif()
+pkg_check_modules(GTK3 QUIET "gtk+-3.0${__gtk3_required_version}" IMPORTED_TARGET)
+
+if (NOT TARGET PkgConfig::GTK3)
+    set(GTK3_FOUND 0)
+endif()
+unset(__gtk3_required_version)

+ 1 - 107
direct/src/showbase/PythonUtil.py

@@ -11,7 +11,7 @@ __all__ = [
     'boolEqual', 'lineupPos', 'formatElapsedSeconds', 'solveQuadratic',
     'findPythonModule', 'mostDerivedLast', 'clampScalar', 'weightedChoice',
     'randFloat', 'normalDistrib', 'weightedRand', 'randUint31', 'randInt32',
-    'SerialNumGen', 'serialNum', 'uniqueName', 'Enum', 'Singleton',
+    'SerialNumGen', 'serialNum', 'uniqueName', 'Singleton',
     'SingletonError', 'printListEnum', 'safeRepr', 'fastRepr',
     'isDefaultValue', 'ScratchPad', 'Sync', 'itype', 'getNumberedTypedString',
     'getNumberedTypedSortedString', 'printNumberedTyped', 'DelayedCall',
@@ -1223,111 +1223,6 @@ def uniqueName(name):
     global _serialGen
     return '%s-%s' % (name, _serialGen.next())
 
-class EnumIter:
-    def __init__(self, enum):
-        self._values = tuple(enum._stringTable.keys())
-        self._index = 0
-    def __iter__(self):
-        return self
-    def __next__(self):
-        if self._index >= len(self._values):
-            raise StopIteration
-        self._index += 1
-        return self._values[self._index-1]
-
-class Enum:
-    """Pass in list of strings or string of comma-separated strings.
-    Items are accessible as instance.item, and are assigned unique,
-    increasing integer values. Pass in integer for 'start' to override
-    starting value.
-
-    Example:
-
-    >>> colors = Enum('red, green, blue')
-    >>> colors.red
-    0
-    >>> colors.green
-    1
-    >>> colors.blue
-    2
-    >>> colors.getString(colors.red)
-    'red'
-    """
-
-    if __debug__:
-        # chars that cannot appear within an item string.
-        def _checkValidIdentifier(item):
-            import string
-            invalidChars = string.whitespace + string.punctuation
-            invalidChars = invalidChars.replace('_', '')
-            invalidFirstChars = invalidChars+string.digits
-            if item[0] in invalidFirstChars:
-                raise SyntaxError("Enum '%s' contains invalid first char" %
-                                    item)
-            if not disjoint(item, invalidChars):
-                for char in item:
-                    if char in invalidChars:
-                        raise SyntaxError(
-                            "Enum\n'%s'\ncontains illegal char '%s'" %
-                            (item, char))
-            return 1
-        _checkValidIdentifier = staticmethod(_checkValidIdentifier)
-
-    def __init__(self, items, start=0):
-        if isinstance(items, str):
-            items = items.split(',')
-
-        self._stringTable = {}
-
-        # make sure we don't overwrite an existing element of the class
-        assert self._checkExistingMembers(items)
-        assert uniqueElements(items)
-
-        i = start
-        for item in items:
-            # remove leading/trailing whitespace
-            item = item.strip()
-            # is there anything left?
-            if len(item) == 0:
-                continue
-            # make sure there are no invalid characters
-            assert Enum._checkValidIdentifier(item)
-            self.__dict__[item] = i
-            self._stringTable[i] = item
-            i += 1
-
-    def __iter__(self):
-        return EnumIter(self)
-
-    def hasString(self, string):
-        return string in set(self._stringTable.values())
-
-    def fromString(self, string):
-        if self.hasString(string):
-            return self.__dict__[string]
-        # throw an error
-        {}[string]
-
-    def getString(self, value):
-        return self._stringTable[value]
-
-    def __contains__(self, value):
-        return value in self._stringTable
-
-    def __len__(self):
-        return len(self._stringTable)
-
-    def copyTo(self, obj):
-        # copies all members onto obj
-        for name, value in self._stringTable:
-            setattr(obj, name, value)
-
-    if __debug__:
-        def _checkExistingMembers(self, items):
-            for item in items:
-                if hasattr(self, item):
-                    return 0
-            return 1
 
 ############################################################
 # class: Singleton
@@ -2635,7 +2530,6 @@ class PriorityCallbacks:
 builtins.Functor = Functor
 builtins.Stack = Stack
 builtins.Queue = Queue
-builtins.Enum = Enum
 builtins.SerialNumGen = SerialNumGen
 builtins.SerialMaskedGen = SerialMaskedGen
 builtins.ScratchPad = ScratchPad

+ 1 - 1
doc/INSTALL

@@ -186,7 +186,7 @@ it will show you the available command-line options:
   --use-opencv      --no-opencv    (enable/disable use of OPENCV)
   --use-directcam   --no-directcam (enable/disable use of DIRECTCAM)
   --use-vision      --no-vision    (enable/disable use of VISION)
-  --use-gtk2        --no-gtk2      (enable/disable use of GTK2)
+  --use-gtk3        --no-gtk3      (enable/disable use of GTK3)
   --use-npapi       --no-npapi     (enable/disable use of NPAPI)
   --use-mfc         --no-mfc       (enable/disable use of MFC)
   --use-wx          --no-wx        (enable/disable use of WX)

+ 42 - 5
dtool/Config.cmake

@@ -293,6 +293,26 @@ mark_as_advanced(SIMULATE_NETWORK_DELAY DO_MEMORY_USAGE DO_DCAST)
 # The following options have to do with the memory allocation system.
 #
 
+find_package(MIMALLOC 1.0 QUIET)
+
+package_option(MIMALLOC
+  "The mimalloc allocator.  See also USE_MEMORY_MIMALLOC, which
+you will need to use to activate it by default.  If you do not set
+USE_MEMORY_MIMALLOC, Panda will decide whether to use it."
+  IMPORTED_AS mimalloc-static)
+
+if (WIN32 AND HAVE_MIMALLOC)
+  set(_prefer_mimalloc ON)
+else()
+  set(_prefer_mimalloc OFF)
+endif()
+
+option(USE_MEMORY_MIMALLOC
+  "This is an optional memory allocator with good multi-threading
+support.  It is recommended on Windows, where it gives much better
+performance than the built-in malloc.  However, it does not appear
+to be significantly faster on glibc-based systems." ${_prefer_mimalloc})
+
 option(USE_MEMORY_DLMALLOC
   "This is an optional alternative memory-allocation scheme
 available within Panda.  You can experiment with it to see
@@ -312,16 +332,33 @@ if 16-byte alignment must be performed on top of it, wasting up to
 is required and not provided by the system malloc library, then an
 alternative malloc system (above) will be used instead." OFF)
 
-option(USE_DELETED_CHAIN
-  "Define this true to use the DELETED_CHAIN macros, which support
+if (WIN32 AND NOT HAVE_MIMALLOC)
+  option(USE_DELETED_CHAIN
+    "Define this true to use the DELETED_CHAIN macros, which support
 fast re-use of existing allocated blocks, minimizing the low-level
 calls to malloc() and free() for frequently-created and -deleted
-objects.  There's usually no reason to set this false, unless you
-suspect a bug in Panda's memory management code." ON)
+objects.  This is significantly better than built-in malloc on Windows
+but suffers with multiple threads, where mimalloc performs better, so
+it is preferred to get mimalloc instead and turn this OFF." ON)
+else()
+  option(USE_DELETED_CHAIN
+    "Define this true to use the DELETED_CHAIN macros, which support
+fast re-use of existing allocated blocks, minimizing the low-level
+calls to malloc() and free() for frequently-created and -deleted
+objects.  However, modern memory allocators generally perform as good,
+especially with threading, so best leave this OFF." OFF)
+endif()
 
 mark_as_advanced(USE_MEMORY_DLMALLOC USE_MEMORY_PTMALLOC2
-  MEMORY_HOOK_DO_ALIGN USE_DELETED_CHAIN)
+  USE_MEMORY_MIMALLOC MEMORY_HOOK_DO_ALIGN USE_DELETED_CHAIN)
+
+if(USE_MEMORY_MIMALLOC)
+  package_status(MIMALLOC "mimalloc memory allocator")
+else()
+  package_status(MIMALLOC "mimalloc memory allocator (not used)")
+endif()
 
+unset(_prefer_mimalloc)
 
 #
 # This section relates to mobile-device/phone support and options

+ 8 - 7
dtool/Package.cmake

@@ -556,16 +556,17 @@ package_option(HarfBuzz
 
 package_status(HarfBuzz "HarfBuzz")
 
-# GTK2
+# GTK3
 
-set(Freetype_FIND_QUIETLY TRUE) # Fix for builtin FindGTK2
-set(GTK2_GTK_FIND_QUIETLY TRUE) # Fix for builtin FindGTK2
-find_package(GTK2 QUIET COMPONENTS gtk)
-
-package_option(GTK2)
+if(NOT WIN32)
+  find_package(GTK3 QUIET)
+endif()
 
-package_status(GTK2 "gtk+-2")
+package_option(GTK3
+  "This is necessary to build the PStats performance analysis tool on platforms
+  other than Windows.")
 
+package_status(GTK3 "gtk+-3")
 
 #
 # ------------ Physics engines ------------

+ 5 - 1
dtool/dtool_config.h.in

@@ -130,8 +130,12 @@
 /* Define if we want to support fixed-function OpenGL rendering. */
 #cmakedefine SUPPORT_FIXED_FUNCTION
 
-/* Define for either of the alternative malloc schemes. */
+/* Define if we have mimalloc available. */
+#cmakedefine HAVE_MIMALLOC
+
+/* Define for one of the alternative malloc schemes. */
 #cmakedefine USE_MEMORY_DLMALLOC
+#cmakedefine USE_MEMORY_MIMALLOC
 #cmakedefine USE_MEMORY_PTMALLOC2
 
 /* Define if we want to compile in support for pipelining.  */

+ 1 - 1
dtool/src/dtoolbase/CMakeLists.txt

@@ -92,7 +92,7 @@ add_component_library(p3dtoolbase NOINIT SYMBOL BUILDING_DTOOL_DTOOLBASE
 target_include_directories(p3dtoolbase PUBLIC
   $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
   $<BUILD_INTERFACE:${PANDA_OUTPUT_DIR}/include>)
-target_link_libraries(p3dtoolbase PKG::EIGEN PKG::THREADS)
+target_link_libraries(p3dtoolbase PKG::EIGEN PKG::THREADS PKG::MIMALLOC)
 target_interrogate(p3dtoolbase ${P3DTOOLBASE_SOURCES} EXTENSIONS ${P3DTOOLBASE_IGATEEXT})
 
 if(NOT BUILD_METALIBS)

+ 1 - 1
dtool/src/dtoolbase/deletedBufferChain.I

@@ -27,7 +27,7 @@ validate(void *ptr) {
 
 #if defined(USE_DELETEDCHAINFLAG) && defined(USE_DELETED_CHAIN)
   const ObjectNode *obj = buffer_to_node(ptr);
-  return AtomicAdjust::get(obj->_flag) == DCF_alive;
+  return obj->_flag.load(std::memory_order_relaxed) == DCF_alive;
 #else
   return true;
 #endif  // USE_DELETEDCHAINFLAG

+ 13 - 11
dtool/src/dtoolbase/deletedBufferChain.cxx

@@ -50,8 +50,8 @@ allocate(size_t size, TypeHandle type_handle) {
     _lock.unlock();
 
 #ifdef USE_DELETEDCHAINFLAG
-    assert(obj->_flag == (AtomicAdjust::Integer)DCF_deleted);
-    obj->_flag = DCF_alive;
+    DeletedChainFlag orig_flag = obj->_flag.exchange(DCF_alive, std::memory_order_relaxed);
+    assert(orig_flag == DCF_deleted);
 #endif  // USE_DELETEDCHAINFLAG
 
     void *ptr = node_to_buffer(obj);
@@ -75,7 +75,7 @@ allocate(size_t size, TypeHandle type_handle) {
   obj = (ObjectNode *)(aligned - flag_reserved_bytes);
 
 #ifdef USE_DELETEDCHAINFLAG
-  obj->_flag = DCF_alive;
+  obj->_flag.store(DCF_alive, std::memory_order_relaxed);
 #endif  // USE_DELETEDCHAINFLAG
 
   void *ptr = node_to_buffer(obj);
@@ -116,14 +116,16 @@ deallocate(void *ptr, TypeHandle type_handle) {
   ObjectNode *obj = buffer_to_node(ptr);
 
 #ifdef USE_DELETEDCHAINFLAG
-  AtomicAdjust::Integer orig_flag = AtomicAdjust::compare_and_exchange(obj->_flag, DCF_alive, DCF_deleted);
-
-  // If this assertion is triggered, you double-deleted an object.
-  assert(orig_flag != (AtomicAdjust::Integer)DCF_deleted);
-
-  // If this assertion is triggered, you tried to delete an object that was
-  // never allocated, or you have heap corruption.
-  assert(orig_flag == (AtomicAdjust::Integer)DCF_alive);
+  DeletedChainFlag orig_flag = DCF_alive;
+  if (UNLIKELY(!obj->_flag.compare_exchange_strong(orig_flag, DCF_deleted,
+                                                   std::memory_order_relaxed))) {
+    // If this assertion is triggered, you double-deleted an object.
+    assert(orig_flag != DCF_deleted);
+
+    // If this assertion is triggered, you tried to delete an object that was
+    // never allocated, or you have heap corruption.
+    assert(orig_flag == DCF_alive);
+  }
 #endif  // USE_DELETEDCHAINFLAG
 
   _lock.lock();

+ 3 - 2
dtool/src/dtoolbase/deletedBufferChain.h

@@ -20,6 +20,7 @@
 #include "atomicAdjust.h"
 #include "numeric_types.h"
 #include "typeHandle.h"
+#include "patomic.h"
 #include <assert.h>
 
 // Though it's tempting, it doesn't seem to be possible to implement
@@ -37,7 +38,7 @@
 #endif // NDEBUG
 
 #ifdef USE_DELETEDCHAINFLAG
-enum DeletedChainFlag {
+enum DeletedChainFlag : unsigned int {
   DCF_deleted = 0xfeedba0f,
   DCF_alive = 0x12487654,
 };
@@ -73,7 +74,7 @@ private:
     // In development mode, we piggyback this extra data.  This is maintained
     // out-of-band from the actual pointer returned, so we can safely use this
     // flag to indicate the difference between allocated and freed pointers.
-    TVOLATILE AtomicAdjust::Integer _flag;
+    patomic<DeletedChainFlag> _flag;
 #endif
 
     // This pointer sits within the buffer, in the same space referenced by

+ 11 - 1
dtool/src/dtoolbase/dtoolbase.h

@@ -375,6 +375,10 @@ typedef struct _object PyObject;
 // This specialized malloc implementation can perform the required alignment.
 #undef MEMORY_HOOK_DO_ALIGN
 
+#elif defined(USE_MEMORY_MIMALLOC)
+// This one does, too.
+#undef MEMORY_HOOK_DO_ALIGN
+
 #elif defined(USE_MEMORY_PTMALLOC2)
 // But not this one.  For some reason it crashes when we try to build it with
 // alignment 16.  So if we're using ptmalloc2, we need to enforce alignment
@@ -385,6 +389,12 @@ typedef struct _object PyObject;
 // The OS-provided malloc implementation will do the required alignment.
 #undef MEMORY_HOOK_DO_ALIGN
 
+#elif defined(HAVE_MIMALLOC) && defined(_WIN32)
+// Prefer mimalloc on Windows, if we have it.  It is significantly faster than
+// standard malloc, supports multi-threading well and does the alignment too.
+#undef MEMORY_HOOK_DO_ALIGN
+#define USE_MEMORY_MIMALLOC 1
+
 #elif defined(MEMORY_HOOK_DO_ALIGN)
 // We need memory alignment, and we're willing to provide it ourselves.
 
@@ -426,7 +436,7 @@ typedef struct _object PyObject;
 #endif
 
 /* Determine our memory-allocation requirements. */
-#if defined(USE_MEMORY_PTMALLOC2) || defined(USE_MEMORY_DLMALLOC) || defined(DO_MEMORY_USAGE) || defined(MEMORY_HOOK_DO_ALIGN)
+#if defined(USE_MEMORY_MIMALLOC) || defined(USE_MEMORY_PTMALLOC2) || defined(USE_MEMORY_DLMALLOC) || defined(DO_MEMORY_USAGE) || defined(MEMORY_HOOK_DO_ALIGN)
 /* In this case we have some custom memory management requirements. */
 #else
 /* Otherwise, if we have no custom memory management needs at all, we

+ 0 - 5
dtool/src/dtoolbase/dtoolbase_cc.h

@@ -49,8 +49,6 @@
 // interrogate pass (CPPPARSER isn't defined), this maps to public.
 #define PUBLISHED __published
 
-#define PHAVE_ATOMIC 1
-
 typedef int ios_openmode;
 typedef int ios_fmtflags;
 typedef int ios_iostate;
@@ -112,9 +110,6 @@ typedef std::ios::seekdir ios_seekdir;
 #define INLINE inline
 #endif
 
-// Expect that we have access to the <atomic> header.
-#define PHAVE_ATOMIC 1
-
 // Determine the availability of C++11 features.
 #if defined(_MSC_VER) && _MSC_VER < 1900 // Visual Studio 2015
 #error Microsoft Visual C++ 2015 or later is required to compile Panda3D.

+ 12 - 0
dtool/src/dtoolbase/memoryHook.cxx

@@ -51,6 +51,18 @@ static_assert((MEMORY_HOOK_ALIGNMENT & (MEMORY_HOOK_ALIGNMENT - 1)) == 0,
 
 #if defined(CPPPARSER)
 
+#elif defined(USE_MEMORY_MIMALLOC)
+
+// mimalloc is a modern memory manager by Microsoft that is very fast as well
+// as thread-safe.
+
+#include "mimalloc.h"
+
+#define call_malloc mi_malloc
+#define call_realloc mi_realloc
+#define call_free mi_free
+#undef MEMORY_HOOK_MALLOC_LOCK
+
 #elif defined(USE_MEMORY_DLMALLOC)
 
 // Memory manager: DLMALLOC This is Doug Lea's memory manager.  It is very

+ 0 - 2
dtool/src/dtoolbase/mutexSpinlockImpl.h

@@ -19,9 +19,7 @@
 
 #ifdef MUTEX_SPINLOCK
 
-#ifdef PHAVE_ATOMIC
 #include <atomic>
-#endif
 
 /**
  * Uses a simple user-space spinlock to implement a mutex.  It is usually not

+ 267 - 0
dtool/src/dtoolbase/patomic.I

@@ -0,0 +1,267 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file patomic.I
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+/**
+ * Value initializer.
+ */
+template<class T>
+constexpr patomic<T>::
+patomic(T desired) noexcept : _value(desired) {
+}
+
+/**
+ * Returns true if this is a lock free type (which it always is).
+ */
+template<class T>
+ALWAYS_INLINE bool patomic<T>::
+is_lock_free() const noexcept {
+  return true;
+}
+
+/**
+ * Returns the stored value.
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+load(std::memory_order order) const noexcept {
+  return _value;
+}
+
+/**
+ * Returns the stored value.
+ */
+template<class T>
+ALWAYS_INLINE patomic<T>::
+operator T() const noexcept {
+  return _value;
+}
+
+/**
+ * Changes the stored value.
+ */
+template<class T>
+ALWAYS_INLINE void patomic<T>::
+store(T desired, std::memory_order order) noexcept {
+  _value = desired;
+}
+
+/**
+ * Changes the stored value.
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator=(T desired) noexcept {
+  _value = desired;
+}
+
+/**
+ * Changes the stored value, returning the previous value.
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+exchange(T desired, std::memory_order) noexcept {
+  T current = _value;
+  _value = desired;
+  return current;
+}
+
+/**
+ * Sets the desired value if the current value is as the first argument.
+ * If it is not, the current value is written to expected.
+ */
+template<class T>
+ALWAYS_INLINE bool patomic<T>::
+compare_exchange_weak(T &expected, T desired,
+                      std::memory_order, std::memory_order) noexcept {
+  T current = _value;
+  if (_value == expected) {
+    _value = desired;
+    return true;
+  } else {
+    expected = current;
+    return false;
+  }
+}
+
+/**
+ * Sets the desired value if the current value is as the first argument.
+ * If it is not, the current value is written to expected.
+ */
+template<class T>
+ALWAYS_INLINE bool patomic<T>::
+compare_exchange_strong(T &expected, T desired,
+                        std::memory_order, std::memory_order) noexcept {
+  T current = _value;
+  if (_value == expected) {
+    _value = desired;
+    return true;
+  } else {
+    expected = current;
+    return false;
+  }
+}
+
+/**
+ * Adds to the stored value, returns the old value.
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+fetch_add(T arg, std::memory_order) noexcept {
+  T old = _value;
+  _value += arg;
+  return old;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+fetch_sub(T arg, std::memory_order) noexcept {
+  T old = _value;
+  _value -= arg;
+  return old;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+fetch_and(T arg, std::memory_order) noexcept {
+  T old = _value;
+  _value &= arg;
+  return old;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+fetch_or(T arg, std::memory_order) noexcept {
+  T old = _value;
+  _value |= arg;
+  return old;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+fetch_xor(T arg, std::memory_order) noexcept {
+  T old = _value;
+  _value ^= arg;
+  return old;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator ++(int) noexcept {
+  return _value++;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator --(int) noexcept {
+  return _value--;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator ++() noexcept {
+  return ++_value;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator --() noexcept {
+  return --_value;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator +=(T arg) noexcept {
+  return _value += arg;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator -=(T arg) noexcept {
+  return _value -= arg;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator &=(T arg) noexcept {
+  return _value &= arg;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator |=(T arg) noexcept {
+  return _value |= arg;
+}
+
+/**
+ *
+ */
+template<class T>
+ALWAYS_INLINE T patomic<T>::
+operator ^=(T arg) noexcept {
+  return _value ^= arg;
+}
+
+
+/**
+ * Sets the flag to true and returns the previous value.
+ */
+ALWAYS_INLINE bool patomic_flag::
+test_and_set(std::memory_order order) noexcept {
+  bool value = __internal_flag;
+  __internal_flag = true;
+  return value;
+}
+
+/**
+ * Sets the flag to false.
+ */
+ALWAYS_INLINE void patomic_flag::
+clear(std::memory_order order) noexcept {
+  __internal_flag = false;
+}

+ 108 - 0
dtool/src/dtoolbase/patomic.h

@@ -0,0 +1,108 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file patomic.h
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#ifndef PATOMIC_H
+#define PATOMIC_H
+
+#include "dtoolbase.h"
+#include "selectThreadImpl.h"
+
+#include <atomic>
+
+#if defined(THREAD_DUMMY_IMPL) || defined(THREAD_SIMPLE_IMPL)
+
+/**
+ * Dummy implementation of std::atomic that does not do any atomic operations,
+ * used when compiling without HAVE_THREADS or with SIMPLE_THREADS.
+ */
+template<class T>
+struct patomic {
+  using value_type = T;
+
+  constexpr patomic() noexcept = default;
+  constexpr patomic(T desired) noexcept;
+
+  ALWAYS_INLINE patomic(const patomic &) = delete;
+  ALWAYS_INLINE patomic &operator=(const patomic &) = delete;
+
+  static constexpr bool is_always_lock_free = true;
+  ALWAYS_INLINE bool is_lock_free() const noexcept;
+
+  ALWAYS_INLINE T load(std::memory_order order = std::memory_order_seq_cst) const noexcept;
+  ALWAYS_INLINE operator T() const noexcept;
+
+  ALWAYS_INLINE void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE T operator=(T desired) noexcept;
+
+  ALWAYS_INLINE T exchange(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
+
+  ALWAYS_INLINE bool compare_exchange_weak(T &expected, T desired,
+                                           std::memory_order success = std::memory_order_seq_cst,
+                                           std::memory_order failure = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE bool compare_exchange_strong(T &expected, T desired,
+                                             std::memory_order success = std::memory_order_seq_cst,
+                                             std::memory_order failure = std::memory_order_seq_cst) noexcept;
+
+  ALWAYS_INLINE T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE T fetch_and(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE T fetch_or(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE T fetch_xor(T arg, std::memory_order order = std::memory_order_seq_cst) noexcept;
+
+  ALWAYS_INLINE T operator ++(int) noexcept;
+  ALWAYS_INLINE T operator --(int) noexcept;
+  ALWAYS_INLINE T operator ++() noexcept;
+  ALWAYS_INLINE T operator --() noexcept;
+  ALWAYS_INLINE T operator +=(T arg) noexcept;
+  ALWAYS_INLINE T operator -=(T arg) noexcept;
+  ALWAYS_INLINE T operator &=(T arg) noexcept;
+  ALWAYS_INLINE T operator |=(T arg) noexcept;
+  ALWAYS_INLINE T operator ^=(T arg) noexcept;
+
+private:
+  T _value;
+};
+
+/**
+ * Dummy implementation of std::atomic_flag that does not do any atomic
+ * operations.
+ */
+struct EXPCL_DTOOL_DTOOLBASE patomic_flag {
+  constexpr patomic_flag() noexcept = default;
+
+  patomic_flag(const patomic_flag &) = delete;
+  patomic_flag &operator=(const patomic_flag &) = delete;
+
+  ALWAYS_INLINE bool test_and_set(std::memory_order order = std::memory_order_seq_cst) noexcept;
+  ALWAYS_INLINE void clear(std::memory_order order = std::memory_order_seq_cst) noexcept;
+
+  bool __internal_flag = false;
+};
+
+#define patomic_thread_fence(order) (std::atomic_signal_fence((order)))
+
+#include "patomic.I"
+
+#else
+
+// We're using real threading, so use the real implementation.
+template<class T>
+using patomic = std::atomic<T>;
+
+typedef std::atomic_flag patomic_flag;
+
+#define patomic_thread_fence(order) (std::atomic_thread_fence((order)))
+
+#endif
+
+#endif

+ 14 - 16
dtool/src/dtoolbase/typeHandle.cxx

@@ -13,7 +13,6 @@
 
 #include "typeHandle.h"
 #include "typeRegistryNode.h"
-#include "atomicAdjust.h"
 
 /**
  * Returns the total allocated memory used by objects of this type, for the
@@ -29,7 +28,7 @@ get_memory_usage(MemoryClass memory_class) const {
   } else {
     TypeRegistryNode *rnode = TypeRegistry::ptr()->look_up(*this, nullptr);
     assert(rnode != nullptr);
-    return (size_t)AtomicAdjust::get(rnode->_memory_usage[memory_class]);
+    return rnode->_memory_usage[memory_class].load(std::memory_order_relaxed);
   }
 #endif  // DO_MEMORY_USAGE
   return 0;
@@ -48,10 +47,8 @@ inc_memory_usage(MemoryClass memory_class, size_t size) {
   if ((*this) != TypeHandle::none()) {
     TypeRegistryNode *rnode = TypeRegistry::ptr()->look_up(*this, nullptr);
     assert(rnode != nullptr);
-    AtomicAdjust::add(rnode->_memory_usage[memory_class], (AtomicAdjust::Integer)size);
-    // cerr << *this << ".inc(" << memory_class << ", " << size << ") -> " <<
-    // rnode->_memory_usage[memory_class] << "\n";
-    if (rnode->_memory_usage[memory_class] < 0) {
+    size_t prev = rnode->_memory_usage[memory_class].fetch_add(size, std::memory_order_relaxed);
+    if (prev + size < prev) {
       std::cerr << "Memory usage overflow for type " << rnode->_name << ".\n";
       abort();
     }
@@ -72,10 +69,8 @@ dec_memory_usage(MemoryClass memory_class, size_t size) {
   if ((*this) != TypeHandle::none()) {
     TypeRegistryNode *rnode = TypeRegistry::ptr()->look_up(*this, nullptr);
     assert(rnode != nullptr);
-    AtomicAdjust::add(rnode->_memory_usage[memory_class], -(AtomicAdjust::Integer)size);
-    // cerr << *this << ".dec(" << memory_class << ", " << size << ") -> " <<
-    // rnode->_memory_usage[memory_class] << "\n";
-    assert(rnode->_memory_usage[memory_class] >= 0);
+    size_t prev = rnode->_memory_usage[memory_class].fetch_sub(size, std::memory_order_relaxed);
+    assert(prev - size <= prev);
   }
 #endif  // DO_MEMORY_USAGE
 }
@@ -97,8 +92,8 @@ allocate_array(size_t size) {
 #endif
     TypeRegistryNode *rnode = TypeRegistry::ptr()->look_up(*this, nullptr);
     assert(rnode != nullptr);
-    AtomicAdjust::add(rnode->_memory_usage[MC_array], (AtomicAdjust::Integer)alloc_size);
-    if (rnode->_memory_usage[MC_array] < 0) {
+    size_t prev = rnode->_memory_usage[MC_array].fetch_add(alloc_size, std::memory_order_relaxed);
+    if (prev + size < prev) {
       std::cerr << "Memory usage overflow for type " << rnode->_name << ".\n";
       abort();
     }
@@ -124,8 +119,11 @@ reallocate_array(void *old_ptr, size_t size) {
 
     TypeRegistryNode *rnode = TypeRegistry::ptr()->look_up(*this, nullptr);
     assert(rnode != nullptr);
-    AtomicAdjust::add(rnode->_memory_usage[MC_array], (AtomicAdjust::Integer)new_size - (AtomicAdjust::Integer)old_size);
-    assert(rnode->_memory_usage[MC_array] >= 0);
+    if (new_size > old_size) {
+      rnode->_memory_usage[MC_array].fetch_add(new_size - old_size, std::memory_order_relaxed);
+    } else {
+      rnode->_memory_usage[MC_array].fetch_sub(old_size - new_size, std::memory_order_relaxed);
+    }
   }
 #else
   void *new_ptr = PANDA_REALLOC_ARRAY(old_ptr, size);
@@ -146,8 +144,8 @@ deallocate_array(void *ptr) {
   if ((*this) != TypeHandle::none()) {
     TypeRegistryNode *rnode = TypeRegistry::ptr()->look_up(*this, nullptr);
     assert(rnode != nullptr);
-    AtomicAdjust::add(rnode->_memory_usage[MC_array], -(AtomicAdjust::Integer)alloc_size);
-    assert(rnode->_memory_usage[MC_array] >= 0);
+    size_t prev = rnode->_memory_usage[MC_array].fetch_sub(alloc_size, std::memory_order_relaxed);
+    assert(prev - alloc_size <= prev);
   }
 #endif  // DO_MEMORY_USAGE
   PANDA_FREE_ARRAY(ptr);

+ 1 - 2
dtool/src/dtoolbase/typeRegistryNode.cxx

@@ -23,10 +23,9 @@ bool TypeRegistryNode::_paranoid_inheritance = false;
  */
 TypeRegistryNode::
 TypeRegistryNode(TypeHandle handle, const std::string &name, TypeHandle &ref) :
-  _handle(handle), _name(name), _ref(ref)
+  _handle(handle), _name(name), _ref(ref), _memory_usage{}
 {
   clear_subtree();
-  memset(_memory_usage, 0, sizeof(_memory_usage));
 }
 
 /**

+ 2 - 1
dtool/src/dtoolbase/typeRegistryNode.h

@@ -18,6 +18,7 @@
 
 #include "typeHandle.h"
 #include "numeric_types.h"
+#include "patomic.h"
 
 #include <assert.h>
 #include <vector>
@@ -50,7 +51,7 @@ public:
   Classes _child_classes;
   PyObject *_python_type = nullptr;
 
-  AtomicAdjust::Integer _memory_usage[TypeHandle::MC_limit];
+  patomic<size_t> _memory_usage[TypeHandle::MC_limit];
 
   static bool _paranoid_inheritance;
 

+ 2 - 2
dtool/src/interrogatedb/py_panda.cxx

@@ -744,7 +744,7 @@ PyObject *copy_from_make_copy(PyObject *self, PyObject *noargs) {
   if (callable == nullptr) {
     return nullptr;
   }
-  PyObject *result = _PyObject_CallNoArg(callable);
+  PyObject *result = PyObject_CallNoArgs(callable);
   Py_DECREF(callable);
   return result;
 }
@@ -768,7 +768,7 @@ PyObject *map_deepcopy_to_copy(PyObject *self, PyObject *args) {
   if (callable == nullptr) {
     return nullptr;
   }
-  PyObject *result = _PyObject_CallNoArg(callable);
+  PyObject *result = PyObject_CallNoArgs(callable);
   Py_DECREF(callable);
   return result;
 }

+ 2 - 5
dtool/src/prc/notify.cxx

@@ -18,13 +18,10 @@
 #include "configVariableBool.h"
 #include "filename.h"
 #include "config_prc.h"
+#include "patomic.h"
 
 #include <ctype.h>
 
-#ifdef PHAVE_ATOMIC
-#include <atomic>
-#endif
-
 #ifdef BUILD_IPHONE
 #include <fcntl.h>
 #endif
@@ -503,7 +500,7 @@ config_initialized() {
        "The filename to which to write all the output of notify");
 
     // We use this to ensure that only one thread can initialize the output.
-    static std::atomic_flag initialized = ATOMIC_FLAG_INIT;
+    static patomic_flag initialized = ATOMIC_FLAG_INIT;
 
     std::string value = notify_output.get_value();
     if (!value.empty() && !initialized.test_and_set()) {

+ 66 - 29
makepanda/makepanda.py

@@ -93,7 +93,7 @@ PkgListSet(["PYTHON", "DIRECT",                        # Python support
   "VRPN", "OPENSSL",                                   # Transport
   "FFTW",                                              # Algorithm helpers
   "ARTOOLKIT", "OPENCV", "DIRECTCAM", "VISION",        # Augmented Reality
-  "GTK2",                                              # GTK2 is used for PStats on Unix
+  "GTK3",                                              # GTK3 is used for PStats on Unix
   "MFC", "WX", "FLTK",                                 # Used for web plug-in only
   "COCOA",                                             # macOS toolkits
   "X11",                                               # Unix platform support
@@ -103,6 +103,7 @@ PkgListSet(["PYTHON", "DIRECT",                        # Python support
   "PANDAPARTICLESYSTEM",                               # Built in particle system
   "CONTRIB",                                           # Experimental
   "SSE2", "NEON",                                      # Compiler features
+  "MIMALLOC",                                          # Memory allocators
 ])
 
 CheckPandaSourceTree()
@@ -633,13 +634,19 @@ if (COMPILER == "MSVC"):
     if (PkgSkip("DIRECTCAM")==0): LibName("DIRECTCAM", "quartz.lib")
     if (PkgSkip("DIRECTCAM")==0): LibName("DIRECTCAM", "odbc32.lib")
     if (PkgSkip("DIRECTCAM")==0): LibName("DIRECTCAM", "odbccp32.lib")
+    if (PkgSkip("MIMALLOC")==0): LibName("MIMALLOC", GetThirdpartyDir() + "mimalloc/lib/mimalloc-static.lib")
     if (PkgSkip("OPENSSL")==0):
         if os.path.isfile(GetThirdpartyDir() + "openssl/lib/libpandassl.lib"):
             LibName("OPENSSL", GetThirdpartyDir() + "openssl/lib/libpandassl.lib")
             LibName("OPENSSL", GetThirdpartyDir() + "openssl/lib/libpandaeay.lib")
-        else:
+        elif os.path.isfile(GetThirdpartyDir() + "openssl/lib/ssleay32.lib"):
             LibName("OPENSSL", GetThirdpartyDir() + "openssl/lib/libeay32.lib")
             LibName("OPENSSL", GetThirdpartyDir() + "openssl/lib/ssleay32.lib")
+        else:
+            LibName("OPENSSL", GetThirdpartyDir() + "openssl/lib/libssl.lib")
+            LibName("OPENSSL", GetThirdpartyDir() + "openssl/lib/libcrypto.lib")
+            LibName("OPENSSL", "crypt32.lib")
+            LibName("OPENSSL", "ws2_32.lib")
     if (PkgSkip("PNG")==0):
         if os.path.isfile(GetThirdpartyDir() + "png/lib/libpng16_static.lib"):
             LibName("PNG", GetThirdpartyDir() + "png/lib/libpng16_static.lib")
@@ -651,23 +658,39 @@ if (COMPILER == "MSVC"):
         else:
             LibName("TIFF", GetThirdpartyDir() + "tiff/lib/tiff.lib")
     if (PkgSkip("OPENEXR")==0):
-        suffix = ""
-        if os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf-2_2.lib"):
-            suffix = "-2_2"
-        elif os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf-2_3.lib"):
-            suffix = "-2_3"
-        elif os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf-2_4.lib"):
-            suffix = "-2_4"
-            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Imath" + suffix + ".lib")
-        if os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf" + suffix + "_s.lib"):
-            suffix += "_s"  # _s suffix observed for OpenEXR 2.3 only so far
-        LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmImf" + suffix + ".lib")
-        LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmThread" + suffix + ".lib")
-        LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Iex" + suffix + ".lib")
-        if suffix == "-2_2":
-            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Half.lib")
+        if os.path.isfile(GetThirdpartyDir() + "openexr/lib/OpenEXRCore-3_1.lib"):
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/OpenEXR-3_1.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmThread-3_1.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Imath-3_1.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Iex-3_1.lib")
+        elif os.path.isfile(GetThirdpartyDir() + "openexr/lib/OpenEXR-3_0.lib"):
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/OpenEXR-3_0.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmThread-3_0.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Imath-3_0.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Iex-3_0.lib")
+        elif os.path.isfile(GetThirdpartyDir() + "openexr/lib/OpenEXR.lib"):
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/OpenEXR.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmThread.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Imath.lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Iex.lib")
         else:
-            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Half" + suffix + ".lib")
+            suffix = ""
+            if os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf-2_2.lib"):
+                suffix = "-2_2"
+            elif os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf-2_3.lib"):
+                suffix = "-2_3"
+            elif os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf-2_4.lib"):
+                suffix = "-2_4"
+                LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Imath" + suffix + ".lib")
+            if os.path.isfile(GetThirdpartyDir() + "openexr/lib/IlmImf" + suffix + "_s.lib"):
+                suffix += "_s"  # _s suffix observed for OpenEXR 2.3 only so far
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmImf" + suffix + ".lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/IlmThread" + suffix + ".lib")
+            LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Iex" + suffix + ".lib")
+            if suffix == "-2_2":
+                LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Half.lib")
+            else:
+                LibName("OPENEXR", GetThirdpartyDir() + "openexr/lib/Half" + suffix + ".lib")
         IncDirectory("OPENEXR", GetThirdpartyDir() + "openexr/include/OpenEXR")
         IncDirectory("OPENEXR", GetThirdpartyDir() + "openexr/include/Imath")
     if (PkgSkip("JPEG")==0):     LibName("JPEG",     GetThirdpartyDir() + "jpeg/lib/jpeg-static.lib")
@@ -841,6 +864,7 @@ if (COMPILER=="GCC"):
     SmartPkgEnable("VRPN",      "",          ("vrpn", "quat"), ("vrpn", "quat.h", "vrpn/vrpn_Types.h"))
     SmartPkgEnable("OPUS",      "opusfile",  ("opusfile", "opus", "ogg"), ("ogg/ogg.h", "opus/opusfile.h", "opus"))
     SmartPkgEnable("JPEG",      "",          ("jpeg"), "jpeglib.h")
+    SmartPkgEnable("MIMALLOC",  "",          ("mimalloc"), "mimalloc.h")
 
     if GetTarget() != 'emscripten':
         # Most of these are provided by emscripten or via emscripten-ports.
@@ -941,6 +965,9 @@ if (COMPILER=="GCC"):
             LibName("ARTOOLKIT", "-Wl,--exclude-libs,libAR.a")
             LibName("ARTOOLKIT", "-Wl,--exclude-libs,libARMulti.a")
 
+        if not PkgSkip("MIMALLOC"):
+            LibName("MIMALLOC", "-Wl,--exclude-libs,libmimalloc.a")
+
     if PkgSkip("FFMPEG") or GetTarget() == "darwin":
         cv_lib = ChooseLib(("opencv_core", "cv"), "OPENCV")
         if cv_lib == "opencv_core":
@@ -980,7 +1007,7 @@ if (COMPILER=="GCC"):
             LibName("PYTHON", "-lrt")
 
     SmartPkgEnable("OPENSSL",   "openssl",   ("ssl", "crypto"), ("openssl/ssl.h", "openssl/crypto.h"))
-    SmartPkgEnable("GTK2",      "gtk+-2.0")
+    SmartPkgEnable("GTK3",      "gtk+-3.0")
     if GetTarget() != 'emscripten':
        SmartPkgEnable("ZLIB",      "zlib",      ("z"), "zlib.h")
 
@@ -1000,11 +1027,6 @@ if (COMPILER=="GCC"):
     if GetHost() != "darwin":
         # Workaround for an issue where pkg-config does not include this path
         if GetTargetArch() in ("x86_64", "amd64"):
-            if (os.path.isdir("/usr/lib64/glib-2.0/include")):
-                IncDirectory("GTK2", "/usr/lib64/glib-2.0/include")
-            if (os.path.isdir("/usr/lib64/gtk-2.0/include")):
-                IncDirectory("GTK2", "/usr/lib64/gtk-2.0/include")
-
             if not PkgSkip("X11"):
                 if (os.path.isdir("/usr/X11R6/lib64")):
                     LibDirectory("ALWAYS", "/usr/X11R6/lib64")
@@ -2354,6 +2376,7 @@ DTOOL_CONFIG=[
     ("REPORT_OPENSSL_ERRORS",          '1',                      '1'),
     ("USE_PANDAFILESTREAM",            '1',                      '1'),
     ("USE_DELETED_CHAIN",              '1',                      '1'),
+    ("HAVE_MIMALLOC",                  'UNDEF',                  'UNDEF'),
     ("HAVE_WGL",                       '1',                      'UNDEF'),
     ("HAVE_DX9",                       'UNDEF',                  'UNDEF'),
     ("HAVE_THREADS",                   '1',                      '1'),
@@ -2499,6 +2522,20 @@ def WriteConfigSettings():
 
     dtool_config["HAVE_NET"] = '1'
 
+    if GetTarget() == 'windows':
+        if not PkgSkip("MIMALLOC"):
+            # This is faster than both DeletedBufferChain and malloc,
+            # especially in the multi-threaded case.
+            dtool_config["USE_MEMORY_MIMALLOC"] = '1'
+            dtool_config["USE_DELETED_CHAIN"] = 'UNDEF'
+        else:
+            # If we don't have mimalloc, use DeletedBufferChain as fallback,
+            # which is still more efficient than malloc.
+            dtool_config["USE_DELETED_CHAIN"] = '1'
+    else:
+        # On other systems, the default malloc seems to be fine.
+        dtool_config["USE_DELETED_CHAIN"] = 'UNDEF'
+
     if (PkgSkip("NVIDIACG")==0):
         dtool_config["HAVE_CG"] = '1'
         dtool_config["HAVE_CGGL"] = '1'
@@ -3426,7 +3463,7 @@ if GetTarget() == 'windows':
 # DIRECTORY: dtool/src/dtoolbase/
 #
 
-OPTS=['DIR:dtool/src/dtoolbase', 'BUILDING:DTOOL']
+OPTS=['DIR:dtool/src/dtoolbase', 'BUILDING:DTOOL', 'MIMALLOC']
 TargetAdd('p3dtoolbase_composite1.obj', opts=OPTS, input='p3dtoolbase_composite1.cxx')
 TargetAdd('p3dtoolbase_composite2.obj', opts=OPTS, input='p3dtoolbase_composite2.cxx')
 TargetAdd('p3dtoolbase_lookup3.obj',    opts=OPTS, input='lookup3.c')
@@ -3457,7 +3494,7 @@ TargetAdd('libp3dtool.dll', input='p3dtoolbase_composite1.obj')
 TargetAdd('libp3dtool.dll', input='p3dtoolbase_composite2.obj')
 TargetAdd('libp3dtool.dll', input='p3dtoolbase_indent.obj')
 TargetAdd('libp3dtool.dll', input='p3dtoolbase_lookup3.obj')
-TargetAdd('libp3dtool.dll', opts=['ADVAPI','WINSHELL','WINKERNEL'])
+TargetAdd('libp3dtool.dll', opts=['ADVAPI','WINSHELL','WINKERNEL','MIMALLOC'])
 
 #
 # DIRECTORY: dtool/src/cppparser/
@@ -5952,19 +5989,19 @@ if not PkgSkip("PANDATOOL"):
 # DIRECTORY: pandatool/src/gtk-stats/
 #
 
-if not PkgSkip("PANDATOOL") and (GetTarget() == 'windows' or not PkgSkip("GTK2")):
+if not PkgSkip("PANDATOOL") and (GetTarget() == 'windows' or not PkgSkip("GTK3")):
     if GetTarget() == 'windows':
         OPTS=['DIR:pandatool/src/win-stats']
         TargetAdd('pstats_composite1.obj', opts=OPTS, input='winstats_composite1.cxx')
     else:
-        OPTS=['DIR:pandatool/src/gtk-stats', 'GTK2']
+        OPTS=['DIR:pandatool/src/gtk-stats', 'GTK3']
         TargetAdd('pstats_composite1.obj', opts=OPTS, input='gtkstats_composite1.cxx')
     TargetAdd('pstats.exe', input='pstats_composite1.obj')
     TargetAdd('pstats.exe', input='libp3pstatserver.lib')
     TargetAdd('pstats.exe', input='libp3progbase.lib')
     TargetAdd('pstats.exe', input='libp3pandatoolbase.lib')
     TargetAdd('pstats.exe', input=COMMON_PANDA_LIBS)
-    TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK2'])
+    TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINCOMCTL', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK3'])
 
 #
 # DIRECTORY: pandatool/src/xfileprogs/

+ 5 - 2
makepanda/makepandacore.py

@@ -651,6 +651,9 @@ def oscmd(cmd, ignoreError = False, cwd=None):
 
         res = os.spawnl(os.P_WAIT, exe_path, cmd)
 
+        if res == -1073741510: # 0xc000013a
+            exit("keyboard interrupt")
+
         if cwd is not None:
             os.chdir(pwd)
     else:
@@ -733,7 +736,7 @@ def GetTimestamp(path):
     if path in TIMESTAMPCACHE:
         return TIMESTAMPCACHE[path]
     try:
-        date = os.path.getmtime(path)
+        date = int(os.path.getmtime(path))
     except:
         date = 0
     TIMESTAMPCACHE[path] = date
@@ -887,7 +890,7 @@ def JavaGetImports(path):
 ##
 ########################################################################
 
-DCACHE_VERSION = 2
+DCACHE_VERSION = 3
 DCACHE_BACKED_UP = False
 
 def SaveDependencyCache():

+ 6 - 3
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -1929,11 +1929,14 @@ handle_wheel_event(double x, double y) {
     _input->button_up(MouseButton::wheel_down());
   }
 
-  // TODO: check if this is correct, I don't own a MacBook
-  if (x > 0.0) {
+  if (x != 0 && cocoa_invert_wheel_x) {
+    x = -x;
+  }
+
+  if (x < 0.0) {
     _input->button_down(MouseButton::wheel_right());
     _input->button_up(MouseButton::wheel_right());
-  } else if (x < 0.0) {
+  } else if (x > 0.0) {
     _input->button_down(MouseButton::wheel_left());
     _input->button_up(MouseButton::wheel_left());
   }

+ 2 - 0
panda/src/cocoadisplay/config_cocoadisplay.h

@@ -20,6 +20,8 @@
 
 NotifyCategoryDecl(cocoadisplay, EXPCL_PANDA_COCOADISPLAY, EXPTP_PANDA_COCOADISPLAY);
 
+extern ConfigVariableBool cocoa_invert_wheel_x;
+
 extern EXPCL_PANDA_COCOADISPLAY void init_libcocoadisplay();
 
 #endif

+ 5 - 0
panda/src/cocoadisplay/config_cocoadisplay.mm

@@ -31,6 +31,11 @@ ConfigureFn(config_cocoadisplay) {
   init_libcocoadisplay();
 }
 
+ConfigVariableBool cocoa_invert_wheel_x
+("cocoa-invert-wheel-x", false,
+ PRC_DESC("Set this to true to swap the wheel_left and wheel_right mouse "
+          "button events, to restore to the pre-1.10.12 behavior."));
+
 /**
  * Initializes the library.  This must be called at least once before any of
  * the functions or classes in this library can be used.  Normally it will be

+ 23 - 4
panda/src/device/ioKitInputDeviceManager.cxx

@@ -16,6 +16,18 @@
 
 #if defined(__APPLE__) && !defined(CPPPARSER)
 
+static ConfigVariableBool iokit_scan_mouse_devices
+("iokit-scan-mouse-devices", false,
+ PRC_DESC("Set this to true to enable capturing raw mouse data via IOKit on "
+          "macOS.  This is disabled by default because newer macOS versions "
+          "will prompt the user explicitly for permissions when this is on."));
+
+static ConfigVariableBool iokit_scan_keyboard_devices
+("iokit-scan-keyboard-devices", false,
+ PRC_DESC("Set this to true to enable capturing raw keyboard data via IOKit on "
+          "macOS.  This is disabled by default because newer macOS versions "
+          "will prompt the user explicitly for permissions when this is on."));
+
 /**
  * Initializes the input device manager by scanning which devices are currently
  * connected and setting up any platform-dependent structures necessary for
@@ -34,15 +46,22 @@ IOKitInputDeviceManager() {
   int page = kHIDPage_GenericDesktop;
   int usages[] = {kHIDUsage_GD_GamePad,
                   kHIDUsage_GD_Joystick,
-                  kHIDUsage_GD_Mouse,
-                  kHIDUsage_GD_Keyboard,
-                  kHIDUsage_GD_MultiAxisController, 0};
-  int *usage = usages;
+                  kHIDUsage_GD_MultiAxisController,
+                  0, 0, 0};
+
+  int num_usages = 3;
+  if (iokit_scan_mouse_devices) {
+    usages[num_usages++] = kHIDUsage_GD_Mouse;
+  }
+  if (iokit_scan_keyboard_devices) {
+    usages[num_usages++] = kHIDUsage_GD_Keyboard;
+  }
 
   // This giant mess is necessary to create an array of match dictionaries
   // that will match the devices we're interested in.
   CFMutableArrayRef match = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
   nassertv(match);
+  int *usage = usages;
   while (*usage) {
     CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
     CFNumberRef page_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);

+ 1 - 0
panda/src/event/asyncFuture.cxx

@@ -39,6 +39,7 @@ AsyncFuture::
   if (result_ref != nullptr) {
     _result_ref.cheat() = nullptr;
     if (!result_ref->unref()) {
+      patomic_thread_fence(std::memory_order_acquire);
       delete _result;
     }
     _result = nullptr;

+ 49 - 32
panda/src/express/referenceCount.I

@@ -27,9 +27,9 @@ TypeHandle RefCountObj<Base>::_type_handle;
  * inheritance.
  */
 INLINE ReferenceCount::
-ReferenceCount() {
-  _weak_list = nullptr;
-  _ref_count = 0;
+ReferenceCount() :
+  _weak_list(nullptr),
+  _ref_count(0) {
 #ifdef DO_MEMORY_USAGE
   MemoryUsage::record_pointer(this);
 #endif
@@ -44,9 +44,9 @@ ReferenceCount() {
  * try.
  */
 INLINE ReferenceCount::
-ReferenceCount(const ReferenceCount &) {
-  _weak_list = nullptr;
-  _ref_count = 0;
+ReferenceCount(const ReferenceCount &) :
+  _weak_list(nullptr),
+  _ref_count(0) {
 #ifdef DO_MEMORY_USAGE
   MemoryUsage::record_pointer(this);
 #endif
@@ -69,7 +69,7 @@ operator = (const ReferenceCount &) {
   // to create an automatic (local variable) instance of a class that derives
   // from ReferenceCount.  Or maybe your headers are out of sync, and you need
   // to make clean in direct or some higher tree.
-  nassertv(_ref_count != deleted_ref_count);
+  nassertv(_ref_count.load(std::memory_order_relaxed) != deleted_ref_count);
 }
 
 /**
@@ -78,23 +78,29 @@ operator = (const ReferenceCount &) {
 ReferenceCount::
 ~ReferenceCount() {
   TAU_PROFILE("ReferenceCount::~ReferenceCount()", " ", TAU_USER);
+
+  // We can safely use relaxed ordering for everything in this destructor,
+  // since (1) we already issued an acquire barrier before invoking delete,
+  // and (2) we are the only thread accessing this object at this point.
+  int ref_count = _ref_count.load(std::memory_order_relaxed);
+
   // If this assertion fails, we're trying to delete an object that was just
   // deleted.  Possibly you used a real pointer instead of a PointerTo at some
   // point, and the object was deleted when the PointerTo went out of scope.
   // Maybe you tried to create an automatic (local variable) instance of a
   // class that derives from ReferenceCount.  Or maybe your headers are out of
   // sync, and you need to make clean in direct or some higher tree.
-  nassertv(_ref_count != deleted_ref_count);
+  nassertv(ref_count != deleted_ref_count);
 
   // If this assertion fails, we're trying to delete a static object that
   // still has an outstanding reference count.  You should make sure that all
   // references to your static objects are gone by the time the object itself
   // destructs.
-  nassertv(_ref_count <= local_ref_count);
+  nassertv(ref_count <= local_ref_count);
 
   // If this assertion fails, the reference counts are all screwed up
   // altogether.  Maybe some errant code stomped all over memory somewhere.
-  nassertv(_ref_count >= 0);
+  nassertv(ref_count >= 0);
 
   // If this assertion fails, someone tried to delete this object while its
   // reference count was still positive.  Maybe you tried to point a PointerTo
@@ -105,19 +111,20 @@ ReferenceCount::
   // Another possibility is you inadvertently omitted a copy constructor for a
   // ReferenceCount object, and then bitwise copied a dynamically allocated
   // value--reference count and all--onto a locally allocated one.
-  nassertv(_ref_count == 0 || _ref_count == local_ref_count);
+  nassertv(ref_count == 0 || ref_count == local_ref_count);
 
   // Tell our weak reference holders that we're going away now.
-  if (_weak_list != nullptr) {
+  WeakReferenceList *weak_list = _weak_list.load(std::memory_order_relaxed);
+  if (weak_list != nullptr) {
     ((WeakReferenceList *)_weak_list)->mark_deleted();
-    _weak_list = nullptr;
+    _weak_list.store(nullptr, std::memory_order_release);
   }
 
 #ifndef NDEBUG
   // Ok, all clear to delete.  Now set the reference count to
   // deleted_ref_count, so we'll have a better chance of noticing if we happen
   // to have a stray pointer to it still out there.
-  _ref_count = deleted_ref_count;
+  _ref_count.store(deleted_ref_count, std::memory_order_relaxed);
 #endif
 
 #ifdef DO_MEMORY_USAGE
@@ -133,7 +140,7 @@ get_ref_count() const {
 #ifdef _DEBUG
   test_ref_count_integrity();
 #endif
-  return (int)AtomicAdjust::get(_ref_count);
+  return _ref_count.load(std::memory_order_acquire);
 }
 
 /**
@@ -154,7 +161,7 @@ ref() const {
   nassertv(test_ref_count_integrity());
 #endif
 
-  AtomicAdjust::inc(_ref_count);
+  _ref_count.fetch_add(1, std::memory_order_relaxed);
 }
 
 /**
@@ -184,9 +191,9 @@ unref() const {
   // If this assertion fails, you tried to unref an object with a zero
   // reference count.  Are you using ref() and unref() directly?  Are you sure
   // you can't use PointerTo's?
-  nassertr(_ref_count > 0, 0);
+  nassertr(_ref_count.load(std::memory_order_relaxed) > 0, 0);
 #endif
-  return AtomicAdjust::dec(_ref_count);
+  return _ref_count.fetch_sub(1, std::memory_order_release) != 1;
 }
 
 /**
@@ -227,11 +234,11 @@ test_ref_count_nonzero() const {
  */
 INLINE void ReferenceCount::
 local_object() {
+  int prev_count = _ref_count.exchange(local_ref_count, std::memory_order_relaxed);
+
   // If this assertion fails, you didn't call this immediately after creating
   // a local object.
-  nassertv(_ref_count == 0);
-
-  _ref_count = local_ref_count;
+  nassertv(prev_count == 0);
 }
 
 /**
@@ -242,7 +249,7 @@ local_object() {
  */
 INLINE bool ReferenceCount::
 has_weak_list() const {
-  return _weak_list != nullptr;
+  return _weak_list.load(std::memory_order_relaxed) != nullptr;
 }
 
 /**
@@ -255,10 +262,10 @@ has_weak_list() const {
  */
 INLINE WeakReferenceList *ReferenceCount::
 get_weak_list() const {
-  if (AtomicAdjust::get_ptr(_weak_list) == nullptr) {
+  if (_weak_list.load(std::memory_order_relaxed) == nullptr) {
     ((ReferenceCount *)this)->create_weak_list();
   }
-  return (WeakReferenceList *)AtomicAdjust::get_ptr(_weak_list);
+  return _weak_list.load(std::memory_order_consume);
 }
 
 /**
@@ -273,7 +280,7 @@ weak_ref() {
 #ifdef _DEBUG
   nassertr(test_ref_count_integrity(), nullptr);
 #else
-  nassertr(_ref_count != deleted_ref_count, nullptr);
+  nassertr(_ref_count.load(std::memory_order_relaxed) != deleted_ref_count, nullptr);
 #endif
   WeakReferenceList *weak_ref = get_weak_list();
   weak_ref->ref();
@@ -290,7 +297,7 @@ weak_unref() {
 #ifdef _DEBUG
   nassertv(test_ref_count_integrity());
 #endif
-  WeakReferenceList *weak_list = (WeakReferenceList *)_weak_list;
+  WeakReferenceList *weak_list = _weak_list.load(std::memory_order_consume);
   nassertv(weak_list != nullptr);
   bool nonzero = weak_list->unref();
   nassertv(nonzero);
@@ -307,13 +314,16 @@ ref_if_nonzero() const {
 #ifdef _DEBUG
   test_ref_count_integrity();
 #endif
-  AtomicAdjust::Integer ref_count;
+  int ref_count = _ref_count.load(std::memory_order_relaxed);
   do {
-    ref_count = AtomicAdjust::get(_ref_count);
     if (ref_count <= 0) {
       return false;
     }
-  } while (ref_count != AtomicAdjust::compare_and_exchange(_ref_count, ref_count, ref_count + 1));
+  }
+  while (!_ref_count.compare_exchange_weak(ref_count, ref_count + 1,
+                                           std::memory_order_seq_cst,
+                                           std::memory_order_relaxed));
+
   return true;
 }
 
@@ -321,15 +331,21 @@ ref_if_nonzero() const {
  * Atomically decreases the reference count of this object if it is one.
  * Do not use this.  This exists only to implement a special case with the
  * state cache.
- * @return false if the reference count was decremented to zero.
+ * @return false on success, ie. if the reference count was decremented to 0.
  */
 INLINE bool ReferenceCount::
 unref_if_one() const {
 #ifdef _DEBUG
   nassertr(test_ref_count_integrity(), 0);
-  nassertr(_ref_count > 0, 0);
+  nassertr(_ref_count.load(std::memory_order_relaxed) > 0, 0);
 #endif
-  return (AtomicAdjust::compare_and_exchange(_ref_count, 1, 0) != 1);
+
+  // Presumably if the ref count becomes 0, someone is about to delete the
+  // object or something like that, hence the acquire order on success.
+  int expected = 1;
+  return !_ref_count.compare_exchange_strong(expected, 0,
+                                             std::memory_order_acquire,
+                                             std::memory_order_relaxed);
 }
 
 /**
@@ -350,6 +366,7 @@ unref_delete(RefCountType *ptr) {
 
   if (!ptr->unref()) {
     // If the reference count has gone to zero, delete the object.
+    patomic_thread_fence(std::memory_order_acquire);
     delete ptr;
   }
 }

+ 11 - 8
panda/src/express/referenceCount.cxx

@@ -23,17 +23,19 @@ TypeHandle ReferenceCount::_type_handle;
  */
 bool ReferenceCount::
 do_test_ref_count_integrity() const {
+  int ref_count = _ref_count.load(std::memory_order_relaxed);
+
   // If this assertion fails, we're trying to delete an object that was just
   // deleted.  Possibly you used a real pointer instead of a PointerTo at some
   // point, and the object was deleted when the PointerTo went out of scope.
   // Maybe you tried to create an automatic (local variable) instance of a
   // class that derives from ReferenceCount.  Or maybe your headers are out of
   // sync, and you need to make clean in direct or some higher tree.
-  nassertr(_ref_count != deleted_ref_count, false);
+  nassertr(ref_count != deleted_ref_count, false);
 
   // If this assertion fails, the reference counts are all screwed up
   // altogether.  Maybe some errant code stomped all over memory somewhere.
-  nassertr(_ref_count >= 0, false);
+  nassertr(ref_count >= 0, false);
 
   return true;
 }
@@ -44,7 +46,7 @@ do_test_ref_count_integrity() const {
 bool ReferenceCount::
 do_test_ref_count_nonzero() const {
   nassertr(do_test_ref_count_integrity(), false);
-  nassertr(_ref_count > 0, false);
+  nassertr(_ref_count.load(std::memory_order_relaxed) > 0, false);
 
   return true;
 }
@@ -54,11 +56,12 @@ do_test_ref_count_nonzero() const {
  */
 void ReferenceCount::
 create_weak_list() {
-  WeakReferenceList *weak_list = new WeakReferenceList;
-  void *orig =
-    AtomicAdjust::compare_and_exchange_ptr(_weak_list, nullptr, weak_list);
-  if (orig != nullptr) {
+  WeakReferenceList *new_list = new WeakReferenceList;
+  WeakReferenceList *old_list = nullptr;
+  if (!_weak_list.compare_exchange_strong(old_list, new_list,
+                                          std::memory_order_release,
+                                          std::memory_order_relaxed)) {
     // Someone else created it first.
-    delete weak_list;
+    delete new_list;
   }
 }

+ 3 - 2
panda/src/express/referenceCount.h

@@ -23,6 +23,7 @@
 #include "atomicAdjust.h"
 #include "numeric_types.h"
 #include "deletedChain.h"
+#include "patomic.h"
 
 #include <stdlib.h>
 
@@ -89,8 +90,8 @@ private:
     deleted_ref_count = -100,
   };
 
-  mutable AtomicAdjust::Integer _ref_count;
-  AtomicAdjust::Pointer _weak_list;  // WeakReferenceList *
+  mutable patomic<int> _ref_count;
+  patomic<WeakReferenceList *> _weak_list;
 
 public:
   static TypeHandle get_class_type() {

+ 7 - 0
panda/src/express/weakPointerToBase.I

@@ -117,6 +117,7 @@ INLINE WeakPointerToBase<T>::
 ~WeakPointerToBase() {
   WeakReferenceList *old_ref = (WeakReferenceList *)_weak_ref;
   if (old_ref != nullptr && !old_ref->unref()) {
+    patomic_thread_fence(std::memory_order_acquire);
     delete old_ref;
   }
 }
@@ -143,6 +144,7 @@ reassign(To *ptr) {
 
     // Now remove the old reference.
     if (old_ref != nullptr && !old_ref->unref()) {
+      patomic_thread_fence(std::memory_order_acquire);
       delete old_ref;
     }
   }
@@ -183,6 +185,7 @@ reassign(const WeakPointerToBase<To> &copy) {
 
     // Now remove the old reference.
     if (old_ref != nullptr && !old_ref->unref()) {
+      patomic_thread_fence(std::memory_order_acquire);
       delete old_ref;
     }
   }
@@ -205,6 +208,7 @@ reassign(WeakPointerToBase<To> &&from) noexcept {
 
     // Now delete the old pointer.
     if (old_ref != nullptr && !old_ref->unref()) {
+      patomic_thread_fence(std::memory_order_acquire);
       delete old_ref;
     }
   }
@@ -233,6 +237,7 @@ reassign(const WeakPointerToBase<Y> &copy) {
 
     // Now remove the old reference.
     if (old_ref != nullptr && !old_ref->unref()) {
+      patomic_thread_fence(std::memory_order_acquire);
       delete old_ref;
     }
   }
@@ -260,6 +265,7 @@ reassign(WeakPointerToBase<Y> &&from) noexcept {
 
     // Now delete the old pointer.
     if (old_ref != nullptr && !old_ref->unref()) {
+      patomic_thread_fence(std::memory_order_acquire);
       delete old_ref;
     }
   }
@@ -627,6 +633,7 @@ clear() {
 
   // Now remove the old reference.
   if (old_ref != nullptr && !old_ref->unref()) {
+    patomic_thread_fence(std::memory_order_acquire);
     delete old_ref;
   }
 }

+ 3 - 3
panda/src/express/weakReferenceList.I

@@ -18,7 +18,7 @@
  */
 INLINE void WeakReferenceList::
 ref() const {
-  AtomicAdjust::inc(_count);
+  _count.fetch_add(1, std::memory_order_relaxed);
 }
 
 /**
@@ -30,7 +30,7 @@ ref() const {
  */
 INLINE bool WeakReferenceList::
 unref() const {
-  return AtomicAdjust::dec(_count);
+  return _count.fetch_sub(1, std::memory_order_release) != 1;
 }
 
 /**
@@ -41,5 +41,5 @@ unref() const {
  */
 INLINE bool WeakReferenceList::
 was_deleted() const {
-  return AtomicAdjust::get(_count) < _alive_offset;
+  return _count.load(std::memory_order_relaxed) < _alive_offset;
 }

+ 2 - 2
panda/src/express/weakReferenceList.cxx

@@ -27,7 +27,7 @@ WeakReferenceList() : _count(_alive_offset) {
  */
 WeakReferenceList::
 ~WeakReferenceList() {
-  nassertv(_count == 0);
+  nassertv(_count.load(std::memory_order_relaxed) == 0);
 }
 
 /**
@@ -91,7 +91,7 @@ mark_deleted() {
 
   // Decrement the special offset added to the weak pointer count to indicate
   // that it can be deleted when all the weak references have gone.
-  AtomicAdjust::Integer result = AtomicAdjust::add(_count, -_alive_offset);
+  int result = _count.fetch_sub(_alive_offset, std::memory_order_relaxed) - _alive_offset;
   _lock.unlock();
   if (result == 0) {
     // There are no weak references remaining either, so delete this.

+ 3 - 2
panda/src/express/weakReferenceList.h

@@ -17,6 +17,7 @@
 #include "pandabase.h"
 #include "pmap.h"
 #include "mutexImpl.h"
+#include "patomic.h"
 
 class WeakPointerCallback;
 
@@ -53,8 +54,8 @@ private:
   // This has a very large number added to it if the object is still alive.
   // It could be 1, but having it be a large number makes it easy to check
   // whether the object has been deleted or not.
-  static const AtomicAdjust::Integer _alive_offset = (1 << 30);
-  mutable AtomicAdjust::Integer _count;
+  static const int _alive_offset = (1 << 30);
+  mutable patomic<int> _count;
 
   friend class ReferenceCount;
 };

+ 14 - 0
panda/src/mathutil/plane_src.cxx

@@ -158,3 +158,17 @@ void FLOATNAME(LPlane)::
 write(std::ostream &out, int indent_level) const {
   indent(out, indent_level) << *this << "\n";
 }
+
+/**
+ * Returns a string representation of this LPlane.
+ */
+std::string FLOATNAME(LPlane)::
+__repr__() const {
+  std::ostringstream out;
+  out << "LPlane" << FLOATTOKEN << "("
+      << MAYBE_ZERO(_v(0)) << ", "
+      << MAYBE_ZERO(_v(1)) << ", "
+      << MAYBE_ZERO(_v(2)) << ", "
+      << MAYBE_ZERO(_v(3)) << ")";
+  return out.str();
+}

+ 1 - 0
panda/src/mathutil/plane_src.h

@@ -61,6 +61,7 @@ PUBLISHED:
 
   void output(std::ostream &out) const;
   void write(std::ostream &out, int indent_level = 0) const;
+  std::string __repr__() const;
 };
 
 INLINE_MATHUTIL std::ostream &

+ 1 - 1
panda/src/pgraph/cacheStats.I

@@ -90,6 +90,6 @@ add_total_size(int count) {
 INLINE void CacheStats::
 add_num_states(int count) {
 #ifndef NDEBUG
-  _num_states += count;
+  _num_states.fetch_add(count, std::memory_order_relaxed);
 #endif  // NDEBUG
 }

+ 3 - 2
panda/src/pgraph/cacheStats.cxx

@@ -51,12 +51,13 @@ reset(double now) {
 void CacheStats::
 write(std::ostream &out, const char *name) const {
 #ifndef NDEBUG
+  int num_states = _num_states.load(std::memory_order_relaxed);
   out << name << " cache: " << _cache_hits << " hits, "
       << _cache_misses << " misses\n"
       << _cache_adds + _cache_new_adds << "(" << _cache_new_adds << ") adds(new), "
       << _cache_dels << " dels, "
-      << _total_cache_size << " / " << _num_states << " = "
-      << (double)_total_cache_size / (double)_num_states
+      << _total_cache_size << " / " << num_states << " = "
+      << (double)_total_cache_size / (double)num_states
       << " average cache size\n";
 #endif  // NDEBUG
 }

+ 2 - 1
panda/src/pgraph/cacheStats.h

@@ -16,6 +16,7 @@
 
 #include "pandabase.h"
 #include "clockObject.h"
+#include "patomic.h"
 #include "pnotify.h"
 
 /**
@@ -45,7 +46,7 @@ private:
   int _cache_new_adds = 0;
   int _cache_dels = 0;
   int _total_cache_size = 0;
-  int _num_states = 0;
+  patomic<int> _num_states {0};
   double _last_reset = 0.0;
 
   bool _cache_report = false;

+ 0 - 2
panda/src/pgraph/renderState.cxx

@@ -117,8 +117,6 @@ RenderState::
   nassertv(!is_destructing());
   set_destructing();
 
-  LightReMutexHolder holder(*_states_lock);
-
   // unref() should have cleared these.
   nassertv(_saved_entry == -1);
   nassertv(_composition_cache.is_empty() && _invert_composition_cache.is_empty());

+ 2 - 5
panda/src/pgui/pgScrollFrame.h

@@ -19,10 +19,7 @@
 #include "pgVirtualFrame.h"
 #include "pgSliderBarNotify.h"
 #include "pgSliderBar.h"
-
-#ifdef PHAVE_ATOMIC
-#include <atomic>
-#endif
+#include "patomic.h"
 
 /**
  * This is a special kind of frame that pretends to be much larger than it
@@ -96,7 +93,7 @@ private:
 private:
   bool _needs_remanage;
   bool _needs_recompute_clip;
-  std::atomic_flag _canvas_computed;
+  patomic_flag _canvas_computed;
 
   bool _has_virtual_frame;
   LVecBase4 _virtual_frame;

+ 2 - 5
panda/src/physics/physicalNode.cxx

@@ -13,13 +13,10 @@
 
 #include "physicalNode.h"
 #include "physicsManager.h"
-
-#ifdef PHAVE_ATOMIC
-#include <atomic>
-#endif
+#include "patomic.h"
 
 // static stuff.
-static std::atomic_flag warned_copy_physical_node = ATOMIC_FLAG_INIT;
+static patomic_flag warned_copy_physical_node = ATOMIC_FLAG_INIT;
 
 TypeHandle PhysicalNode::_type_handle;
 

+ 0 - 16
panda/src/pipeline/conditionVarDummyImpl.I

@@ -25,22 +25,6 @@ INLINE ConditionVarDummyImpl::
 ~ConditionVarDummyImpl() {
 }
 
-/**
- *
- */
-INLINE void ConditionVarDummyImpl::
-wait() {
-  Thread::force_yield();
-}
-
-/**
- *
- */
-INLINE void ConditionVarDummyImpl::
-wait(double) {
-  Thread::force_yield();
-}
-
 /**
  *
  */

+ 17 - 0
panda/src/pipeline/conditionVarDummyImpl.cxx

@@ -13,3 +13,20 @@
 
 #include "selectThreadImpl.h"
 #include "conditionVarDummyImpl.h"
+#include "thread.h"
+
+/**
+ *
+ */
+void ConditionVarDummyImpl::
+wait() {
+  Thread::force_yield();
+}
+
+/**
+ *
+ */
+void ConditionVarDummyImpl::
+wait(double) {
+  Thread::force_yield();
+}

+ 2 - 3
panda/src/pipeline/conditionVarDummyImpl.h

@@ -16,7 +16,6 @@
 
 #include "pandabase.h"
 #include "selectThreadImpl.h"
-#include "thread.h"
 
 #include "pnotify.h"
 
@@ -31,8 +30,8 @@ public:
   INLINE ConditionVarDummyImpl(MutexDummyImpl &mutex);
   INLINE ~ConditionVarDummyImpl();
 
-  INLINE void wait();
-  INLINE void wait(double timeout);
+  void wait();
+  void wait(double timeout);
   INLINE void notify();
   INLINE void notify_all();
 };

+ 2 - 1
panda/src/pipeline/thread.h

@@ -42,7 +42,8 @@ class AsyncTask;
  * object will automatically be destructed if no other pointers are
  * referencing it.
  */
-class EXPCL_PANDA_PIPELINE Thread : public TypedReferenceCount, public Namable {
+// Due to a GCC bug, we can't use alignas() together with an attribute.
+class ALIGN_64BYTE EXPCL_PANDA_PIPELINE Thread : public TypedReferenceCount, public Namable {
 protected:
   Thread(const std::string &name, const std::string &sync_name);
   Thread(const Thread &copy) = delete;

+ 2 - 20
panda/src/pipeline/threadPosixImpl.I

@@ -46,26 +46,8 @@ prepare_for_exit() {
 INLINE Thread *ThreadPosixImpl::
 get_current_thread() {
   TAU_PROFILE("Thread *ThreadPosixImpl::get_current_thread()", " ", TAU_USER);
-  if (!_got_pt_ptr_index) {
-    init_pt_ptr_index();
-  }
-  return (Thread *)pthread_getspecific(_pt_ptr_index);
-}
-
-/**
- * Associates the indicated Thread object with the currently-executing thread.
- * You should not call this directly; use Thread::bind_thread() instead.
- */
-INLINE void ThreadPosixImpl::
-bind_thread(Thread *thread) {
-  if (!_got_pt_ptr_index) {
-    init_pt_ptr_index();
-  }
-  int result = pthread_setspecific(_pt_ptr_index, thread);
-  nassertv(result == 0);
-#ifdef ANDROID
-  bind_java_thread();
-#endif
+  Thread *thread = _current_thread;
+  return (thread != nullptr) ? thread : init_current_thread();
 }
 
 /**

+ 28 - 27
panda/src/pipeline/threadPosixImpl.cxx

@@ -28,8 +28,8 @@
 static JavaVM *java_vm = nullptr;
 #endif
 
-pthread_key_t ThreadPosixImpl::_pt_ptr_index = 0;
-bool ThreadPosixImpl::_got_pt_ptr_index = false;
+__thread Thread *ThreadPosixImpl::_current_thread = nullptr;
+static patomic_flag _main_thread_known = ATOMIC_FLAG_INIT;
 
 /**
  *
@@ -80,10 +80,6 @@ start(ThreadPriority priority, bool joinable) {
   _status = S_start_called;
   _detached = false;
 
-  if (!_got_pt_ptr_index) {
-    init_pt_ptr_index();
-  }
-
   pthread_attr_t attr;
   pthread_attr_init(&attr);
 
@@ -186,6 +182,21 @@ get_unique_id() const {
   return strm.str();
 }
 
+/**
+ * Associates the indicated Thread object with the currently-executing thread.
+ * You should not call this directly; use Thread::bind_thread() instead.
+ */
+void ThreadPosixImpl::
+bind_thread(Thread *thread) {
+  if (_current_thread == nullptr && thread == Thread::get_main_thread()) {
+    _main_thread_known.test_and_set(std::memory_order_relaxed);
+  }
+  _current_thread = thread;
+#ifdef ANDROID
+  bind_java_thread();
+#endif
+}
+
 #ifdef ANDROID
 /**
  * Attaches the thread to the Java virtual machine.  If this returns true, a
@@ -247,8 +258,7 @@ root_func(void *data) {
     // TAU_PROFILE("void ThreadPosixImpl::root_func()", " ", TAU_USER);
 
     ThreadPosixImpl *self = (ThreadPosixImpl *)data;
-    int result = pthread_setspecific(_pt_ptr_index, self->_parent_obj);
-    nassertr(result == 0, nullptr);
+    _current_thread = self->_parent_obj;
 
     {
       self->_mutex.lock();
@@ -302,27 +312,18 @@ root_func(void *data) {
 }
 
 /**
- * Allocate a new index to store the Thread parent pointer as a piece of per-
- * thread private data.
+ * Called by get_current_thread() if the current therad pointer is null; checks
+ * whether it might be the main thread.
  */
-void ThreadPosixImpl::
-init_pt_ptr_index() {
-  nassertv(!_got_pt_ptr_index);
-
-  int result = pthread_key_create(&_pt_ptr_index, nullptr);
-  if (result != 0) {
-    thread_cat->error()
-      << "Unable to associate Thread pointers with threads.\n";
-    return;
+Thread *ThreadPosixImpl::
+init_current_thread() {
+  Thread *thread = _current_thread;
+  if (!_main_thread_known.test_and_set(std::memory_order_relaxed)) {
+    thread = Thread::get_main_thread();
+    _current_thread = thread;
   }
-
-  _got_pt_ptr_index = true;
-
-  // Assume that we must be in the main thread, since this method must be
-  // called before the first thread is spawned.
-  Thread *main_thread_obj = Thread::get_main_thread();
-  result = pthread_setspecific(_pt_ptr_index, main_thread_obj);
-  nassertv(result == 0);
+  nassertr(thread != nullptr, nullptr);
+  return thread;
 }
 
 #ifdef ANDROID

+ 3 - 4
panda/src/pipeline/threadPosixImpl.h

@@ -49,7 +49,7 @@ public:
   INLINE static void prepare_for_exit();
 
   INLINE static Thread *get_current_thread();
-  INLINE static void bind_thread(Thread *thread);
+  static void bind_thread(Thread *thread);
   INLINE static bool is_threading_supported();
   INLINE static bool is_true_threads();
   INLINE static bool is_simple_threads();
@@ -65,7 +65,7 @@ public:
 
 private:
   static void *root_func(void *data);
-  static void init_pt_ptr_index();
+  static Thread *init_current_thread();
 
   // There appears to be a name collision with the word "Status".
   enum PStatus {
@@ -86,8 +86,7 @@ private:
   JNIEnv *_jni_env;
 #endif
 
-  static pthread_key_t _pt_ptr_index;
-  static bool _got_pt_ptr_index;
+  static __thread Thread *_current_thread;
 };
 
 #include "threadPosixImpl.I"

+ 0 - 24
panda/src/pipeline/threadWin32Impl.I

@@ -38,30 +38,6 @@ INLINE void ThreadWin32Impl::
 prepare_for_exit() {
 }
 
-/**
- *
- */
-INLINE Thread *ThreadWin32Impl::
-get_current_thread() {
-  if (!_got_pt_ptr_index) {
-    init_pt_ptr_index();
-  }
-  return (Thread *)TlsGetValue(_pt_ptr_index);
-}
-
-/**
- * Associates the indicated Thread object with the currently-executing thread.
- * You should not call this directly; use Thread::bind_thread() instead.
- */
-INLINE void ThreadWin32Impl::
-bind_thread(Thread *thread) {
-  if (!_got_pt_ptr_index) {
-    init_pt_ptr_index();
-  }
-  BOOL result = TlsSetValue(_pt_ptr_index, thread);
-  nassertv(result);
-}
-
 /**
  *
  */

+ 44 - 32
panda/src/pipeline/threadWin32Impl.cxx

@@ -20,8 +20,28 @@
 #include "pointerTo.h"
 #include "config_pipeline.h"
 
-DWORD ThreadWin32Impl::_pt_ptr_index = 0;
-bool ThreadWin32Impl::_got_pt_ptr_index = false;
+static thread_local Thread *_current_thread = nullptr;
+static patomic_flag _main_thread_known = ATOMIC_FLAG_INIT;
+
+/**
+ * Called by get_current_thread() if the current thread pointer is null; checks
+ * whether it might be the main thread.
+ * Note that adding noinline speeds up this call *significantly*, don't remove!
+ */
+static __declspec(noinline) Thread *
+init_current_thread() {
+  Thread *thread = _current_thread;
+  if (!_main_thread_known.test_and_set(std::memory_order_relaxed)) {
+    // Assume that we must be in the main thread, since this method must be
+    // called before the first thread is spawned.
+    thread = Thread::get_main_thread();
+    _current_thread = thread;
+  }
+  // If this assertion triggers, you are making Panda calls from a thread
+  // that has not first been registered using Thread::bind_thread().
+  nassertr(thread != nullptr, nullptr);
+  return thread;
+}
 
 /**
  *
@@ -62,10 +82,6 @@ start(ThreadPriority priority, bool joinable) {
   _joinable = joinable;
   _status = S_start_called;
 
-  if (!_got_pt_ptr_index) {
-    init_pt_ptr_index();
-  }
-
   // Increment the parent object's reference count first.  The thread will
   // eventually decrement it when it terminates.
   _parent_obj->ref();
@@ -133,6 +149,27 @@ get_unique_id() const {
   return strm.str();
 }
 
+/**
+ *
+ */
+Thread *ThreadWin32Impl::
+get_current_thread() {
+  Thread *thread = _current_thread;
+  return (thread != nullptr) ? thread : init_current_thread();
+}
+
+/**
+ * Associates the indicated Thread object with the currently-executing thread.
+ * You should not call this directly; use Thread::bind_thread() instead.
+ */
+void ThreadWin32Impl::
+bind_thread(Thread *thread) {
+  if (_current_thread == nullptr && thread == Thread::get_main_thread()) {
+    _main_thread_known.test_and_set(std::memory_order_relaxed);
+  }
+  _current_thread = thread;
+}
+
 /**
  * The entry point of each thread.
  */
@@ -143,8 +180,7 @@ root_func(LPVOID data) {
     // TAU_PROFILE("void ThreadWin32Impl::root_func()", " ", TAU_USER);
 
     ThreadWin32Impl *self = (ThreadWin32Impl *)data;
-    BOOL result = TlsSetValue(_pt_ptr_index, self->_parent_obj);
-    nassertr(result, 1);
+    _current_thread = self->_parent_obj;
 
     {
       self->_mutex.lock();
@@ -185,28 +221,4 @@ root_func(LPVOID data) {
   return 0;
 }
 
-/**
- * Allocate a new index to store the Thread parent pointer as a piece of per-
- * thread private data.
- */
-void ThreadWin32Impl::
-init_pt_ptr_index() {
-  nassertv(!_got_pt_ptr_index);
-
-  _pt_ptr_index = TlsAlloc();
-  if (_pt_ptr_index == TLS_OUT_OF_INDEXES) {
-    thread_cat->error()
-      << "Unable to associate Thread pointers with threads.\n";
-    return;
-  }
-
-  _got_pt_ptr_index = true;
-
-  // Assume that we must be in the main thread, since this method must be
-  // called before the first thread is spawned.
-  Thread *main_thread_obj = Thread::get_main_thread();
-  BOOL result = TlsSetValue(_pt_ptr_index, main_thread_obj);
-  nassertv(result);
-}
-
 #endif  // THREAD_WIN32_IMPL

+ 2 - 6
panda/src/pipeline/threadWin32Impl.h

@@ -43,8 +43,8 @@ public:
 
   INLINE static void prepare_for_exit();
 
-  INLINE static Thread *get_current_thread();
-  INLINE static void bind_thread(Thread *thread);
+  static Thread *get_current_thread();
+  static void bind_thread(Thread *thread);
   INLINE static bool is_threading_supported();
   INLINE static bool is_true_threads();
   INLINE static bool is_simple_threads();
@@ -54,7 +54,6 @@ public:
 
 private:
   static DWORD WINAPI root_func(LPVOID data);
-  static void init_pt_ptr_index();
 
   enum Status {
     S_new,
@@ -70,9 +69,6 @@ private:
   DWORD _thread_id;
   bool _joinable;
   Status _status;
-
-  static DWORD _pt_ptr_index;
-  static bool _got_pt_ptr_index;
 };
 
 #include "threadWin32Impl.I"

+ 8 - 10
panda/src/pstatclient/pStatClient.I

@@ -16,8 +16,7 @@
  */
 INLINE int PStatClient::
 get_num_collectors() const {
-  ReMutexHolder holder(_lock);
-  return (int)_num_collectors;
+  return _num_collectors.load(std::memory_order_relaxed);
 }
 
 /**
@@ -25,7 +24,7 @@ get_num_collectors() const {
  */
 INLINE PStatCollectorDef *PStatClient::
 get_collector_def(int index) const {
-  nassertr(index >= 0 && index < _num_collectors, nullptr);
+  nassertr(index >= 0 && index < get_num_collectors(), nullptr);
 
   return get_collector_ptr(index)->get_def(this, index);
 }
@@ -35,8 +34,7 @@ get_collector_def(int index) const {
  */
 INLINE int PStatClient::
 get_num_threads() const {
-  ReMutexHolder holder(_lock);
-  return (int)_num_threads;
+  return _num_threads.load(std::memory_order_relaxed);
 }
 
 /**
@@ -44,7 +42,7 @@ get_num_threads() const {
  */
 INLINE std::string PStatClient::
 get_thread_name(int index) const {
-  nassertr(index >= 0 && index < AtomicAdjust::get(_num_threads), std::string());
+  nassertr(index >= 0 && index < get_num_threads(), std::string());
   return get_thread_ptr(index)->_name;
 }
 
@@ -53,7 +51,7 @@ get_thread_name(int index) const {
  */
 INLINE std::string PStatClient::
 get_thread_sync_name(int index) const {
-  nassertr(index >= 0 && index < AtomicAdjust::get(_num_threads), std::string());
+  nassertr(index >= 0 && index < get_num_threads(), std::string());
   return get_thread_ptr(index)->_sync_name;
 }
 
@@ -62,7 +60,7 @@ get_thread_sync_name(int index) const {
  */
 INLINE PT(Thread) PStatClient::
 get_thread_object(int index) const {
-  nassertr(index >= 0 && index < AtomicAdjust::get(_num_threads), nullptr);
+  nassertr(index >= 0 && index < get_num_threads(), nullptr);
   InternalThread *thread = get_thread_ptr(index);
   return thread->_thread.lock();
 }
@@ -144,7 +142,7 @@ get_impl() const {
  */
 INLINE PStatClient::Collector *PStatClient::
 get_collector_ptr(int collector_index) const {
-  CollectorPointer *collectors = (CollectorPointer *)AtomicAdjust::get_ptr(_collectors);
+  CollectorPointer *collectors = _collectors.load(std::memory_order_consume);
   return collectors[collector_index];
 }
 
@@ -153,7 +151,7 @@ get_collector_ptr(int collector_index) const {
  */
 INLINE PStatClient::InternalThread *PStatClient::
 get_thread_ptr(int thread_index) const {
-  ThreadPointer *threads = (ThreadPointer *)AtomicAdjust::get_ptr(_threads);
+  ThreadPointer *threads = _threads.load(std::memory_order_consume);
   return threads[thread_index];
 }
 

+ 77 - 83
panda/src/pstatclient/pStatClient.cxx

@@ -77,14 +77,6 @@ PStatClient() :
   _lock("PStatClient::_lock"),
   _impl(nullptr)
 {
-  _collectors = nullptr;
-  _collectors_size = 0;
-  _num_collectors = 0;
-
-  _threads = nullptr;
-  _threads_size = 0;
-  _num_threads = 0;
-
   // We always have a collector at index 0 named "Frame".  This tracks the
   // total frame time and is the root of all other collectors.  We have to
   // make this one by hand since it's the root.
@@ -152,7 +144,7 @@ get_max_rate() const {
  */
 PStatCollector PStatClient::
 get_collector(int index) const {
-  nassertr(index >= 0 && index < AtomicAdjust::get(_num_collectors), PStatCollector());
+  nassertr(index >= 0 && index < get_num_collectors(), PStatCollector());
   return PStatCollector((PStatClient *)this, index);
 }
 
@@ -161,7 +153,7 @@ get_collector(int index) const {
  */
 string PStatClient::
 get_collector_name(int index) const {
-  nassertr(index >= 0 && index < AtomicAdjust::get(_num_collectors), string());
+  nassertr(index >= 0 && index < get_num_collectors(), string());
 
   return get_collector_ptr(index)->get_name();
 }
@@ -173,7 +165,7 @@ get_collector_name(int index) const {
  */
 string PStatClient::
 get_collector_fullname(int index) const {
-  nassertr(index >= 0 && index < AtomicAdjust::get(_num_collectors), string());
+  nassertr(index >= 0 && index < get_num_collectors(), string());
 
   Collector *collector = get_collector_ptr(index);
   int parent_index = collector->get_parent_index();
@@ -191,7 +183,7 @@ get_collector_fullname(int index) const {
 PStatThread PStatClient::
 get_thread(int index) const {
   ReMutexHolder holder(_lock);
-  nassertr(index >= 0 && index < _num_threads, PStatThread());
+  nassertr(index >= 0 && index < get_num_threads(), PStatThread());
   return PStatThread((PStatClient *)this, index);
 }
 
@@ -462,8 +454,9 @@ client_disconnect() {
     _impl = nullptr;
   }
 
-  ThreadPointer *threads = (ThreadPointer *)_threads;
-  for (int ti = 0; ti < _num_threads; ++ti) {
+  // These can be relaxed loads because we hold the lock.
+  ThreadPointer *threads = _threads.load(std::memory_order_relaxed);
+  for (int ti = 0; ti < get_num_threads(); ++ti) {
     InternalThread *thread = threads[ti];
     thread->_frame_number = 0;
     thread->_is_active = false;
@@ -471,14 +464,11 @@ client_disconnect() {
     thread->_frame_data.clear();
   }
 
-  CollectorPointer *collectors = (CollectorPointer *)_collectors;
-  for (int ci = 0; ci < _num_collectors; ++ci) {
+  CollectorPointer *collectors = _collectors.load(std::memory_order_relaxed);
+  for (int ci = 0; ci < get_num_collectors(); ++ci) {
     Collector *collector = collectors[ci];
-    PerThread::iterator ii;
-    for (ii = collector->_per_thread.begin();
-         ii != collector->_per_thread.end();
-         ++ii) {
-      (*ii)._nested_count = 0;
+    for (PerThreadData &per_thread : collector->_per_thread) {
+      per_thread._nested_count = 0;
     }
   }
 }
@@ -576,7 +566,8 @@ PStatCollector PStatClient::
 make_collector_with_name(int parent_index, const string &name) {
   ReMutexHolder holder(_lock);
 
-  nassertr(parent_index >= 0 && parent_index < _num_collectors,
+  int num_collectors = get_num_collectors();
+  nassertr(parent_index >= 0 && parent_index < num_collectors,
            PStatCollector());
 
   Collector *parent = get_collector_ptr(parent_index);
@@ -593,26 +584,25 @@ make_collector_with_name(int parent_index, const string &name) {
   if (ni != parent->_children.end()) {
     // We already had a collector by this name; return it.
     int index = (*ni).second;
-    nassertr(index >= 0 && index < _num_collectors, PStatCollector());
+    nassertr(index >= 0 && index < num_collectors, PStatCollector());
     return PStatCollector(this, (*ni).second);
   }
 
   // Create a new collector for this name.
-  int new_index = _num_collectors;
-  parent->_children.insert(ThingsByName::value_type(name, new_index));
+  parent->_children.insert(ThingsByName::value_type(name, num_collectors));
 
   Collector *collector = new Collector(parent_index, name);
-  // collector->_def = new PStatCollectorDef(new_index, name);
+  // collector->_def = new PStatCollectorDef(num_collectors, name);
   // collector->_def->set_parent(*_collectors[parent_index]._def);
   // initialize_collector_def(this, collector->_def);
 
   // We need one PerThreadData for each thread.
-  while ((int)collector->_per_thread.size() < _num_threads) {
+  while ((int)collector->_per_thread.size() < get_num_threads()) {
     collector->_per_thread.push_back(PerThreadData());
   }
   add_collector(collector);
 
-  return PStatCollector(this, new_index);
+  return PStatCollector(this, num_collectors);
 }
 
 /**
@@ -662,8 +652,8 @@ do_make_thread(Thread *thread) {
          vi != indices.end();
          ++vi) {
       int index = (*vi);
-      nassertr(index >= 0 && index < _num_threads, PStatThread());
-      ThreadPointer *threads = (ThreadPointer *)_threads;
+      nassertr(index >= 0 && index < get_num_threads(), PStatThread());
+      ThreadPointer *threads = _threads.load(std::memory_order_relaxed);
       if (threads[index]->_thread.was_deleted() &&
           threads[index]->_sync_name == thread->get_sync_name()) {
         // Yes, re-use this one.
@@ -676,7 +666,7 @@ do_make_thread(Thread *thread) {
   }
 
   // Create a new PStatsThread for this thread pointer.
-  int new_index = _num_threads;
+  int new_index = get_num_threads();
   thread->set_pstats_index(new_index);
   thread->set_pstats_callback(this);
 
@@ -693,7 +683,7 @@ do_make_thread(Thread *thread) {
 PStatThread PStatClient::
 make_gpu_thread(const string &name) {
   ReMutexHolder holder(_lock);
-  int new_index = _num_threads;
+  int new_index = get_num_threads();
 
   InternalThread *pthread = new InternalThread(name, "GPU");
   add_thread(pthread);
@@ -710,8 +700,8 @@ make_gpu_thread(const string &name) {
  */
 bool PStatClient::
 is_active(int collector_index, int thread_index) const {
-  nassertr(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors), false);
-  nassertr(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads), false);
+  nassertr(collector_index >= 0 && collector_index < get_num_collectors(), false);
+  nassertr(thread_index >= 0 && thread_index < get_num_threads(), false);
 
   return (client_is_connected() &&
           get_collector_ptr(collector_index)->is_active() &&
@@ -727,8 +717,8 @@ is_active(int collector_index, int thread_index) const {
  */
 bool PStatClient::
 is_started(int collector_index, int thread_index) const {
-  nassertr(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors), false);
-  nassertr(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads), false);
+  nassertr(collector_index >= 0 && collector_index < get_num_collectors(), false);
+  nassertr(thread_index >= 0 && thread_index < get_num_threads(), false);
 
   Collector *collector = get_collector_ptr(collector_index);
   InternalThread *thread = get_thread_ptr(thread_index);
@@ -758,8 +748,8 @@ start(int collector_index, int thread_index) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -789,8 +779,8 @@ start(int collector_index, int thread_index, double as_of) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -820,8 +810,8 @@ stop(int collector_index, int thread_index) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -862,8 +852,8 @@ stop(int collector_index, int thread_index, double as_of) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -905,8 +895,8 @@ clear_level(int collector_index, int thread_index) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -930,8 +920,8 @@ set_level(int collector_index, int thread_index, double level) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -963,8 +953,8 @@ add_level(int collector_index, int thread_index, double increment) {
   }
 
 #ifdef _DEBUG
-  nassertv(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors));
-  nassertv(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads));
+  nassertv(collector_index >= 0 && collector_index < get_num_collectors());
+  nassertv(thread_index >= 0 && thread_index < get_num_threads());
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -991,8 +981,8 @@ get_level(int collector_index, int thread_index) const {
   }
 
 #ifdef _DEBUG
-  nassertr(collector_index >= 0 && collector_index < AtomicAdjust::get(_num_collectors), 0.0f);
-  nassertr(thread_index >= 0 && thread_index < AtomicAdjust::get(_num_threads), 0.0f);
+  nassertr(collector_index >= 0 && collector_index < get_num_collectors(), 0.0f);
+  nassertr(thread_index >= 0 && thread_index < get_num_threads(), 0.0f);
 #endif
 
   Collector *collector = get_collector_ptr(collector_index);
@@ -1050,17 +1040,19 @@ stop_clock_wait() {
  */
 void PStatClient::
 add_collector(PStatClient::Collector *collector) {
-  if (_num_collectors >= _collectors_size) {
+  int num_collectors = get_num_collectors();
+  if (num_collectors >= _collectors_size) {
     // We need to grow the array.  We have to be careful here, because there
     // might be clients accessing the array right now who are not protected by
     // the lock.
-    int new_collectors_size = (_collectors_size == 0) ? 128 : _collectors_size * 2;
+    size_t new_collectors_size = (_collectors_size == 0) ? 128 : _collectors_size * 2;
+    CollectorPointer *old_collectors = _collectors.load(std::memory_order_relaxed);
     CollectorPointer *new_collectors = new CollectorPointer[new_collectors_size];
-    if (_collectors != nullptr) {
-      memcpy(new_collectors, _collectors, _num_collectors * sizeof(CollectorPointer));
+    if (old_collectors != nullptr) {
+      memcpy(new_collectors, old_collectors, num_collectors * sizeof(CollectorPointer));
     }
-    AtomicAdjust::set_ptr(_collectors, new_collectors);
-    AtomicAdjust::set(_collectors_size, new_collectors_size);
+    _collectors_size = new_collectors_size;
+    _collectors.store(new_collectors, std::memory_order_release);
 
     // Now, we still have the old array, which we allow to leak.  We should
     // delete it, but there might be a thread out there that's still trying to
@@ -1068,14 +1060,14 @@ add_collector(PStatClient::Collector *collector) {
     // much, since it's not a big leak.  (We will only reallocate the array so
     // many times in an application, and then no more.)
 
-    new_collectors[_num_collectors] = collector;
-    AtomicAdjust::inc(_num_collectors);
-
-  } else {
-    CollectorPointer *collectors = (CollectorPointer *)_collectors;
-    collectors[_num_collectors] = collector;
-    AtomicAdjust::inc(_num_collectors);
+    new_collectors[num_collectors] = collector;
+  }
+  else {
+    CollectorPointer *collectors = _collectors.load(std::memory_order_relaxed);
+    collectors[num_collectors] = collector;
   }
+
+  _num_collectors.fetch_add(1, std::memory_order_release);
 }
 
 /**
@@ -1084,21 +1076,22 @@ add_collector(PStatClient::Collector *collector) {
  */
 void PStatClient::
 add_thread(PStatClient::InternalThread *thread) {
-  _threads_by_name[thread->_name].push_back(_num_threads);
-  _threads_by_sync_name[thread->_sync_name].push_back(_num_threads);
+  int num_threads = get_num_threads();
+  _threads_by_name[thread->_name].push_back(num_threads);
+  _threads_by_sync_name[thread->_sync_name].push_back(num_threads);
 
-  if (_num_threads >= _threads_size) {
+  if (num_threads >= _threads_size) {
     // We need to grow the array.  We have to be careful here, because there
     // might be clients accessing the array right now who are not protected by
     // the lock.
-    int new_threads_size = (_threads_size == 0) ? 128 : _threads_size * 2;
+    size_t new_threads_size = (_threads_size == 0) ? 128 : _threads_size * 2;
+    ThreadPointer *old_threads = _threads.load(std::memory_order_relaxed);
     ThreadPointer *new_threads = new ThreadPointer[new_threads_size];
-    if (_threads != nullptr) {
-      memcpy(new_threads, _threads, _num_threads * sizeof(ThreadPointer));
+    if (old_threads != nullptr) {
+      memcpy(new_threads, old_threads, num_threads * sizeof(ThreadPointer));
     }
-    // We assume that assignment to a pointer and to an int are each atomic.
-    AtomicAdjust::set_ptr(_threads, new_threads);
-    AtomicAdjust::set(_threads_size, new_threads_size);
+    _threads_size = new_threads_size;
+    _threads.store(new_threads, std::memory_order_release);
 
     // Now, we still have the old array, which we allow to leak.  We should
     // delete it, but there might be a thread out there that's still trying to
@@ -1106,22 +1099,23 @@ add_thread(PStatClient::InternalThread *thread) {
     // much, since it's not a big leak.  (We will only reallocate the array so
     // many times in an application, and then no more.)
 
-    new_threads[_num_threads] = thread;
-
-  } else {
-    ThreadPointer *threads = (ThreadPointer *)_threads;
-    threads[_num_threads] = thread;
+    new_threads[num_threads] = thread;
+  }
+  else {
+    ThreadPointer *threads = _threads.load(std::memory_order_relaxed);
+    threads[num_threads] = thread;
   }
 
-  AtomicAdjust::inc(_num_threads);
+  _num_threads.fetch_add(1, std::memory_order_release);
+  ++num_threads;
 
   // We need an additional PerThreadData for this thread in all of the
   // collectors.
-  CollectorPointer *collectors = (CollectorPointer *)_collectors;
-  for (int ci = 0; ci < _num_collectors; ++ci) {
+  CollectorPointer *collectors = _collectors.load(std::memory_order_relaxed);
+  for (int ci = 0; ci < get_num_collectors(); ++ci) {
     Collector *collector = collectors[ci];
     collector->_per_thread.push_back(PerThreadData());
-    nassertv((int)collector->_per_thread.size() == _num_threads);
+    nassertv((int)collector->_per_thread.size() == num_threads);
   }
 }
 

+ 7 - 8
panda/src/pstatclient/pStatClient.h

@@ -26,7 +26,7 @@
 #include "thread.h"
 #include "weakPointerTo.h"
 #include "vector_int.h"
-#include "atomicAdjust.h"
+#include "patomic.h"
 #include "numeric_types.h"
 #include "bitArray.h"
 
@@ -195,9 +195,9 @@ private:
     PerThread _per_thread;
   };
   typedef Collector *CollectorPointer;
-  AtomicAdjust::Pointer _collectors;  // CollectorPointer *_collectors;
-  AtomicAdjust::Integer _collectors_size;  // size of the allocated array
-  AtomicAdjust::Integer _num_collectors;   // number of in-use elements within the array
+  patomic<CollectorPointer *> _collectors {nullptr};
+  size_t _collectors_size {0};  // size of the allocated array
+  patomic<int> _num_collectors {0};   // number of in-use elements within the array
 
   // This defines a single thread, i.e.  a separate chain of execution,
   // independent of all other threads.  Timing and level data are maintained
@@ -216,7 +216,6 @@ private:
     double _next_packet;
 
     bool _thread_active;
-    BitArray _active_collectors;  // no longer used.
 
     // This mutex is used to protect writes to _frame_data for this particular
     // thread, as well as writes to the _per_thread data for this particular
@@ -224,9 +223,9 @@ private:
     LightMutex _thread_lock;
   };
   typedef InternalThread *ThreadPointer;
-  AtomicAdjust::Pointer _threads;  // ThreadPointer *_threads;
-  AtomicAdjust::Integer _threads_size;  // size of the allocated array
-  AtomicAdjust::Integer _num_threads;   // number of in-use elements within the array
+  patomic<ThreadPointer *> _threads {nullptr};
+  size_t _threads_size {0};  // size of the allocated array
+  patomic<int> _num_threads {0};   // number of in-use elements within the array
 
   mutable PStatClientImpl *_impl;
 

+ 6 - 0
panda/src/pstatclient/pStatClientControlMessage.cxx

@@ -39,6 +39,7 @@ encode(Datagram &datagram) const {
     datagram.add_string(_client_progname);
     datagram.add_uint16(_major_version);
     datagram.add_uint16(_minor_version);
+    datagram.add_uint32(_client_pid);
     break;
 
   case T_define_collectors:
@@ -86,6 +87,11 @@ decode(const Datagram &datagram, PStatClientVersion *version) {
       _major_version = source.get_uint16();
       _minor_version = source.get_uint16();
     }
+    if (source.get_remaining_size() >= 4) {
+      _client_pid = source.get_uint32();
+    } else {
+      _client_pid = -1;
+    }
     break;
 
   case T_define_collectors:

+ 1 - 0
panda/src/pstatclient/pStatClientControlMessage.h

@@ -47,6 +47,7 @@ public:
   // Used for T_hello
   std::string _client_hostname;
   std::string _client_progname;
+  int _client_pid;
   int _major_version;
   int _minor_version;
 

+ 7 - 1
panda/src/pstatclient/pStatClientImpl.cxx

@@ -158,6 +158,8 @@ client_disconnect() {
  */
 void PStatClientImpl::
 new_frame(int thread_index) {
+  double frame_start = get_real_time();
+
   nassertv(thread_index >= 0 && thread_index < _client->_num_threads);
 
   PStatClient::InternalThread *pthread = _client->get_thread_ptr(thread_index);
@@ -178,7 +180,6 @@ new_frame(int thread_index) {
     return;
   }
 
-  double frame_start = get_real_time();
   int frame_number = -1;
   PStatFrameData frame_data;
 
@@ -396,6 +397,11 @@ send_hello() {
   message._type = PStatClientControlMessage::T_hello;
   message._client_hostname = get_hostname();
   message._client_progname = _client_name;
+#ifdef _WIN32
+  message._client_pid = GetCurrentProcessId();
+#else
+  message._client_pid = getpid();
+#endif
   message._major_version = get_current_pstat_major_version();
   message._minor_version = get_current_pstat_minor_version();
 

+ 1 - 0
panda/src/pstatclient/pStatProperties.cxx

@@ -109,6 +109,7 @@ static TimeCollectorProperties time_properties[] = {
   { 1, "App:Collisions:Reset",             { 0.0, 0.0, 0.5 } },
   { 0, "App:Data graph",                   { 0.5, 0.8, 0.4 } },
   { 1, "App:Show code",                    { 0.8, 0.2, 1.0 } },
+  { 0, "App:Show code:General",            { 0.4, 0.3, 0.9 } },
   { 0, "App:Show code:Nametags",           { 0.8, 0.8, 1.0 } },
   { 0, "App:Show code:Nametags:2d",        { 0.0, 0.0, 0.5 } },
   { 0, "App:Show code:Nametags:2d:Contents", { 0.0, 0.5, 0.0 } },

+ 4 - 2
pandatool/src/gtk-stats/CMakeLists.txt

@@ -1,9 +1,10 @@
-if(NOT HAVE_GTK2 OR NOT HAVE_NET)
+if(NOT HAVE_GTK3 OR NOT HAVE_NET)
   return()
 endif()
 
 set(GTKSTATS_HEADERS
   gtkStatsChartMenu.h
+  gtkStatsFlameGraph.h
   gtkStatsGraph.h
   gtkStatsLabel.h
   gtkStatsLabelStack.h
@@ -17,6 +18,7 @@ set(GTKSTATS_HEADERS
 set(GTKSTATS_SOURCES
   gtkStats.cxx
   gtkStatsChartMenu.cxx
+  gtkStatsFlameGraph.cxx
   gtkStatsGraph.cxx
   gtkStatsLabel.cxx
   gtkStatsLabelStack.cxx
@@ -28,7 +30,7 @@ set(GTKSTATS_SOURCES
 
 composite_sources(gtk-stats GTKSTATS_SOURCES)
 add_executable(gtk-stats ${GTKSTATS_HEADERS} ${GTKSTATS_SOURCES})
-target_link_libraries(gtk-stats p3progbase p3pstatserver PKG::GTK2)
+target_link_libraries(gtk-stats p3progbase p3pstatserver PKG::GTK3)
 
 # This program is NOT actually called gtk-stats. It's pstats-gtk on Win32 and
 # pstats everywhere else (as the Win32 GUI is not built).

+ 1 - 1
pandatool/src/gtk-stats/gtkStats.cxx

@@ -44,7 +44,7 @@ timer(gpointer data) {
     // are getting starved and falling behind, so that the user still gets a
     // chance to see *something* happen onscreen, even if it's just
     // increasingly old data.
-    gdk_window_process_all_updates();
+    //gdk_window_process_all_updates();
   }
 
   return TRUE;

+ 32 - 14
pandatool/src/gtk-stats/gtkStatsChartMenu.cxx

@@ -104,9 +104,9 @@ do_update() {
       // We put a separator between the above frame collector and the first
       // level collector.
       if (needs_separator) {
-  GtkWidget *sep = gtk_separator_menu_item_new();
-  gtk_widget_show(sep);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
+        GtkWidget *sep = gtk_separator_menu_item_new();
+        gtk_widget_show(sep);
+        gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
 
         needs_separator = false;
       }
@@ -116,20 +116,34 @@ do_update() {
     }
   }
 
-  // Also a menu item for a piano roll (following a separator).
+  // Also menu items for flame graph and piano roll (following a separator).
   GtkWidget *sep = gtk_separator_menu_item_new();
   gtk_widget_show(sep);
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
 
-  GtkStatsMonitor::MenuDef smd(_thread_index, -1, false);
-  const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+  {
+    GtkStatsMonitor::MenuDef smd(_thread_index, -2, false);
+    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
 
-  GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll");
-  gtk_widget_show(menu_item);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Flame Graph");
+    gtk_widget_show(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
 
-  g_signal_connect_swapped(G_OBJECT(menu_item), "activate",
-         G_CALLBACK(handle_menu), (void *)(const void *)menu_def);
+    g_signal_connect_swapped(G_OBJECT(menu_item), "activate",
+           G_CALLBACK(handle_menu), (void *)(const void *)menu_def);
+  }
+
+  {
+    GtkStatsMonitor::MenuDef smd(_thread_index, -1, false);
+    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll");
+    gtk_widget_show(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
+
+    g_signal_connect_swapped(G_OBJECT(menu_item), "activate",
+           G_CALLBACK(handle_menu), (void *)(const void *)menu_def);
+  }
 }
 
 /**
@@ -138,7 +152,7 @@ do_update() {
  */
 void GtkStatsChartMenu::
 add_view(GtkWidget *parent_menu, const PStatViewLevel *view_level,
-   bool show_level) {
+         bool show_level) {
   int collector = view_level->get_collector();
 
   const PStatClientData *client_data = _monitor->get_client_data();
@@ -189,9 +203,13 @@ handle_menu(gpointer data) {
     return;
   }
 
-  if (menu_def->_collector_index < 0) {
+  if (menu_def->_collector_index == -2) {
+    monitor->open_flame_graph(menu_def->_thread_index);
+  }
+  else if (menu_def->_collector_index < 0) {
     monitor->open_piano_roll(menu_def->_thread_index);
-  } else {
+  }
+  else {
     monitor->open_strip_chart(menu_def->_thread_index,
             menu_def->_collector_index,
             menu_def->_show_level);

+ 551 - 0
pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx

@@ -0,0 +1,551 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file gtkStatsFlameGraph.cxx
+ * @author rdb
+ * @date 2022-02-02
+ */
+
+#include "gtkStatsFlameGraph.h"
+#include "gtkStatsLabel.h"
+#include "gtkStatsMonitor.h"
+#include "pStatCollectorDef.h"
+
+static const int default_flame_graph_width = 800;
+static const int default_flame_graph_height = 150;
+
+/**
+ *
+ */
+GtkStatsFlameGraph::
+GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
+                   int collector_index) :
+  PStatFlameGraph(monitor, monitor->get_view(thread_index),
+                  thread_index, collector_index,
+                  default_flame_graph_width,
+                  default_flame_graph_height),
+  GtkStatsGraph(monitor)
+{
+  // Let's show the units on the guide bar labels.  There's room.
+  set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+  // Put some stuff on top of the graph.
+  _top_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+  gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox,
+         FALSE, FALSE, 0);
+
+  _average_check_box = gtk_check_button_new_with_label("Average");
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_average_check_box), TRUE);
+  g_signal_connect(G_OBJECT(_average_check_box), "toggled",
+       G_CALLBACK(toggled_callback), this);
+
+  // Add a DrawingArea widget on top of the graph, to display all of the scale
+  // units.
+  _scale_area = gtk_drawing_area_new();
+  g_signal_connect(G_OBJECT(_scale_area), "draw", G_CALLBACK(draw_callback), this);
+
+  _total_label = gtk_label_new("");
+  gtk_box_pack_start(GTK_BOX(_top_hbox), _average_check_box, FALSE, FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(_top_hbox), _scale_area, TRUE, TRUE, 0);
+  gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label, FALSE, FALSE, 0);
+
+  gtk_widget_set_size_request(_graph_window, default_flame_graph_width,
+                              default_flame_graph_height);
+
+  // Add a fixed container to the overlay to allow arbitrary positioning
+  // of labels therein.
+  _fixed = gtk_fixed_new();
+  gtk_overlay_add_overlay(GTK_OVERLAY(_graph_overlay), _fixed);
+
+  gtk_widget_show_all(_window);
+  gtk_widget_show(_window);
+
+  // Allow the window to be resized as small as the user likes.  We have to do
+  // this after the window has been shown; otherwise, it will affect the
+  // window's initial size.
+  gtk_widget_set_size_request(_window, 0, 0);
+
+  clear_region();
+}
+
+/**
+ *
+ */
+GtkStatsFlameGraph::
+~GtkStatsFlameGraph() {
+}
+
+/**
+ * Called whenever a new Collector definition is received from the client.
+ */
+void GtkStatsFlameGraph::
+new_collector(int collector_index) {
+  GtkStatsGraph::new_collector(collector_index);
+}
+
+/**
+ * Called as each frame's data is made available.  There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all.  The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void GtkStatsFlameGraph::
+new_data(int thread_index, int frame_number) {
+  if (is_title_unknown()) {
+    std::string window_title = get_title_text();
+    if (!is_title_unknown()) {
+      gtk_window_set_title(GTK_WINDOW(_window), window_title.c_str());
+    }
+  }
+
+  if (!_pause) {
+    update();
+
+    std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+    if (_net_value_text != text) {
+      _net_value_text = text;
+      gtk_label_set_text(GTK_LABEL(_total_label), _net_value_text.c_str());
+    }
+  }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void GtkStatsFlameGraph::
+force_redraw() {
+  PStatFlameGraph::force_redraw();
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void GtkStatsFlameGraph::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+  PStatFlameGraph::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void GtkStatsFlameGraph::
+set_time_units(int unit_mask) {
+  int old_unit_mask = get_guide_bar_units();
+  if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+    unit_mask = unit_mask & (GBU_hz | GBU_ms);
+    unit_mask |= (old_unit_mask & GBU_show_units);
+    set_guide_bar_units(unit_mask);
+
+    gtk_widget_queue_draw(_scale_area);
+  }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+void GtkStatsFlameGraph::
+on_click_label(int collector_index) {
+  int prev_collector_index = get_collector_index();
+  if (collector_index == prev_collector_index && collector_index != 0) {
+    // Clicking on the top label means to go up to the parent level.
+    const PStatClientData *client_data =
+      GtkStatsGraph::_monitor->get_client_data();
+    if (client_data->has_collector(collector_index)) {
+      const PStatCollectorDef &def =
+        client_data->get_collector_def(collector_index);
+      collector_index = def._parent_index;
+      set_collector_index(collector_index);
+    }
+  }
+  else {
+    // Clicking on any other label means to focus on that.
+    set_collector_index(collector_index);
+  }
+
+  // Change the root collector to show the full name.
+  if (prev_collector_index != collector_index) {
+    auto it = _labels.find(prev_collector_index);
+    if (it != _labels.end()) {
+      it->second->update_text(false);
+    }
+    it = _labels.find(collector_index);
+    if (it != _labels.end()) {
+      it->second->update_text(true);
+    }
+  }
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void GtkStatsFlameGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void GtkStatsFlameGraph::
+on_leave_label(int collector_index) {
+  if (collector_index == _highlighted_index && collector_index != -1) {
+    _highlighted_index = -1;
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsFlameGraph::
+get_label_tooltip(int collector_index) const {
+  return PStatFlameGraph::get_label_tooltip(collector_index);
+}
+
+/**
+ * Repositions the labels.
+ */
+void GtkStatsFlameGraph::
+update_labels() {
+  PStatFlameGraph::update_labels();
+}
+
+/**
+ * Repositions a label.  If width is 0, the label should be deleted.
+ */
+void GtkStatsFlameGraph::
+update_label(int collector_index, int row, int x, int width) {
+  GtkStatsLabel *label;
+
+  auto it = _labels.find(collector_index);
+  if (it != _labels.end()) {
+    label = it->second;
+    if (width == 0) {
+      gtk_container_remove(GTK_CONTAINER(_fixed), label->get_widget());
+      delete label;
+      _labels.erase(it);
+      return;
+    }
+    gtk_fixed_move(GTK_FIXED(_fixed), label->get_widget(), x, _ysize - (row + 1) * label->get_height());
+  }
+  else {
+    if (width == 0) {
+      return;
+    }
+    label = new GtkStatsLabel(GtkStatsGraph::_monitor, this, _thread_index, collector_index, false, false);
+    _labels[collector_index] = label;
+    gtk_fixed_put(GTK_FIXED(_fixed), label->get_widget(), x, _ysize - (row + 1) * label->get_height());
+  }
+
+  gtk_widget_set_size_request(label->get_widget(), std::min(width, _xsize), label->get_height());
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void GtkStatsFlameGraph::
+normal_guide_bars() {
+  // We want vaguely 100 pixels between guide bars.
+  double res = gdk_screen_get_resolution(gdk_screen_get_default());
+  int num_bars = (int)(get_xsize() / (100.0 * (res > 0 ? res / 96.0 : 1.0)));
+
+  _guide_bars.clear();
+
+  double dist = get_horizontal_scale() / num_bars;
+
+  for (int i = 1; i < num_bars; ++i) {
+    _guide_bars.push_back(make_guide_bar(i * dist));
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Erases the chart area.
+ */
+void GtkStatsFlameGraph::
+clear_region() {
+  cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
+  cairo_paint(_cr);
+}
+
+/**
+ * Erases the chart area in preparation for drawing a bunch of bars.
+ */
+void GtkStatsFlameGraph::
+begin_draw() {
+  clear_region();
+
+  // Draw in the guide bars.
+  int num_guide_bars = get_num_guide_bars();
+  for (int i = 0; i < num_guide_bars; i++) {
+    draw_guide_bar(_cr, get_guide_bar(i));
+  }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void GtkStatsFlameGraph::
+end_draw() {
+  gtk_widget_queue_draw(_graph_window);
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void GtkStatsFlameGraph::
+idle() {
+}
+
+/**
+ * This is called during the servicing of the draw event; it gives a derived
+ * class opportunity to do some further painting into the graph window.
+ */
+void GtkStatsFlameGraph::
+additional_graph_window_paint(cairo_t *cr) {
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (int i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_bar(cr, get_user_guide_bar(i));
+  }
+}
+
+/**
+ * Based on the mouse position within the window's client area, look for
+ * draggable things the mouse might be hovering over and return the
+ * apprioprate DragMode enum or DM_none if nothing is indicated.
+ */
+GtkStatsGraph::DragMode GtkStatsFlameGraph::
+consider_drag_start(int graph_x, int graph_y) {
+  if (graph_y >= 0 && graph_y < get_ysize()) {
+    if (graph_x >= 0 && graph_x < get_xsize()) {
+      // See if the mouse is over a user-defined guide bar.
+      int x = graph_x;
+      double from_height = pixel_to_height(x - 2);
+      double to_height = pixel_to_height(x + 2);
+      _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+      if (_drag_guide_bar >= 0) {
+        return DM_guide_bar;
+      }
+
+    } else {
+      // The mouse is left or right of the graph; maybe create a new guide
+      // bar.
+      return DM_new_guide_bar;
+    }
+  }
+
+  return DM_none;
+}
+
+/**
+ * Called when the mouse button is depressed within the graph window.
+ */
+gboolean GtkStatsFlameGraph::
+handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
+        bool double_click) {
+  if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+    if (double_click) {
+      // Clicking on whitespace in the graph goes to the parent.
+      on_click_label(get_collector_index());
+      return TRUE;
+    }
+  }
+
+  if (_potential_drag_mode == DM_none) {
+    set_drag_mode(DM_scale);
+    _drag_scale_start = pixel_to_height(graph_x);
+    // SetCapture(_graph_window);
+    return TRUE;
+
+  } else if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+    set_drag_mode(DM_guide_bar);
+    _drag_start_x = graph_x;
+    // SetCapture(_graph_window);
+    return TRUE;
+  }
+
+  return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y,
+              double_click);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+gboolean GtkStatsFlameGraph::
+handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
+  if (_drag_mode == DM_scale) {
+    set_drag_mode(DM_none);
+    // ReleaseCapture();
+    return handle_motion(widget, graph_x, graph_y);
+
+  } else if (_drag_mode == DM_guide_bar) {
+    if (graph_x < 0 || graph_x >= get_xsize()) {
+      remove_user_guide_bar(_drag_guide_bar);
+    } else {
+      move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+    }
+    set_drag_mode(DM_none);
+    // ReleaseCapture();
+    return handle_motion(widget, graph_x, graph_y);
+  }
+
+  return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+gboolean GtkStatsFlameGraph::
+handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
+  if (_drag_mode == DM_new_guide_bar) {
+    // We haven't created the new guide bar yet; we won't until the mouse
+    // comes within the graph's region.
+    if (graph_x >= 0 && graph_x < get_xsize()) {
+      set_drag_mode(DM_guide_bar);
+      _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_x));
+      return TRUE;
+    }
+  }
+  else if (_drag_mode == DM_guide_bar) {
+    move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+    return TRUE;
+  }
+
+  return GtkStatsGraph::handle_motion(widget, graph_x, graph_y);
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void GtkStatsFlameGraph::
+draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) {
+  int x = height_to_pixel(bar._height);
+
+  if (x > 0 && x < get_xsize() - 1) {
+    // Only draw it if it's not too close to the top.
+    switch (bar._style) {
+    case GBS_target:
+      cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
+      break;
+
+    case GBS_user:
+      cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
+      break;
+
+    case GBS_normal:
+      cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
+      break;
+    }
+    cairo_move_to(cr, x, 0);
+    cairo_line_to(cr, x, get_ysize());
+    cairo_stroke(cr);
+  }
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void GtkStatsFlameGraph::
+draw_guide_labels(cairo_t *cr) {
+  int i;
+  int num_guide_bars = get_num_guide_bars();
+  for (i = 0; i < num_guide_bars; i++) {
+    draw_guide_label(cr, get_guide_bar(i));
+  }
+
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_label(cr, get_user_guide_bar(i));
+  }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void GtkStatsFlameGraph::
+draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
+  switch (bar._style) {
+  case GBS_target:
+    cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
+    break;
+
+  case GBS_user:
+    cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
+    break;
+
+  case GBS_normal:
+    cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
+    break;
+  }
+
+  int x = height_to_pixel(bar._height);
+  const std::string &label = bar._label;
+
+  PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str());
+  int width, height;
+  pango_layout_get_pixel_size(layout, &width, &height);
+
+  if (bar._style != GBS_user) {
+    double from_height = pixel_to_height(x - width);
+    double to_height = pixel_to_height(x + width);
+    if (find_user_guide_bar(from_height, to_height) >= 0) {
+      // Omit the label: there's a user-defined guide bar in the same space.
+      g_object_unref(layout);
+      return;
+    }
+  }
+
+  if (x >= 0 && x < get_xsize()) {
+    // Now convert our x to a coordinate within our drawing area.
+    int junk_y;
+
+    // The x coordinate comes from the graph_window.
+    gtk_widget_translate_coordinates(_graph_window, _scale_area,
+             x, 0,
+             &x, &junk_y);
+
+    GtkAllocation allocation;
+    gtk_widget_get_allocation(_scale_area, &allocation);
+
+    int this_x = x - width / 2;
+    if (this_x >= 0 && this_x + width < allocation.width) {
+      cairo_move_to(cr, this_x, allocation.height - height);
+      pango_cairo_show_layout(cr, layout);
+    }
+  }
+
+  g_object_unref(layout);
+}
+
+/**
+ * Called when the average check box is toggled.
+ */
+void GtkStatsFlameGraph::
+toggled_callback(GtkToggleButton *button, gpointer data) {
+  GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data;
+
+  bool active = gtk_toggle_button_get_active(button);
+  self->set_average_mode(active);
+}
+
+/**
+ * Draws in the scale labels.
+ */
+gboolean GtkStatsFlameGraph::
+draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
+  GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data;
+  self->draw_guide_labels(cr);
+
+  return TRUE;
+}

+ 81 - 0
pandatool/src/gtk-stats/gtkStatsFlameGraph.h

@@ -0,0 +1,81 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file gtkStatsFlameGraph.h
+ * @author rdb
+ * @date 2022-02-02
+ */
+
+#ifndef GTKSTATSFLAMEGRAPH_H
+#define GTKSTATSFLAMEGRAPH_H
+
+#include "pandatoolbase.h"
+
+#include "gtkStatsGraph.h"
+#include "pStatFlameGraph.h"
+
+class GtkStatsLabel;
+
+/**
+ * A window that draws a flame chart, which shows the collectors explicitly
+ * stopping and starting, one frame at a time.
+ */
+class GtkStatsFlameGraph : public PStatFlameGraph, public GtkStatsGraph {
+public:
+  GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
+                     int collector_index=0);
+  virtual ~GtkStatsFlameGraph();
+
+  virtual void new_collector(int collector_index);
+  virtual void new_data(int thread_index, int frame_number);
+  virtual void force_redraw();
+  virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+  virtual void set_time_units(int unit_mask);
+  virtual void on_click_label(int collector_index);
+  virtual void on_enter_label(int collector_index);
+  virtual void on_leave_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
+
+protected:
+  virtual void update_labels();
+  virtual void update_label(int collector_index, int row, int x, int width);
+  virtual void normal_guide_bars();
+
+  void clear_region();
+  virtual void begin_draw();
+  virtual void end_draw();
+  virtual void idle();
+
+  virtual void additional_graph_window_paint(cairo_t *cr);
+  virtual DragMode consider_drag_start(int graph_x, int graph_y);
+
+  virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
+               bool double_click);
+  virtual gboolean handle_button_release(GtkWidget *widget, int graph_x, int graph_y);
+  virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y);
+
+private:
+  void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar);
+  void draw_guide_labels(cairo_t *cr);
+  void draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar);
+
+  static void toggled_callback(GtkToggleButton *button, gpointer data);
+  static gboolean draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data);
+
+private:
+  std::string _net_value_text;
+  pmap<int, GtkStatsLabel *> _labels;
+
+  GtkWidget *_top_hbox;
+  GtkWidget *_average_check_box;
+  GtkWidget *_total_label;
+  GtkWidget *_fixed;
+};
+
+#endif

+ 105 - 83
pandatool/src/gtk-stats/gtkStatsGraph.cxx

@@ -14,21 +14,16 @@
 #include "gtkStatsGraph.h"
 #include "gtkStatsMonitor.h"
 #include "gtkStatsLabelStack.h"
+#include "convert_srgb.h"
 
-const GdkColor GtkStatsGraph::rgb_white = {
-  0, 0xffff, 0xffff, 0xffff
+const double GtkStatsGraph::rgb_light_gray[3] = {
+  0x9a / (double)0xff, 0x9a / (double)0xff, 0x9a / (double)0xff,
 };
-const GdkColor GtkStatsGraph::rgb_light_gray = {
-  0, 0x9a9a, 0x9a9a, 0x9a9a,
+const double GtkStatsGraph::rgb_dark_gray[3] = {
+  0x33 / (double)0xff, 0x33 / (double)0xff, 0x33 / (double)0xff,
 };
-const GdkColor GtkStatsGraph::rgb_dark_gray = {
-  0, 0x3333, 0x3333, 0x3333,
-};
-const GdkColor GtkStatsGraph::rgb_black = {
-  0, 0x0000, 0x0000, 0x0000
-};
-const GdkColor GtkStatsGraph::rgb_user_guide_bar = {
-  0, 0x8282, 0x9696, 0xffff
+const double GtkStatsGraph::rgb_user_guide_bar[3] = {
+  0x82 / (double)0xff, 0x96 / (double)0xff, 0xff / (double)0xff,
 };
 
 /**
@@ -45,14 +40,14 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
 
   GtkWidget *parent_window = monitor->get_window();
 
-  GdkDisplay *display = gdk_drawable_get_display(parent_window->window);
+  GdkDisplay *display = gdk_window_get_display(gtk_widget_get_window(parent_window));
   _hand_cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
 
-  _pixmap = nullptr;
-  _pixmap_gc = nullptr;
+  _cr_surface = nullptr;
+  _cr = nullptr;
 
-  _pixmap_xsize = 0;
-  _pixmap_ysize = 0;
+  _surface_xsize = 0;
+  _surface_ysize = 0;
 
   _window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 
@@ -79,8 +74,8 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
   gtk_widget_add_events(_graph_window,
       GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
       GDK_POINTER_MOTION_MASK);
-  g_signal_connect(G_OBJECT(_graph_window), "expose_event",
-       G_CALLBACK(graph_expose_callback), this);
+  g_signal_connect(G_OBJECT(_graph_window), "draw",
+       G_CALLBACK(graph_draw_callback), this);
   g_signal_connect(G_OBJECT(_graph_window), "configure_event",
        G_CALLBACK(configure_graph_callback), this);
   g_signal_connect(G_OBJECT(_graph_window), "button_press_event",
@@ -90,29 +85,35 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
   g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event",
        G_CALLBACK(motion_notify_event_callback), this);
 
+  // An overlay inside the frame, for charts that want to display widgets on
+  // top of the graph.
+  _graph_overlay = gtk_overlay_new();
+  gtk_container_add(GTK_CONTAINER(_graph_overlay), _graph_window);
+
   // A Frame to hold the graph.
-  GtkWidget *graph_frame = gtk_frame_new(nullptr);
-  gtk_frame_set_shadow_type(GTK_FRAME(graph_frame), GTK_SHADOW_IN);
-  gtk_container_add(GTK_CONTAINER(graph_frame), _graph_window);
+  _graph_frame = gtk_frame_new(nullptr);
+  gtk_frame_set_shadow_type(GTK_FRAME(_graph_frame), GTK_SHADOW_IN);
+  gtk_container_add(GTK_CONTAINER(_graph_frame), _graph_overlay);
 
   // A VBox to hold the graph's frame, and any numbers (scale legend?  total?)
   // above it.
-  _graph_vbox = gtk_vbox_new(FALSE, 0);
-  gtk_box_pack_end(GTK_BOX(_graph_vbox), graph_frame,
+  _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+  gtk_box_pack_end(GTK_BOX(_graph_vbox), _graph_frame,
        TRUE, TRUE, 0);
 
   // An HBox to hold the graph's frame, and the scale legend to the right of
   // it.
-  _graph_hbox = gtk_hbox_new(FALSE, 0);
+  _graph_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
   gtk_box_pack_start(GTK_BOX(_graph_hbox), _graph_vbox,
          TRUE, TRUE, 0);
 
   // An HPaned to hold the label stack and the graph hbox.
-  _hpaned = gtk_hpaned_new();
+  _hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
+  gtk_paned_set_wide_handle(GTK_PANED(_hpaned), TRUE);
   gtk_container_add(GTK_CONTAINER(_window), _hpaned);
   gtk_container_set_border_width(GTK_CONTAINER(_window), 8);
 
-  gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), TRUE, TRUE);
+  gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), FALSE, FALSE);
   gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE);
 
   _drag_mode = DM_none;
@@ -128,13 +129,13 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
 GtkStatsGraph::
 ~GtkStatsGraph() {
   _monitor = nullptr;
-  release_pixmap();
+  release_surface();
 
-  Brushes::iterator bi;
-  for (bi = _brushes.begin(); bi != _brushes.end(); ++bi) {
-    GdkGC *gc = (*bi).second;
-    g_object_unref(gc);
+  for (auto &item : _brushes) {
+    cairo_pattern_destroy(item.second.first);
+    cairo_pattern_destroy(item.second.second);
   }
+  _brushes.clear();
 
   _label_stack.clear_labels();
 
@@ -159,13 +160,6 @@ void GtkStatsGraph::
 new_data(int thread_index, int frame_number) {
 }
 
-/**
- * Called when it is necessary to redraw the entire graph.
- */
-void GtkStatsGraph::
-force_redraw() {
-}
-
 /**
  * Called when the user has resized the window, forcing a resize of the graph.
  */
@@ -214,7 +208,38 @@ user_guide_bars_changed() {
  * Called when the user single-clicks on a label.
  */
 void GtkStatsGraph::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void GtkStatsGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void GtkStatsGraph::
+on_leave_label(int collector_index) {
+  if (collector_index == _highlighted_index && collector_index != -1) {
+    _highlighted_index = -1;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsGraph::
+get_label_tooltip(int collector_index) const {
+  return std::string();
 }
 
 /**
@@ -236,37 +261,37 @@ close() {
 }
 
 /**
- * Returns a GC suitable for drawing in the indicated collector's color.
+ * Returns a pattern suitable for drawing in the indicated collector's color.
  */
-GdkGC *GtkStatsGraph::
-get_collector_gc(int collector_index) {
+cairo_pattern_t *GtkStatsGraph::
+get_collector_pattern(int collector_index, bool highlight) {
   Brushes::iterator bi;
   bi = _brushes.find(collector_index);
   if (bi != _brushes.end()) {
-    return (*bi).second;
+    return highlight ? (*bi).second.second : (*bi).second.first;
   }
 
   // Ask the monitor what color this guy should be.
   LRGBColor rgb = _monitor->get_collector_color(collector_index);
-
-  GdkColor c;
-  c.red = (int)(rgb[0] * 65535.0f);
-  c.green = (int)(rgb[1] * 65535.0f);
-  c.blue = (int)(rgb[2] * 65535.0f);
-  GdkGC *gc = gdk_gc_new(_pixmap);
-  // g_object_ref(gc);   Should this be ref_sink?
-  gdk_gc_set_rgb_fg_color(gc, &c);
-
-  _brushes[collector_index] = gc;
-  return gc;
+  cairo_pattern_t *pattern = cairo_pattern_create_rgb(
+    encode_sRGB_float((float)rgb[0]),
+    encode_sRGB_float((float)rgb[1]),
+    encode_sRGB_float((float)rgb[2]));
+  cairo_pattern_t *hpattern = cairo_pattern_create_rgb(
+    encode_sRGB_float((float)rgb[0] * 0.75f),
+    encode_sRGB_float((float)rgb[1] * 0.75f),
+    encode_sRGB_float((float)rgb[2] * 0.75f));
+
+  _brushes[collector_index] = std::make_pair(pattern, hpattern);
+  return highlight ? hpattern : pattern;
 }
 
 /**
- * This is called during the servicing of expose_event; it gives a derived
+ * This is called during the servicing of the draw event; it gives a derived
  * class opportunity to do some further painting into the graph window.
  */
 void GtkStatsGraph::
-additional_graph_window_paint() {
+additional_graph_window_paint(cairo_t *cr) {
 }
 
 /**
@@ -323,12 +348,14 @@ gboolean GtkStatsGraph::
 handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   _potential_drag_mode = consider_drag_start(graph_x, graph_y);
 
+  GdkWindow *window = gtk_widget_get_window(_window);
+
   if (_potential_drag_mode == DM_guide_bar ||
       _drag_mode == DM_guide_bar) {
-    gdk_window_set_cursor(_window->window, _hand_cursor);
+    gdk_window_set_cursor(window, _hand_cursor);
 
   } else {
-    gdk_window_set_cursor(_window->window, nullptr);
+    gdk_window_set_cursor(window, nullptr);
   }
 
   return TRUE;
@@ -338,30 +365,27 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
  * Sets up a backing-store bitmap of the indicated size.
  */
 void GtkStatsGraph::
-setup_pixmap(int xsize, int ysize) {
-  release_pixmap();
+setup_surface(int xsize, int ysize) {
+  release_surface();
 
-  _pixmap_xsize = std::max(xsize, 0);
-  _pixmap_ysize = std::max(ysize, 0);
+  _surface_xsize = std::max(xsize, 0);
+  _surface_ysize = std::max(ysize, 0);
 
-  _pixmap = gdk_pixmap_new(_graph_window->window, _pixmap_xsize, _pixmap_ysize, -1);
-  // g_object_ref(_pixmap);   Should this be ref_sink?
-  _pixmap_gc = gdk_gc_new(_pixmap);
-  // g_object_ref(_pixmap_gc);    Should this be ref_sink?
+  _cr_surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, _surface_xsize, _surface_ysize);
+  _cr = cairo_create(_cr_surface);
 
-  gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_white);
-  gdk_draw_rectangle(_pixmap, _pixmap_gc, TRUE, 0, 0,
-         _pixmap_xsize, _pixmap_ysize);
+  cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
+  cairo_paint(_cr);
 }
 
 /**
- * Frees the backing-store bitmap created by setup_pixmap().
+ * Frees the backing-store bitmap created by setup_surface().
  */
 void GtkStatsGraph::
-release_pixmap() {
-  if (_pixmap != nullptr) {
-    g_object_unref(_pixmap);
-    g_object_unref(_pixmap_gc);
+release_surface() {
+  if (_cr_surface != nullptr) {
+    cairo_surface_destroy(_cr_surface);
+    cairo_destroy(_cr);
   }
 }
 
@@ -388,17 +412,15 @@ window_destroy(GtkWidget *widget, gpointer data) {
  * Fills in the graph window.
  */
 gboolean GtkStatsGraph::
-graph_expose_callback(GtkWidget *widget, GdkEventExpose *event, gpointer data) {
+graph_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   GtkStatsGraph *self = (GtkStatsGraph *)data;
 
-  if (self->_pixmap != nullptr) {
-    gdk_draw_drawable(self->_graph_window->window,
-          self->_graph_window->style->fg_gc[0],
-          self->_pixmap, 0, 0, 0, 0,
-          self->_pixmap_xsize, self->_pixmap_ysize);
+  if (self->_cr_surface != nullptr) {
+    cairo_set_source_surface(cr, self->_cr_surface, 0, 0);
+    cairo_paint(cr);
   }
 
-  self->additional_graph_window_paint();
+  self->additional_graph_window_paint(cr);
 
   return TRUE;
 }
@@ -412,7 +434,7 @@ configure_graph_callback(GtkWidget *widget, GdkEventConfigure *event,
   GtkStatsGraph *self = (GtkStatsGraph *)data;
 
   self->changed_graph_size(event->width, event->height);
-  self->setup_pixmap(event->width, event->height);
+  self->setup_surface(event->width, event->height);
   self->force_redraw();
 
   return TRUE;

+ 27 - 28
pandatool/src/gtk-stats/gtkStatsGraph.h

@@ -19,6 +19,7 @@
 #include "pmap.h"
 
 #include <gtk/gtk.h>
+#include <cairo.h>
 
 class GtkStatsMonitor;
 
@@ -43,7 +44,7 @@ public:
 
   virtual void new_collector(int collector_index);
   virtual void new_data(int thread_index, int frame_number);
-  virtual void force_redraw();
+  virtual void force_redraw()=0;
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
   virtual void set_time_units(int unit_mask);
@@ -51,13 +52,16 @@ public:
   void set_pause(bool pause);
 
   void user_guide_bars_changed();
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
+  virtual void on_enter_label(int collector_index);
+  virtual void on_leave_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
 
 protected:
   void close();
-  GdkGC *get_collector_gc(int collector_index);
+  cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false);
 
-  virtual void additional_graph_window_paint();
+  virtual void additional_graph_window_paint(cairo_t *cr);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual void set_drag_mode(DragMode drag_mode);
 
@@ -67,13 +71,15 @@ protected:
   virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y);
 
 protected:
-  // Table of GC's for our various collectors.
-  typedef pmap<int, GdkGC *> Brushes;
+  // Table of patterns for our various collectors.
+  typedef pmap<int, std::pair<cairo_pattern_t *, cairo_pattern_t *> > Brushes;
   Brushes _brushes;
 
   GtkStatsMonitor *_monitor;
   GtkWidget *_parent_window;
   GtkWidget *_window;
+  GtkWidget *_graph_frame;
+  GtkWidget *_graph_overlay;
   GtkWidget *_graph_window;
   GtkWidget *_graph_hbox;
   GtkWidget *_graph_vbox;
@@ -83,18 +89,9 @@ protected:
 
   GdkCursor *_hand_cursor;
 
-  GdkPixmap *_pixmap;
-  GdkGC *_pixmap_gc;
-  int _pixmap_xsize, _pixmap_ysize;
-
-  /*
-  COLORREF _dark_color;
-  COLORREF _light_color;
-  COLORREF _user_guide_bar_color;
-  HPEN _dark_pen;
-  HPEN _light_pen;
-  HPEN _user_guide_bar_pen;
-  */
+  cairo_surface_t *_cr_surface;
+  cairo_t *_cr;
+  int _surface_xsize, _surface_ysize;
 
   DragMode _drag_mode;
   DragMode _potential_drag_mode;
@@ -102,25 +99,27 @@ protected:
   double _drag_scale_start;
   int _drag_guide_bar;
 
+  int _highlighted_index = -1;
+
   bool _pause;
 
-  static const GdkColor rgb_white;
-  static const GdkColor rgb_light_gray;
-  static const GdkColor rgb_dark_gray;
-  static const GdkColor rgb_black;
-  static const GdkColor rgb_user_guide_bar;
+  static const double rgb_white[3];
+  static const double rgb_light_gray[3];
+  static const double rgb_dark_gray[3];
+  static const double rgb_black[3];
+  static const double rgb_user_guide_bar[3];
 
 private:
-  void setup_pixmap(int xsize, int ysize);
-  void release_pixmap();
+  void setup_surface(int xsize, int ysize);
+  void release_surface();
 
   static gboolean window_delete_event(GtkWidget *widget, GdkEvent *event,
               gpointer data);
   static void window_destroy(GtkWidget *widget, gpointer data);
-  static gboolean graph_expose_callback(GtkWidget *widget,
-          GdkEventExpose *event, gpointer data);
+  static gboolean graph_draw_callback(GtkWidget *widget,
+              cairo_t *cr, gpointer data);
   static gboolean configure_graph_callback(GtkWidget *widget,
-             GdkEventConfigure *event, gpointer data);
+              GdkEventConfigure *event, gpointer data);
 
 protected:
   static gboolean button_press_event_callback(GtkWidget *widget,

+ 97 - 49
pandatool/src/gtk-stats/gtkStatsLabel.cxx

@@ -14,6 +14,7 @@
 #include "gtkStatsLabel.h"
 #include "gtkStatsMonitor.h"
 #include "gtkStatsGraph.h"
+#include "convert_srgb.h"
 
 int GtkStatsLabel::_left_margin = 2;
 int GtkStatsLabel::_right_margin = 2;
@@ -25,64 +26,61 @@ int GtkStatsLabel::_bottom_margin = 2;
  */
 GtkStatsLabel::
 GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
-              int thread_index, int collector_index, bool use_fullname) :
+              int thread_index, int collector_index, bool use_fullname,
+              bool align_right) :
   _monitor(monitor),
   _graph(graph),
   _thread_index(thread_index),
-  _collector_index(collector_index)
+  _collector_index(collector_index),
+  _align_right(align_right)
 {
-  _widget = nullptr;
-  if (use_fullname) {
-    _text = _monitor->get_client_data()->get_collector_fullname(_collector_index);
-  } else {
-    _text = _monitor->get_client_data()->get_collector_name(_collector_index);
-  }
-
   _widget = gtk_drawing_area_new();
   gtk_widget_add_events(_widget,
       GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
       GDK_BUTTON_PRESS_MASK);
-  g_signal_connect(G_OBJECT(_widget), "expose_event",
-       G_CALLBACK(expose_event_callback), this);
+  g_signal_connect(G_OBJECT(_widget), "draw",
+       G_CALLBACK(draw_callback), this);
   g_signal_connect(G_OBJECT(_widget), "enter_notify_event",
        G_CALLBACK(enter_notify_event_callback), this);
   g_signal_connect(G_OBJECT(_widget), "leave_notify_event",
        G_CALLBACK(leave_notify_event_callback), this);
   g_signal_connect(G_OBJECT(_widget), "button_press_event",
        G_CALLBACK(button_press_event_callback), this);
+  g_signal_connect(G_OBJECT(_widget), "query-tooltip",
+       G_CALLBACK(query_tooltip_callback), this);
 
+  gtk_widget_set_has_tooltip(_widget, TRUE);
   gtk_widget_show(_widget);
 
-  // Make up a PangoLayout to represent the text.
-  _layout = gtk_widget_create_pango_layout(_widget, _text.c_str());
-
   // Set the fg and bg colors on the label.
   LRGBColor rgb = _monitor->get_collector_color(_collector_index);
-  _bg_color.red = (int)(rgb[0] * 65535.0f);
-  _bg_color.green = (int)(rgb[1] * 65535.0f);
-  _bg_color.blue = (int)(rgb[2] * 65535.0f);
+  _bg_color = LRGBColor(
+    encode_sRGB_float((float)rgb[0]),
+    encode_sRGB_float((float)rgb[1]),
+    encode_sRGB_float((float)rgb[2]));
 
-  // Should our foreground be black or white?
-  double bright =
-    rgb[0] * 0.299 +
-    rgb[1] * 0.587 +
-    rgb[2] * 0.114;
+  _highlight_bg_color = LRGBColor(
+    encode_sRGB_float((float)rgb[0] * 0.75f),
+    encode_sRGB_float((float)rgb[1] * 0.75f),
+    encode_sRGB_float((float)rgb[2] * 0.75f));
 
+  // Should our foreground be black or white?
+  PN_stdfloat bright = _bg_color.dot(LRGBColor(0.2126, 0.7152, 0.0722));
   if (bright >= 0.5) {
-    _fg_color.red = _fg_color.green = _fg_color.blue = 0;
+    _fg_color = LRGBColor(0);
   } else {
-    _fg_color.red = _fg_color.green = _fg_color.blue = 0xffff;
+    _fg_color = LRGBColor(1);
+  }
+  if (bright >= 0.5 * 0.75) {
+    _highlight_fg_color = LRGBColor(0);
+  } else {
+    _highlight_fg_color = LRGBColor(1);
   }
-
-  // What are the extents of the text?  This determines the minimum size of
-  // our widget.
-  int width, height;
-  pango_layout_get_pixel_size(_layout, &width, &height);
-  gtk_widget_set_size_request(_widget, width + 8, height);
 
   _highlight = false;
   _mouse_within = false;
-  _height = height;
+
+  update_text(use_fullname);
 }
 
 /**
@@ -90,7 +88,10 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
  */
 GtkStatsLabel::
 ~GtkStatsLabel() {
-  // DeleteObject(_bg_brush);
+  if (_layout) {
+    g_object_unref(_layout);
+    _layout = nullptr;
+  }
 }
 
 /**
@@ -144,6 +145,33 @@ get_highlight() const {
   return _highlight;
 }
 
+/**
+ * Set to true if the full name of the collector should be shown.
+ */
+void GtkStatsLabel::
+update_text(bool use_fullname) {
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (use_fullname) {
+    _text = client_data->get_collector_fullname(_collector_index);
+  } else {
+    _text = client_data->get_collector_name(_collector_index);
+  }
+
+  // Make up a PangoLayout to represent the text.
+  if (_layout) {
+    g_object_unref(_layout);
+  }
+  _layout = gtk_widget_create_pango_layout(_widget, _text.c_str());
+
+  // What are the extents of the text?  This determines the minimum size of
+  // our widget.
+  int width, height;
+  pango_layout_get_pixel_size(_layout, &width, &height);
+  gtk_widget_set_size_request(_widget, width + 8, height);
+  _ideal_width = width;
+  _height = height;
+}
+
 /**
  * Used internally to indicate whether the mouse is within the label's widget.
  */
@@ -159,31 +187,37 @@ set_mouse_within(bool mouse_within) {
  * Draws the background color of the label.
  */
 gboolean GtkStatsLabel::
-expose_event_callback(GtkWidget *widget, GdkEventExpose *event, gpointer data) {
+draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   GtkStatsLabel *self = (GtkStatsLabel *)data;
 
-  GdkGC *gc = gdk_gc_new(widget->window);
-  gdk_gc_set_rgb_fg_color(gc, &self->_bg_color);
+  LRGBColor bg, fg;
+  if (self->_highlight || self->_mouse_within) {
+    bg = self->_highlight_bg_color;
+    fg = self->_highlight_fg_color;
+  } else {
+    bg = self->_bg_color;
+    fg = self->_fg_color;
 
-  gdk_draw_rectangle(widget->window, gc, TRUE, 0, 0,
-         widget->allocation.width, widget->allocation.height);
+  }
+  cairo_set_source_rgb(cr, bg[0], bg[1], bg[2]);
+
+  GtkAllocation allocation;
+  gtk_widget_get_allocation(widget, &allocation);
+
+  cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
+  cairo_fill(cr);
 
-  // Center the text within the rectangle.
   int width, height;
   pango_layout_get_pixel_size(self->_layout, &width, &height);
 
-  gdk_gc_set_rgb_fg_color(gc, &self->_fg_color);
-  gdk_draw_layout(widget->window, gc,
-      (widget->allocation.width - width) / 2, 0,
-      self->_layout);
-
-  // Now draw the highlight rectangle, if any.
-  if (self->_highlight || self->_mouse_within) {
-    gdk_draw_rectangle(widget->window, gc, FALSE, 0, 0,
-           widget->allocation.width - 1, widget->allocation.height - 1);
+  cairo_set_source_rgb(cr, fg[0], fg[1], fg[2]);
+  if (self->_align_right) {
+    cairo_move_to(cr, allocation.width - width, 0);
+  } else {
+    cairo_move_to(cr, 0, 0);
   }
+  pango_cairo_show_layout(cr, self->_layout);
 
-  g_object_unref(gc);
   return TRUE;
 }
 
@@ -218,7 +252,21 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   bool double_click = (event->type == GDK_2BUTTON_PRESS);
   if (double_click) {
-    self->_graph->clicked_label(self->_collector_index);
+    self->_graph->on_click_label(self->_collector_index);
   }
   return TRUE;
 }
+
+/**
+ * Called when a tooltip should be displayed.
+ */
+gboolean GtkStatsLabel::
+query_tooltip_callback(GtkWidget *widget, gint x, gint y,
+                gboolean keyboard_tip, GtkTooltip *tooltip,
+                gpointer data) {
+  GtkStatsLabel *self = (GtkStatsLabel *)data;
+
+  std::string text = self->_graph->get_label_tooltip(self->_collector_index);
+  gtk_tooltip_set_text(tooltip, text.c_str());
+  return !text.empty();
+}

+ 18 - 14
pandatool/src/gtk-stats/gtkStatsLabel.h

@@ -17,6 +17,7 @@
 #include "pandatoolbase.h"
 
 #include <gtk/gtk.h>
+#include <cairo.h>
 
 class GtkStatsMonitor;
 class GtkStatsGraph;
@@ -29,7 +30,8 @@ class GtkStatsGraph;
 class GtkStatsLabel {
 public:
   GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
-                int thread_index, int collector_index, bool use_fullname);
+                int thread_index, int collector_index, bool use_fullname,
+                bool align_right = true);
   ~GtkStatsLabel();
 
   GtkWidget *get_widget() const;
@@ -41,10 +43,12 @@ public:
   void set_highlight(bool highlight);
   bool get_highlight() const;
 
+  void update_text(bool use_fullname);
+
 private:
   void set_mouse_within(bool mouse_within);
-  static gboolean expose_event_callback(GtkWidget *widget,
-          GdkEventExpose *event, gpointer data);
+  static gboolean draw_callback(GtkWidget *widget,
+                cairo_t *cr, gpointer data);
   static gboolean enter_notify_event_callback(GtkWidget *widget,
                 GdkEventCrossing *event,
                 gpointer data);
@@ -54,6 +58,9 @@ private:
   static gboolean button_press_event_callback(GtkWidget *widget,
                 GdkEventButton *event,
                 gpointer data);
+  static gboolean query_tooltip_callback(GtkWidget *widget, gint x, gint y,
+                gboolean keyboard_tip, GtkTooltip *tooltip,
+                gpointer data);
 
   GtkStatsMonitor *_monitor;
   GtkStatsGraph *_graph;
@@ -61,20 +68,17 @@ private:
   int _collector_index;
   std::string _text;
   GtkWidget *_widget;
-  GdkColor _fg_color;
-  GdkColor _bg_color;
-  PangoLayout *_layout;
-
-  /*
-  COLORREF _bg_color;
-  COLORREF _fg_color;
-  HBRUSH _bg_brush;
-  HBRUSH _highlight_brush;
-  */
+  LRGBColor _fg_color;
+  LRGBColor _highlight_fg_color;
+  LRGBColor _bg_color;
+  LRGBColor _highlight_bg_color;
+  PangoLayout *_layout = nullptr;
 
+  int _height;
+  int _ideal_width;
   bool _highlight;
   bool _mouse_within;
-  int _height;
+  bool _align_right;
 
   static int _left_margin, _right_margin;
   static int _top_margin, _bottom_margin;

+ 6 - 5
pandatool/src/gtk-stats/gtkStatsLabelStack.cxx

@@ -20,7 +20,7 @@
  */
 GtkStatsLabelStack::
 GtkStatsLabelStack() {
-  _widget = gtk_vbox_new(FALSE, 0);
+  _widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
   _highlight_label = -1;
 }
 
@@ -48,9 +48,12 @@ int GtkStatsLabelStack::
 get_label_y(int label_index, GtkWidget *target_widget) const {
   nassertr(label_index >= 0 && label_index < (int)_labels.size(), 0);
 
+  GtkAllocation allocation;
+  gtk_widget_get_allocation(_widget, &allocation);
+
   // Assume all labels have the same height.
   int height = _labels[0]->get_height();
-  int start_y = _widget->allocation.height - height * label_index;
+  int start_y = allocation.height - height * label_index;
 
   int x, y;
   gtk_widget_translate_coordinates(_widget, target_widget,
@@ -81,9 +84,7 @@ get_label_collector_index(int label_index) const {
  */
 void GtkStatsLabelStack::
 clear_labels(bool delete_widgets) {
-  Labels::iterator li;
-  for (li = _labels.begin(); li != _labels.end(); ++li) {
-    GtkStatsLabel *label = (*li);
+  for (GtkStatsLabel *label : _labels) {
     if (delete_widgets) {
       gtk_container_remove(GTK_CONTAINER(_widget), label->get_widget());
     }

+ 145 - 92
pandatool/src/gtk-stats/gtkStatsMonitor.cxx

@@ -17,37 +17,18 @@
 #include "gtkStatsStripChart.h"
 #include "gtkStatsChartMenu.h"
 #include "gtkStatsPianoRoll.h"
+#include "gtkStatsFlameGraph.h"
 #include "gtkStatsMenuId.h"
 #include "pStatGraph.h"
 #include "pStatCollectorDef.h"
 #include "indent.h"
 
-typedef void vc();
-
-GtkItemFactoryEntry GtkStatsMonitor::menu_entries[] = {
-  { (gchar *)"/Options", nullptr, nullptr, 0, (gchar *)"<Branch>" },
-  { (gchar *)"/Options/Units", nullptr, nullptr, 0, (gchar *)"<Branch>" },
-  { (gchar *)"/Options/Units/ms", nullptr, (vc *)&handle_menu_command, MI_time_ms, (gchar *)"<RadioItem>" },
-  { (gchar *)"/Options/Units/Hz", nullptr, (vc *)&handle_menu_command, MI_time_hz, (gchar *)"/Options/Units/ms" },
-  { (gchar *)"/Speed", nullptr, nullptr, 0, (gchar *)"<Branch>" },
-  { (gchar *)"/Speed/1", nullptr, (vc *)&handle_menu_command, MI_speed_1, (gchar *)"<RadioItem>" },
-  { (gchar *)"/Speed/2", nullptr, (vc *)&handle_menu_command, MI_speed_2, (gchar *)"/Speed/1" },
-  { (gchar *)"/Speed/3", nullptr, (vc *)&handle_menu_command, MI_speed_3, (gchar *)"/Speed/1" },
-  { (gchar *)"/Speed/6", nullptr, (vc *)&handle_menu_command, MI_speed_6, (gchar *)"/Speed/1" },
-  { (gchar *)"/Speed/12", nullptr, (vc *)&handle_menu_command, MI_speed_12, (gchar *)"/Speed/1" },
-  { (gchar *)"/Speed/sep", nullptr, nullptr, 0, (gchar *)"<Separator>" },
-  { (gchar *)"/Speed/pause", nullptr, (vc *)&handle_menu_command, MI_pause, (gchar *)"<CheckItem>" },
-};
-
-int GtkStatsMonitor::num_menu_entries = sizeof(menu_entries) / sizeof(GtkItemFactoryEntry);
-
 /**
  *
  */
 GtkStatsMonitor::
 GtkStatsMonitor(GtkStatsServer *server) : PStatMonitor(server) {
   _window = nullptr;
-  _item_factory = nullptr;
 
   // These will be filled in later when the menu is created.
   _time_units = 0;
@@ -160,8 +141,7 @@ new_collector(int collector_index) {
 void GtkStatsMonitor::
 new_thread(int thread_index) {
   GtkStatsChartMenu *chart_menu = new GtkStatsChartMenu(this, thread_index);
-  GtkWidget *menu_bar = gtk_item_factory_get_widget(_item_factory, "<PStats>");
-  chart_menu->add_to_menu_bar(menu_bar, _next_chart_index);
+  chart_menu->add_to_menu_bar(_menu_bar, _next_chart_index);
   ++_next_chart_index;
   _chart_menus.push_back(chart_menu);
 }
@@ -181,7 +161,6 @@ new_data(int thread_index, int frame_number) {
   }
 }
 
-
 /**
  * Called whenever the connection to the client has been lost.  This is a
  * permanent state change.  The monitor should update its display to represent
@@ -273,6 +252,19 @@ open_piano_roll(int thread_index) {
   graph->set_pause(_pause);
 }
 
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+void GtkStatsMonitor::
+open_flame_graph(int thread_index) {
+  GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index);
+  add_graph(graph);
+
+  graph->set_time_units(_time_units);
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
+}
+
 /**
  * Adds a new MenuDef to the monitor, or returns an existing one if there is
  * already one just like it.
@@ -379,40 +371,27 @@ create_window() {
   gtk_window_set_default_size(GTK_WINDOW(_window), 500, 360);
 
   // Set up the menu.
-  GtkAccelGroup *accel_group = gtk_accel_group_new();
-  _item_factory =
-    gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<PStats>", accel_group);
-  gtk_item_factory_create_items(_item_factory, num_menu_entries, menu_entries,
-        this);
+   GtkAccelGroup *accel_group = gtk_accel_group_new();
   gtk_window_add_accel_group(GTK_WINDOW(_window), accel_group);
-  GtkWidget *menu_bar = gtk_item_factory_get_widget(_item_factory, "<PStats>");
+  _menu_bar = gtk_menu_bar_new();
   _next_chart_index = 2;
 
+  setup_options_menu();
+  setup_speed_menu();
   setup_frame_rate_label();
 
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    (*mi)->add_to_menu_bar(menu_bar, _next_chart_index);
+  for (GtkStatsChartMenu *chart_menu : _chart_menus) {
+    chart_menu->add_to_menu_bar(_menu_bar, _next_chart_index);
     ++_next_chart_index;
   }
 
   // Pack the menu into the window.
-  GtkWidget *main_vbox = gtk_vbox_new(FALSE, 1);
+  GtkWidget *main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
   gtk_container_add(GTK_CONTAINER(_window), main_vbox);
-  gtk_box_pack_start(GTK_BOX(main_vbox), menu_bar, FALSE, TRUE, 0);
-
-  gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gtk_item_factory_get_item(_item_factory, "/Speed/3")),
-         TRUE);
-  set_scroll_speed(3);
-
-  gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(gtk_item_factory_get_item(_item_factory, "/Options/Units/ms")),
-         TRUE);
-  set_time_units(PStatGraph::GBU_ms);
+  gtk_box_pack_start(GTK_BOX(main_vbox), _menu_bar, FALSE, TRUE, 0);
 
   gtk_widget_show_all(_window);
   gtk_widget_show(_window);
-
-  set_pause(false);
 }
 
 /**
@@ -462,6 +441,126 @@ window_destroy(GtkWidget *widget, gpointer data) {
   self->close();
 }
 
+
+/**
+ * Creates the "Options" pulldown menu.
+ */
+void GtkStatsMonitor::
+setup_options_menu() {
+  _options_menu = gtk_menu_new();
+
+  GtkWidget *item = gtk_menu_item_new_with_label("Options");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), _options_menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), item);
+
+  GtkWidget *units_menu = gtk_menu_new();
+  item = gtk_menu_item_new_with_label("Units");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), units_menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_options_menu), item);
+
+  item = gtk_radio_menu_item_new_with_label(nullptr, "ms");
+  gtk_menu_shell_append(GTK_MENU_SHELL(units_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+      self->set_time_units(PStatGraph::GBU_ms);
+    }), this);
+
+  item = gtk_radio_menu_item_new_with_label(
+    gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item)), "Hz");
+  gtk_menu_shell_append(GTK_MENU_SHELL(units_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+      self->set_time_units(PStatGraph::GBU_hz);
+    }), this);
+
+  set_time_units(PStatGraph::GBU_ms);
+}
+
+/**
+ * Creates the "Speed" pulldown menu.
+ */
+void GtkStatsMonitor::
+setup_speed_menu() {
+  _speed_menu = gtk_menu_new();
+
+  GtkWidget *item = gtk_menu_item_new_with_label("Speed");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), _speed_menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), item);
+
+  GSList *group = nullptr;
+  item = gtk_radio_menu_item_new_with_label(group, "1");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  g_signal_connect(G_OBJECT(item), "toggled",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
+        GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+        self->set_scroll_speed(1);
+      }
+    }), this);
+  group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
+
+  item = gtk_radio_menu_item_new_with_label(group, "2");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  g_signal_connect(G_OBJECT(item), "toggled",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
+        GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+        self->set_scroll_speed(2);
+      }
+    }), this);
+  group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
+
+  item = gtk_radio_menu_item_new_with_label(group, "3");
+  gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  g_signal_connect(G_OBJECT(item), "toggled",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
+        GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+        self->set_scroll_speed(3);
+      }
+    }), this);
+  group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
+
+  item = gtk_radio_menu_item_new_with_label(group, "6");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  g_signal_connect(G_OBJECT(item), "toggled",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
+        GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+        self->set_scroll_speed(6);
+      }
+    }), this);
+  group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
+
+  item = gtk_radio_menu_item_new_with_label(group, "12");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  g_signal_connect(G_OBJECT(item), "toggled",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
+        GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+        self->set_scroll_speed(12);
+      }
+    }), this);
+  group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
+
+  item = gtk_separator_menu_item_new();
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+
+  item = gtk_check_menu_item_new_with_label("pause");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  g_signal_connect(G_OBJECT(item), "toggled",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsMonitor *self = (GtkStatsMonitor *)data;
+      self->set_pause(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item)));
+    }), this);
+
+  set_scroll_speed(3);
+  set_pause(false);
+}
+
 /**
  * Creates the frame rate label on the right end of the menu bar.  This is
  * used as a text label to display the main thread's frame rate to the user,
@@ -470,59 +569,13 @@ window_destroy(GtkWidget *widget, gpointer data) {
  */
 void GtkStatsMonitor::
 setup_frame_rate_label() {
-  GtkWidget *menu_bar = gtk_item_factory_get_widget(_item_factory, "<PStats>");
-
   _frame_rate_menu_item = gtk_menu_item_new();
   _frame_rate_label = gtk_label_new("");
   gtk_container_add(GTK_CONTAINER(_frame_rate_menu_item), _frame_rate_label);
 
   gtk_widget_show(_frame_rate_menu_item);
   gtk_widget_show(_frame_rate_label);
-  gtk_menu_item_right_justify(GTK_MENU_ITEM(_frame_rate_menu_item));
-
-  gtk_menu_shell_append(GTK_MENU_SHELL(menu_bar), _frame_rate_menu_item);
-}
+  gtk_menu_item_set_right_justified(GTK_MENU_ITEM(_frame_rate_menu_item), TRUE);
 
-/**
- *
- */
-void GtkStatsMonitor::
-handle_menu_command(gpointer callback_data, guint menu_id, GtkWidget *widget) {
-  GtkStatsMonitor *self = (GtkStatsMonitor *)callback_data;
-  switch (menu_id) {
-  case MI_none:
-    break;
-
-  case MI_time_ms:
-    self->set_time_units(PStatGraph::GBU_ms);
-    break;
-
-  case MI_time_hz:
-    self->set_time_units(PStatGraph::GBU_hz);
-    break;
-
-  case MI_speed_1:
-    self->set_scroll_speed(1);
-    break;
-
-  case MI_speed_2:
-    self->set_scroll_speed(2);
-    break;
-
-  case MI_speed_3:
-    self->set_scroll_speed(3);
-    break;
-
-  case MI_speed_6:
-    self->set_scroll_speed(6);
-    break;
-
-  case MI_speed_12:
-    self->set_scroll_speed(12);
-    break;
-
-  case MI_pause:
-    self->set_pause(gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)));
-    break;
-  }
+  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), _frame_rate_menu_item);
 }

+ 6 - 6
pandatool/src/gtk-stats/gtkStatsMonitor.h

@@ -66,6 +66,7 @@ public:
   GtkWidget *get_window() const;
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_piano_roll(int thread_index);
+  void open_flame_graph(int thread_index);
 
   const MenuDef *add_menu(const MenuDef &menu_def);
 
@@ -82,10 +83,10 @@ private:
   static gboolean window_delete_event(GtkWidget *widget, GdkEvent *event,
               gpointer data);
   static void window_destroy(GtkWidget *widget, gpointer data);
+  void setup_options_menu();
+  void setup_speed_menu();
   void setup_frame_rate_label();
 
-  static void handle_menu_command(gpointer callback_data, guint menu_id, GtkWidget *widget);
-
   typedef pset<GtkStatsGraph *> Graphs;
   Graphs _graphs;
 
@@ -96,7 +97,9 @@ private:
   Menus _menus;
 
   GtkWidget *_window;
-  GtkItemFactory *_item_factory;
+  GtkWidget *_menu_bar;
+  GtkWidget *_options_menu;
+  GtkWidget *_speed_menu;
   int _next_chart_index;
   GtkWidget *_frame_rate_menu_item;
   GtkWidget *_frame_rate_label;
@@ -105,9 +108,6 @@ private:
   double _scroll_speed;
   bool _pause;
 
-  static GtkItemFactoryEntry menu_entries[];
-  static int num_menu_entries;
-
   friend class GtkStatsGraph;
 };
 

+ 59 - 41
pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx

@@ -16,7 +16,7 @@
 #include "numeric_types.h"
 #include "gtkStatsLabelStack.h"
 
-static const int default_piano_roll_width = 400;
+static const int default_piano_roll_width = 600;
 static const int default_piano_roll_height = 200;
 
 /**
@@ -35,12 +35,19 @@ GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) :
   // Add a DrawingArea widget on top of the graph, to display all of the scale
   // units.
   _scale_area = gtk_drawing_area_new();
-  g_signal_connect(G_OBJECT(_scale_area), "expose_event",
-       G_CALLBACK(expose_event_callback), this);
+  g_signal_connect(G_OBJECT(_scale_area), "draw",
+       G_CALLBACK(draw_callback), this);
   gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area,
          FALSE, FALSE, 0);
-  gtk_widget_set_size_request(_scale_area, 0, 20);
 
+  // It should be large enough to display the labels.
+  {
+    PangoLayout *layout = gtk_widget_create_pango_layout(_window, "0123456789 ms");
+    int width, height;
+    pango_layout_get_pixel_size(layout, &width, &height);
+    gtk_widget_set_size_request(_scale_area, 0, height + 1);
+    g_object_unref(layout);
+  }
 
   gtk_widget_set_size_request(_graph_window, default_piano_roll_width,
             default_piano_roll_height);
@@ -119,7 +126,7 @@ set_time_units(int unit_mask) {
  * Called when the user single-clicks on a label.
  */
 void GtkStatsPianoRoll::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
   if (collector_index >= 0) {
     GtkStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false);
   }
@@ -142,9 +149,8 @@ set_horizontal_scale(double time_width) {
  */
 void GtkStatsPianoRoll::
 clear_region() {
-  gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_white);
-  gdk_draw_rectangle(_pixmap, _pixmap_gc, TRUE, 0, 0,
-         get_xsize(), get_ysize());
+  cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
+  cairo_paint(_cr);
 }
 
 /**
@@ -157,10 +163,22 @@ begin_draw() {
   // Draw in the guide bars.
   int num_guide_bars = get_num_guide_bars();
   for (int i = 0; i < num_guide_bars; i++) {
-    draw_guide_bar(_pixmap, get_guide_bar(i));
+    draw_guide_bar(_cr, get_guide_bar(i));
   }
 }
 
+/**
+ * Should be overridden by the user class.  This hook will be called before
+ * drawing any one row of bars.  These bars correspond to the collector whose
+ * index is get_row_collector(row), and in the color get_row_color(row).
+ */
+void GtkStatsPianoRoll::
+begin_row(int row) {
+  int collector_index = get_label_collector(row);
+  cairo_set_source(_cr, get_collector_pattern(collector_index,
+    _highlighted_index == collector_index));
+}
+
 /**
  * Draws a single bar on the chart.
  */
@@ -170,12 +188,8 @@ draw_bar(int row, int from_x, int to_x) {
     int y = _label_stack.get_label_y(row, _graph_window);
     int height = _label_stack.get_label_height(row);
 
-    int collector_index = get_label_collector(row);
-    GdkGC *gc = get_collector_gc(collector_index);
-
-    gdk_draw_rectangle(_pixmap, gc, TRUE,
-           from_x, y - height + 2,
-           to_x - from_x, height - 4);
+    cairo_rectangle(_cr, from_x, y - height + 2, to_x - from_x, height - 4);
+    cairo_fill(_cr);
   }
 }
 
@@ -199,14 +213,14 @@ idle() {
 }
 
 /**
- * This is called during the servicing of expose_event; it gives a derived
+ * This is called during the servicing of the draw event; it gives a derived
  * class opportunity to do some further painting into the graph window.
  */
 void GtkStatsPianoRoll::
-additional_graph_window_paint() {
+additional_graph_window_paint(cairo_t *cr) {
   int num_user_guide_bars = get_num_user_guide_bars();
   for (int i = 0; i < num_user_guide_bars; i++) {
-    draw_guide_bar(_graph_window->window, get_user_guide_bar(i));
+    draw_guide_bar(cr, get_user_guide_bar(i));
   }
 }
 
@@ -247,7 +261,7 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
   if (double_click) {
     // Double-clicking on a color bar in the graph is the same as double-
     // clicking on the corresponding label.
-    clicked_label(get_collector_under_pixel(graph_x, graph_y));
+    on_click_label(get_collector_under_pixel(graph_x, graph_y));
     return TRUE;
   }
 
@@ -299,7 +313,9 @@ gboolean GtkStatsPianoRoll::
 handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
     // When the mouse is over a color bar, highlight it.
-    _label_stack.highlight_label(get_collector_under_pixel(graph_x, graph_y));
+    int collector_index = get_collector_under_pixel(graph_x, graph_y);
+    _label_stack.highlight_label(collector_index);
+    on_enter_label(collector_index);
 
     /*
     // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
@@ -316,6 +332,7 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   } else {
     // If the mouse is in some drag mode, stop highlighting.
     _label_stack.highlight_label(-1);
+    on_leave_label(_highlighted_index);
   }
 
   if (_drag_mode == DM_scale) {
@@ -380,42 +397,44 @@ update_labels() {
  * Draws the line for the indicated guide bar on the graph.
  */
 void GtkStatsPianoRoll::
-draw_guide_bar(GdkDrawable *surface, const PStatGraph::GuideBar &bar) {
+draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) {
   int x = height_to_pixel(bar._height);
 
   if (x > 0 && x < get_xsize() - 1) {
     // Only draw it if it's not too close to the top.
     switch (bar._style) {
     case GBS_target:
-      gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_light_gray);
+      cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
       break;
 
     case GBS_user:
-      gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_user_guide_bar);
+      cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       break;
 
     case GBS_normal:
-      gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_dark_gray);
+      cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       break;
     }
-    gdk_draw_line(surface, _pixmap_gc, x, 0, x, get_ysize());
+    cairo_move_to(cr, x, 0);
+    cairo_line_to(cr, x, get_ysize());
+    cairo_stroke(cr);
   }
 }
 
 /**
- * This is called during the servicing of expose_event.
+ * This is called during the servicing of the draw event.
  */
 void GtkStatsPianoRoll::
-draw_guide_labels() {
+draw_guide_labels(cairo_t *cr) {
   int i;
   int num_guide_bars = get_num_guide_bars();
   for (i = 0; i < num_guide_bars; i++) {
-    draw_guide_label(get_guide_bar(i));
+    draw_guide_label(cr, get_guide_bar(i));
   }
 
   int num_user_guide_bars = get_num_user_guide_bars();
   for (i = 0; i < num_user_guide_bars; i++) {
-    draw_guide_label(get_user_guide_bar(i));
+    draw_guide_label(cr, get_user_guide_bar(i));
   }
 }
 
@@ -423,20 +442,18 @@ draw_guide_labels() {
  * Draws the text for the indicated guide bar label at the top of the graph.
  */
 void GtkStatsPianoRoll::
-draw_guide_label(const PStatGraph::GuideBar &bar) {
-  GdkGC *gc = gdk_gc_new(_scale_area->window);
-
+draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
   switch (bar._style) {
   case GBS_target:
-    gdk_gc_set_rgb_fg_color(gc, &rgb_light_gray);
+    cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
     break;
 
   case GBS_user:
-    gdk_gc_set_rgb_fg_color(gc, &rgb_user_guide_bar);
+    cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     break;
 
   case GBS_normal:
-    gdk_gc_set_rgb_fg_color(gc, &rgb_dark_gray);
+    cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     break;
   }
 
@@ -453,7 +470,6 @@ draw_guide_label(const PStatGraph::GuideBar &bar) {
     if (find_user_guide_bar(from_height, to_height) >= 0) {
       // Omit the label: there's a user-defined guide bar in the same space.
       g_object_unref(layout);
-      g_object_unref(gc);
       return;
     }
   }
@@ -467,22 +483,24 @@ draw_guide_label(const PStatGraph::GuideBar &bar) {
              x, 0,
              &x, &junk_y);
 
+    GtkAllocation allocation;
+    gtk_widget_get_allocation(_scale_area, &allocation);
+
     int this_x = x - width / 2;
-    gdk_draw_layout(_scale_area->window, gc, this_x,
-        _scale_area->allocation.height - height, layout);
+    cairo_move_to(cr, this_x, allocation.height - height);
+    pango_cairo_show_layout(cr, layout);
   }
 
   g_object_unref(layout);
-  g_object_unref(gc);
 }
 
 /**
  * Draws in the scale labels.
  */
 gboolean GtkStatsPianoRoll::
-expose_event_callback(GtkWidget *widget, GdkEventExpose *event, gpointer data) {
+draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   GtkStatsPianoRoll *self = (GtkStatsPianoRoll *)data;
-  self->draw_guide_labels();
+  self->draw_guide_labels(cr);
 
   return TRUE;
 }

+ 7 - 7
pandatool/src/gtk-stats/gtkStatsPianoRoll.h

@@ -38,17 +38,18 @@ public:
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
   virtual void set_time_units(int unit_mask);
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
   void set_horizontal_scale(double time_width);
 
 protected:
   void clear_region();
   virtual void begin_draw();
+  virtual void begin_row(int row);
   virtual void draw_bar(int row, int from_x, int to_x);
   virtual void end_draw();
   virtual void idle();
 
-  virtual void additional_graph_window_paint();
+  virtual void additional_graph_window_paint(cairo_t *cr);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
 
   virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
@@ -59,12 +60,11 @@ protected:
 private:
   int get_collector_under_pixel(int xpoint, int ypoint);
   void update_labels();
-  void draw_guide_bar(GdkDrawable *surface, const PStatGraph::GuideBar &bar);
-  void draw_guide_labels();
-  void draw_guide_label(const PStatGraph::GuideBar &bar);
+  void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar);
+  void draw_guide_labels(cairo_t *cr);
+  void draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar);
 
-  static gboolean expose_event_callback(GtkWidget *widget,
-          GdkEventExpose *event, gpointer data);
+  static gboolean draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data);
 };
 
 #endif

+ 106 - 83
pandatool/src/gtk-stats/gtkStatsStripChart.cxx

@@ -33,8 +33,6 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
                   default_strip_chart_height),
   GtkStatsGraph(monitor)
 {
-  _brush_origin = 0;
-
   if (show_level) {
     // If it's a level-type graph, show the appropriate units.
     if (_unit_name.empty()) {
@@ -49,7 +47,7 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
   }
 
   // Put some stuff on top of the graph.
-  _top_hbox = gtk_hbox_new(FALSE, 0);
+  _top_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
   gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox,
          FALSE, FALSE, 0);
 
@@ -66,12 +64,19 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
   // Add a DrawingArea widget to the right of the graph, to display all of the
   // scale units.
   _scale_area = gtk_drawing_area_new();
-  g_signal_connect(G_OBJECT(_scale_area), "expose_event",
-       G_CALLBACK(expose_event_callback), this);
+  g_signal_connect(G_OBJECT(_scale_area), "draw",
+       G_CALLBACK(draw_callback), this);
   gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area,
          FALSE, FALSE, 0);
-  gtk_widget_set_size_request(_scale_area, 40, 0);
 
+  // Make it wide enough to display a typical label.
+  {
+    PangoLayout *layout = gtk_widget_create_pango_layout(_window, "99 ms");
+    int width, height;
+    pango_layout_get_pixel_size(layout, &width, &height);
+    gtk_widget_set_size_request(_scale_area, width, 0);
+    g_object_unref(layout);
+  }
 
   gtk_widget_set_size_request(_graph_window, default_strip_chart_width,
             default_strip_chart_height);
@@ -177,7 +182,7 @@ set_scroll_speed(double scroll_speed) {
  * Called when the user single-clicks on a label.
  */
 void GtkStatsStripChart::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
   if (collector_index < 0) {
     // Clicking on whitespace in the graph is the same as clicking on the top
     // label.
@@ -204,6 +209,15 @@ clicked_label(int collector_index) {
   }
 }
 
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsStripChart::
+get_label_tooltip(int collector_index) const {
+  return PStatStripChart::get_label_tooltip(collector_index);
+}
+
 /**
  * Changes the value the height of the vertical axis represents.  This may
  * force a redraw.
@@ -236,9 +250,8 @@ update_labels() {
  */
 void GtkStatsStripChart::
 clear_region() {
-  gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_white);
-  gdk_draw_rectangle(_pixmap, _pixmap_gc, TRUE, 0, 0,
-         get_xsize(), get_ysize());
+  cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
+  cairo_paint(_cr);
 }
 
 /**
@@ -247,18 +260,29 @@ clear_region() {
  */
 void GtkStatsStripChart::
 copy_region(int start_x, int end_x, int dest_x) {
-  gdk_draw_drawable(_pixmap, _pixmap_gc, _pixmap,
-        start_x, 0, dest_x, 0,
-        end_x - start_x, get_ysize());
+  // We are not allowed to copy a surface onto itself, so we have to create a
+  // temporary surface to copy to.
+  end_x = std::min(end_x, get_xsize());
+  cairo_surface_t *temp_surface =
+    cairo_image_surface_create(CAIRO_FORMAT_RGB24, end_x - start_x, get_ysize());
+  {
+    cairo_t *temp_cr = cairo_create(temp_surface);
+    cairo_set_source_surface(temp_cr, _cr_surface, -start_x, 0);
+    cairo_paint(temp_cr);
+    cairo_destroy(temp_cr);
+  }
+
+  cairo_set_source_surface(_cr, temp_surface, 0, 0);
+  cairo_rectangle(_cr, dest_x, 0, end_x - start_x, get_ysize());
+  cairo_fill(_cr);
 
-  // Also shift the brush origin over, so we still get proper dithering.
-  _brush_origin += (dest_x - start_x);
-  // SetBrushOrgEx(_bitmap_dc, _brush_origin, 0, NULL);
+  cairo_surface_destroy(temp_surface);
 
+  GdkWindow *window = gtk_widget_get_window(_graph_window);
   GdkRectangle rect = {
     dest_x, 0, end_x - start_x, get_ysize()
   };
-  gdk_window_invalidate_rect(_graph_window->window, &rect, FALSE);
+  gdk_window_invalidate_rect(window, &rect, FALSE);
 }
 
 /**
@@ -268,9 +292,9 @@ copy_region(int start_x, int end_x, int dest_x) {
 void GtkStatsStripChart::
 draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) {
   // Start by clearing the band first.
-  gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_white);
-  gdk_draw_rectangle(_pixmap, _pixmap_gc, TRUE, x, 0,
-         w + 1, get_ysize());
+  cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
+  cairo_rectangle(_cr, x, 0, w, get_ysize());
+  cairo_fill(_cr);
 
   double overall_time = 0.0;
   int y = get_ysize();
@@ -279,18 +303,21 @@ draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) {
   for (fi = fdata.begin(); fi != fdata.end(); ++fi) {
     const ColorData &cd = (*fi);
     overall_time += cd._net_value;
-    GdkGC *gc = get_collector_gc(cd._collector_index);
+    cairo_set_source(_cr, get_collector_pattern(cd._collector_index,
+      _highlighted_index == cd._collector_index));
 
     if (overall_time > get_vertical_scale()) {
       // Off the top.  Go ahead and clamp it by hand, in case it's so far off
       // the top we'd overflow the 16-bit pixel value.
-      gdk_draw_rectangle(_pixmap, gc, TRUE, x, 0, w, y);
+      cairo_rectangle(_cr, x, 0, w, y);
+      cairo_fill(_cr);
       // And we can consider ourselves done now.
       return;
     }
 
     int top_y = height_to_pixel(overall_time);
-    gdk_draw_rectangle(_pixmap, gc, TRUE, x, top_y, w, y - top_y);
+    cairo_rectangle(_cr, x, top_y, w, y - top_y);
+    cairo_fill(_cr);
     y = top_y;
   }
 }
@@ -300,9 +327,8 @@ draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) {
  */
 void GtkStatsStripChart::
 draw_empty(int x, int w) {
-  gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_white);
-  gdk_draw_rectangle(_pixmap, _pixmap_gc, TRUE, x, 0,
-         w + 1, get_ysize());
+  cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
+  cairo_rectangle(_cr, x, 0, w, get_ysize());
 }
 
 /**
@@ -310,8 +336,10 @@ draw_empty(int x, int w) {
  */
 void GtkStatsStripChart::
 draw_cursor(int x) {
-  gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_black);
-  gdk_draw_line(_pixmap, _pixmap_gc, x, 0, x, get_ysize());
+  cairo_set_source_rgb(_cr, 0.0, 0.0, 0.0);
+  cairo_move_to(_cr, x, 0);
+  cairo_line_to(_cr, x, get_ysize());
+  cairo_stroke(_cr);
 }
 
 /**
@@ -324,24 +352,25 @@ end_draw(int from_x, int to_x) {
   // Draw in the guide bars.
   int num_guide_bars = get_num_guide_bars();
   for (int i = 0; i < num_guide_bars; i++) {
-    draw_guide_bar(_pixmap, from_x, to_x, get_guide_bar(i));
+    draw_guide_bar(_cr, from_x, to_x, get_guide_bar(i));
   }
 
+  GdkWindow *window = gtk_widget_get_window(_graph_window);
   GdkRectangle rect = {
-    from_x, 0, to_x - from_x + 1, get_ysize()
+    from_x, 0, to_x - from_x, get_ysize()
   };
-  gdk_window_invalidate_rect(_graph_window->window, &rect, FALSE);
+  gdk_window_invalidate_rect(window, &rect, FALSE);
 }
 
 /**
- * This is called during the servicing of expose_event; it gives a derived
+ * This is called during the servicing of the draw event; it gives a derived
  * class opportunity to do some further painting into the graph window.
  */
 void GtkStatsStripChart::
-additional_graph_window_paint() {
+additional_graph_window_paint(cairo_t *cr) {
   int num_user_guide_bars = get_num_user_guide_bars();
   for (int i = 0; i < num_user_guide_bars; i++) {
-    draw_guide_bar(_graph_window->window, 0, get_xsize(), get_user_guide_bar(i));
+    draw_guide_bar(cr, 0, get_xsize(), get_user_guide_bar(i));
   }
 }
 
@@ -402,20 +431,23 @@ set_drag_mode(GtkStatsGraph::DragMode drag_mode) {
 gboolean GtkStatsStripChart::
 handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
         bool double_click) {
-  if (double_click) {
-    // Double-clicking on a color bar in the graph is the same as double-
-    // clicking on the corresponding label.
-    clicked_label(get_collector_under_pixel(graph_x, graph_y));
-    return TRUE;
-  }
+  if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+    if (double_click) {
+      // Double-clicking on a color bar in the graph is the same as double-
+      // clicking on the corresponding label.
+      on_click_label(get_collector_under_pixel(graph_x, graph_y));
+      return TRUE;
+    }
 
-  if (_potential_drag_mode == DM_none) {
-    set_drag_mode(DM_scale);
-    _drag_scale_start = pixel_to_height(graph_y);
-    // SetCapture(_graph_window);
-    return TRUE;
+    if (_potential_drag_mode == DM_none) {
+      set_drag_mode(DM_scale);
+      _drag_scale_start = pixel_to_height(graph_y);
+      // SetCapture(_graph_window);
+      return TRUE;
+    }
+  }
 
-  } else if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+  if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
     set_drag_mode(DM_guide_bar);
     _drag_start_y = graph_y;
     // SetCapture(_graph_window);
@@ -455,25 +487,17 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
  */
 gboolean GtkStatsStripChart::
 handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
-  if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
+  if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
+      graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
     // When the mouse is over a color bar, highlight it.
-    _label_stack.highlight_label(get_collector_under_pixel(graph_x, graph_y));
-
-    /*
-    // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
-    // window.
-    TRACKMOUSEEVENT tme = {
-      sizeof(TRACKMOUSEEVENT),
-      TME_LEAVE,
-      _graph_window,
-      0
-    };
-    TrackMouseEvent(&tme);
-    */
-
-  } else {
+    int collector_index = get_collector_under_pixel(graph_x, graph_y);
+    _label_stack.highlight_label(collector_index);
+    on_enter_label(collector_index);
+  }
+  else {
     // If the mouse is in some drag mode, stop highlighting.
     _label_stack.highlight_label(-1);
+    on_leave_label(_highlighted_index);
   }
 
   if (_drag_mode == DM_scale) {
@@ -504,7 +528,7 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
  * Draws the line for the indicated guide bar on the graph.
  */
 void GtkStatsStripChart::
-draw_guide_bar(GdkDrawable *surface, int from_x, int to_x,
+draw_guide_bar(cairo_t *cr, int from_x, int to_x,
                const PStatGraph::GuideBar &bar) {
   int y = height_to_pixel(bar._height);
 
@@ -512,42 +536,44 @@ draw_guide_bar(GdkDrawable *surface, int from_x, int to_x,
     // Only draw it if it's not too close to the top.
     switch (bar._style) {
     case GBS_target:
-      gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_light_gray);
+      cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
       break;
 
     case GBS_user:
-      gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_user_guide_bar);
+      cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       break;
 
     case GBS_normal:
-      gdk_gc_set_rgb_fg_color(_pixmap_gc, &rgb_dark_gray);
+      cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       break;
     }
-    gdk_draw_line(surface, _pixmap_gc, from_x, y, to_x, y);
+    cairo_move_to(cr, from_x, y);
+    cairo_line_to(cr, to_x, y);
+    cairo_stroke(cr);
   }
 }
 
 /**
- * This is called during the servicing of expose_event.
+ * This is called during the servicing of the draw event.
  */
 void GtkStatsStripChart::
-draw_guide_labels() {
+draw_guide_labels(cairo_t *cr) {
   // Draw in the labels for the guide bars.
   int last_y = -100;
 
   int i;
   int num_guide_bars = get_num_guide_bars();
   for (i = 0; i < num_guide_bars; i++) {
-    last_y = draw_guide_label(get_guide_bar(i), last_y);
+    last_y = draw_guide_label(cr, get_guide_bar(i), last_y);
   }
 
   GuideBar top_value = make_guide_bar(get_vertical_scale());
-  draw_guide_label(top_value, last_y);
+  draw_guide_label(cr, top_value, last_y);
 
   last_y = -100;
   int num_user_guide_bars = get_num_user_guide_bars();
   for (i = 0; i < num_user_guide_bars; i++) {
-    last_y = draw_guide_label(get_user_guide_bar(i), last_y);
+    last_y = draw_guide_label(cr, get_user_guide_bar(i), last_y);
   }
 }
 
@@ -557,20 +583,18 @@ draw_guide_labels() {
  * value is given.  Returns the top pixel value of the new label.
  */
 int GtkStatsStripChart::
-draw_guide_label(const PStatGraph::GuideBar &bar, int last_y) {
-  GdkGC *gc = gdk_gc_new(_scale_area->window);
-
+draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) {
   switch (bar._style) {
   case GBS_target:
-    gdk_gc_set_rgb_fg_color(gc, &rgb_light_gray);
+    cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
     break;
 
   case GBS_user:
-    gdk_gc_set_rgb_fg_color(gc, &rgb_user_guide_bar);
+    cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     break;
 
   case GBS_normal:
-    gdk_gc_set_rgb_fg_color(gc, &rgb_dark_gray);
+    cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     break;
   }
 
@@ -587,7 +611,6 @@ draw_guide_label(const PStatGraph::GuideBar &bar, int last_y) {
     if (find_user_guide_bar(from_height, to_height) >= 0) {
       // Omit the label: there's a user-defined guide bar in the same space.
       g_object_unref(layout);
-      g_object_unref(gc);
       return last_y;
     }
   }
@@ -603,13 +626,13 @@ draw_guide_label(const PStatGraph::GuideBar &bar, int last_y) {
 
     int this_y = y - height / 2;
     if (last_y < this_y || last_y > this_y + height) {
-      gdk_draw_layout(_scale_area->window, gc, 0, this_y, layout);
+      cairo_move_to(cr, 0, this_y);
+      pango_cairo_show_layout(cr, layout);
       last_y = this_y;
     }
   }
 
   g_object_unref(layout);
-  g_object_unref(gc);
   return last_y;
 }
 
@@ -628,9 +651,9 @@ toggled_callback(GtkToggleButton *button, gpointer data) {
  * Draws in the scale labels.
  */
 gboolean GtkStatsStripChart::
-expose_event_callback(GtkWidget *widget, GdkEventExpose *event, gpointer data) {
+draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   GtkStatsStripChart *self = (GtkStatsStripChart *)data;
-  self->draw_guide_labels();
+  self->draw_guide_labels(cr);
 
   return TRUE;
 }

+ 8 - 9
pandatool/src/gtk-stats/gtkStatsStripChart.h

@@ -40,7 +40,8 @@ public:
 
   virtual void set_time_units(int unit_mask);
   virtual void set_scroll_speed(double scroll_speed);
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
   void set_vertical_scale(double value_height);
 
 protected:
@@ -54,7 +55,7 @@ protected:
   virtual void draw_cursor(int x);
   virtual void end_draw(int from_x, int to_x);
 
-  virtual void additional_graph_window_paint();
+  virtual void additional_graph_window_paint(cairo_t *cr);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual void set_drag_mode(DragMode drag_mode);
 
@@ -64,17 +65,15 @@ protected:
   virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y);
 
 private:
-  void draw_guide_bar(GdkDrawable *surface, int from_x, int to_x,
-          const PStatGraph::GuideBar &bar);
-  void draw_guide_labels();
-  int draw_guide_label(const PStatGraph::GuideBar &bar, int last_y);
+  void draw_guide_bar(cairo_t *cr, int from_x, int to_x,
+                      const PStatGraph::GuideBar &bar);
+  void draw_guide_labels(cairo_t *cr);
+  int draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y);
 
   static void toggled_callback(GtkToggleButton *button, gpointer data);
-  static gboolean expose_event_callback(GtkWidget *widget,
-          GdkEventExpose *event, gpointer data);
+  static gboolean draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data);
 
 private:
-  int _brush_origin;
   std::string _net_value_text;
 
   GtkWidget *_top_hbox;

+ 1 - 0
pandatool/src/gtk-stats/gtkstats_composite1.cxx

@@ -1,5 +1,6 @@
 #include "gtkStats.cxx"
 #include "gtkStatsChartMenu.cxx"
+#include "gtkStatsFlameGraph.cxx"
 #include "gtkStatsGraph.cxx"
 #include "gtkStatsLabel.cxx"
 #include "gtkStatsLabelStack.cxx"

+ 4 - 1
pandatool/src/pstatserver/CMakeLists.txt

@@ -4,6 +4,7 @@ endif()
 
 set(P3PSTATSERVER_HEADERS
   pStatClientData.h
+  pStatFlameGraph.h pStatFlameGraph.I
   pStatGraph.h pStatGraph.I
   pStatListener.h
   pStatMonitor.h pStatMonitor.I
@@ -17,7 +18,9 @@ set(P3PSTATSERVER_HEADERS
 )
 
 set(P3PSTATSERVER_SOURCES
-  pStatClientData.cxx pStatGraph.cxx
+  pStatClientData.cxx
+  pStatFlameGraph.cxx
+  pStatGraph.cxx
   pStatListener.cxx
   pStatMonitor.cxx pStatPianoRoll.cxx
   pStatReader.cxx pStatServer.cxx

+ 1 - 0
pandatool/src/pstatserver/p3pstatserver_composite1.cxx

@@ -1,4 +1,5 @@
 #include "pStatClientData.cxx"
+#include "pStatFlameGraph.cxx"
 #include "pStatGraph.cxx"
 #include "pStatListener.cxx"
 #include "pStatMonitor.cxx"

+ 89 - 0
pandatool/src/pstatserver/pStatFlameGraph.I

@@ -0,0 +1,89 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pStatFlameGraph.I
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+/**
+ * Returns the View this chart represents.
+ */
+INLINE PStatView &PStatFlameGraph::
+get_view() const {
+  return _view;
+}
+
+/**
+ * Returns the particular collector whose data this strip chart reflects.
+ */
+INLINE int PStatFlameGraph::
+get_collector_index() const {
+  return _collector_index;
+}
+
+/**
+ * Returns the amount of total time the width of the horizontal axis
+ * represents.
+ */
+INLINE double PStatFlameGraph::
+get_horizontal_scale() const {
+  return _time_width;
+}
+
+/**
+ * Changes the average_mode flag.  When true, the strip chart will average out
+ * the color values over pstats_average_time seconds, which hides spikes and
+ * makes the overall trends easier to read.  When false, the strip chart shows
+ * the actual data as it is happening.
+ */
+INLINE void PStatFlameGraph::
+set_average_mode(bool average_mode) {
+  if (_average_mode != average_mode) {
+    _average_mode = average_mode;
+    force_redraw();
+  }
+}
+
+/**
+ * Returns the current state of the average_mode flag.  When true, the strip
+ * chart will average out the color values over pstats_average_time seconds,
+ * which hides spikes and makes the overall trends easier to read.  When
+ * false, the strip chart shows the actual data as it is happening.
+ */
+INLINE bool PStatFlameGraph::
+get_average_mode() const {
+  return _average_mode;
+}
+
+/**
+ * Converts a value (i.e.  a "height" in the strip chart) to a horizontal
+ * pixel offset.
+ */
+INLINE int PStatFlameGraph::
+height_to_pixel(double value) const {
+  return (int)((double)_xsize * value / _time_width);
+}
+
+/**
+ * Converts a horizontal pixel offset to a value (a "height" in the strip
+ * chart).
+ */
+INLINE double PStatFlameGraph::
+pixel_to_height(int x) const {
+  return _time_width * (double)x / (double)_xsize;
+}
+
+/**
+ * Returns true if get_title_text() has never yet returned an answer, false if
+ * it has.
+ */
+INLINE bool PStatFlameGraph::
+is_title_unknown() const {
+  return _title_unknown;
+}

+ 297 - 0
pandatool/src/pstatserver/pStatFlameGraph.cxx

@@ -0,0 +1,297 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pStatFlameGraph.cxx
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#include "pStatFlameGraph.h"
+
+#include "pStatFrameData.h"
+#include "pStatCollectorDef.h"
+#include "string_utils.h"
+#include "config_pstatclient.h"
+
+#include <algorithm>
+#include <sstream>
+
+/**
+ *
+ */
+PStatFlameGraph::
+PStatFlameGraph(PStatMonitor *monitor, PStatView &view,
+                int thread_index, int collector_index, int xsize, int ysize) :
+  PStatGraph(monitor, xsize, ysize),
+  _thread_index(thread_index),
+  _view(view),
+  _collector_index(collector_index)
+{
+  _average_mode = true;
+  _average_cursor = 0;
+
+  _time_width = 1.0 / pstats_target_frame_rate;
+  _current_frame = -1;
+
+  _title_unknown = true;
+
+  _guide_bar_units = GBU_ms | GBU_hz | GBU_show_units;
+  normal_guide_bars();
+}
+
+/**
+ *
+ */
+PStatFlameGraph::
+~PStatFlameGraph() {
+}
+
+/**
+ * Updates the chart with the latest data.
+ */
+void PStatFlameGraph::
+update() {
+  const PStatClientData *client_data = _monitor->get_client_data();
+
+  // Don't bother to update the thread data until we know at least something
+  // about the collectors and threads.
+  if (client_data->get_num_collectors() != 0 &&
+      client_data->get_num_threads() != 0) {
+    const PStatThreadData *thread_data =
+      client_data->get_thread_data(_thread_index);
+    if (!thread_data->is_empty()) {
+      int frame_number = thread_data->get_latest_frame_number();
+      if (frame_number != _current_frame) {
+        _current_frame = frame_number;
+
+        update_data();
+        force_redraw();
+        update_labels();
+      }
+    }
+  }
+
+  idle();
+}
+
+/**
+ * Changes the collector represented by this flame graph.  This may force a
+ * redraw.
+ */
+void PStatFlameGraph::
+set_collector_index(int collector_index) {
+  if (_collector_index != collector_index) {
+    _collector_index = collector_index;
+    _title_unknown = true;
+    update_data();
+    force_redraw();
+    update_labels();
+  }
+}
+
+/**
+ * Returns the text suitable for the title label on the top line.
+ */
+std::string PStatFlameGraph::
+get_title_text() {
+  std::string text;
+
+  _title_unknown = false;
+
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (client_data->has_collector(_collector_index)) {
+    text = client_data->get_collector_fullname(_collector_index);
+    text += " flame graph";
+  } else {
+    _title_unknown = true;
+  }
+
+  if (_thread_index != 0) {
+    if (client_data->has_thread(_thread_index)) {
+      text += " (" + client_data->get_thread_name(_thread_index) + " thread)";
+    } else {
+      _title_unknown = true;
+    }
+  }
+
+  return text;
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string PStatFlameGraph::
+get_label_tooltip(int collector_index) const {
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (!client_data->has_collector(collector_index)) {
+    return std::string();
+  }
+
+  std::ostringstream text;
+  text << client_data->get_collector_fullname(collector_index);
+
+  Data::const_iterator it = _data.find(collector_index);
+  if (it != _data.end()) {
+    const CollectorData &cd = it->second;
+    text << " (" << format_number(cd._net_value, get_guide_bar_units(), get_guide_bar_unit_name()) << ")";
+  }
+
+  return text.str();
+}
+
+/**
+ *
+ */
+void PStatFlameGraph::
+update_data() {
+  // First clear the net values, so we'll know which labels should be deleted.
+  for (auto it = _data.begin(); it != _data.end(); ++it) {
+    it->second._net_value = 0;
+  }
+
+  _view.set_to_frame(_current_frame);
+
+  const PStatViewLevel *level = _view.get_level(_collector_index);
+  double offset = 0;
+  update_data(level, 0, offset);
+
+  _time_width = (offset != 0) ? offset : 1.0 / pstats_target_frame_rate;
+  normal_guide_bars();
+
+  // Cycle through the ring buffers.
+  _average_cursor = (_average_cursor + 1) % _num_average_frames;
+}
+
+/**
+ * Recursive helper for get_frame_data.
+ */
+void PStatFlameGraph::
+update_data(const PStatViewLevel *level, int depth, double &offset) {
+  double net_value = level->get_net_value();
+
+  Data::iterator it;
+  bool inserted;
+  std::tie(it, inserted) = _data.insert(std::make_pair(level->get_collector(), CollectorData()));
+  CollectorData &cd = it->second;
+  cd._offset = offset;
+  cd._depth = depth;
+
+  if (inserted || !_average_mode) {
+    // Initialize the values array.
+    for (double &v : cd._values) {
+      v = net_value;
+    }
+    cd._net_value = net_value;
+  } else {
+    cd._values[_average_cursor] = net_value;
+
+    // Calculate the average.
+    cd._net_value = 0;
+    for (double value : cd._values) {
+      cd._net_value += value;
+    }
+    cd._net_value /= _num_average_frames;
+  }
+
+  if (cd._net_value != 0.0) {
+    cd._net_value = std::max(cd._net_value, 0.0);
+
+    double child_offset = offset;
+    offset += cd._net_value;
+
+    int num_children = level->get_num_children();
+    for (int i = 0; i < num_children; i++) {
+      const PStatViewLevel *child = level->get_child(i);
+      update_data(child, depth + 1, child_offset);
+    }
+  }
+}
+
+/**
+ * To be called by the user class when the widget size has changed.  This
+ * updates the chart's internal data and causes it to issue redraw commands to
+ * reflect the new size.
+ */
+void PStatFlameGraph::
+changed_size(int xsize, int ysize) {
+  if (xsize != _xsize || ysize != _ysize) {
+    _xsize = xsize;
+    _ysize = ysize;
+
+    normal_guide_bars();
+    force_redraw();
+    update_labels();
+  }
+}
+
+/**
+ * To be called by the user class when the whole thing needs to be redrawn for
+ * some reason.
+ */
+void PStatFlameGraph::
+force_redraw() {
+  begin_draw();
+  end_draw();
+}
+
+/**
+ * Resets the list of labels.
+ */
+void PStatFlameGraph::
+update_labels() {
+  for (auto it = _data.begin(); it != _data.end(); ++it) {
+    int collector_index = it->first;
+    const CollectorData &cd = it->second;
+
+    update_label(collector_index, cd._depth, height_to_pixel(cd._offset), height_to_pixel(cd._net_value));
+  }
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void PStatFlameGraph::
+normal_guide_bars() {
+  // We want vaguely 100 pixels between guide bars.
+  int num_bars = get_xsize() / 100;
+
+  _guide_bars.clear();
+
+  double dist = _time_width / num_bars;
+
+  for (int i = 1; i < num_bars; ++i) {
+    _guide_bars.push_back(make_guide_bar(i * dist));
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called before
+ * drawing any bars in the chart.
+ */
+void PStatFlameGraph::
+begin_draw() {
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called after
+ * drawing a series of color bars in the chart.
+ */
+void PStatFlameGraph::
+end_draw() {
+}
+
+/**
+ * Should be overridden by the user class to perform any other updates might
+ * be necessary after the bars have been redrawn.
+ */
+void PStatFlameGraph::
+idle() {
+}

+ 107 - 0
pandatool/src/pstatserver/pStatFlameGraph.h

@@ -0,0 +1,107 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pStatFlameGraph.h
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#ifndef PSTATFLAMEGRAPH_H
+#define PSTATFLAMEGRAPH_H
+
+#include "pandatoolbase.h"
+
+#include "pStatGraph.h"
+#include "pStatMonitor.h"
+#include "pStatClientData.h"
+
+#include "pmap.h"
+#include "pdeque.h"
+
+class PStatFrameData;
+
+/**
+ * This is an abstract class that presents the interface for drawing a flame
+ * chart: it shows the time spent in each of a number of collectors
+ * as a horizontal bar of color, with time as the horizontal axis.
+ *
+ * This class just manages all the flame chart logic; the actual nuts and bolts
+ * of drawing pixels is left to a user-derived class.
+ */
+class PStatFlameGraph : public PStatGraph {
+public:
+  PStatFlameGraph(PStatMonitor *monitor, PStatView &view,
+                  int thread_index, int collector_index,
+                  int xsize, int ysize);
+  virtual ~PStatFlameGraph();
+
+  void update();
+
+  INLINE PStatView &get_view() const;
+  INLINE int get_collector_index() const;
+  void set_collector_index(int collector_index);
+
+  INLINE double get_horizontal_scale() const;
+
+  INLINE void set_average_mode(bool average_mode);
+  INLINE bool get_average_mode() const;
+
+  INLINE int height_to_pixel(double value) const;
+  INLINE double pixel_to_height(int y) const;
+
+  INLINE bool is_title_unknown() const;
+  std::string get_title_text();
+  std::string get_label_tooltip(int collector_index) const;
+
+protected:
+  static const size_t _num_average_frames = 200;
+
+  struct CollectorData {
+    double _offset;
+    double _net_value;
+    int _depth;
+    // This is updated like a ring buffer, initialized with all the same value
+    // at first, then always at _average_cursor.
+    double _values[_num_average_frames];
+  };
+  typedef pmap<int, CollectorData> Data;
+
+  void update_data();
+  void update_data(const PStatViewLevel *level, int depth, double &offset);
+  void changed_size(int xsize, int ysize);
+  void force_redraw();
+  virtual void update_labels();
+  virtual void update_label(int collector_index, int row, int x, int width)=0;
+  virtual void normal_guide_bars();
+
+  virtual void begin_draw();
+  virtual void end_draw();
+  virtual void idle();
+
+private:
+  void compute_page(const PStatFrameData &frame_data);
+
+protected:
+  int _thread_index;
+
+private:
+  PStatView &_view;
+  int _collector_index;
+  bool _average_mode;
+  size_t _average_cursor;
+
+  Data _data;
+
+  double _time_width;
+  int _current_frame;
+  bool _title_unknown;
+};
+
+#include "pStatFlameGraph.I"
+
+#endif

+ 8 - 0
pandatool/src/pstatserver/pStatMonitor.I

@@ -69,3 +69,11 @@ INLINE std::string PStatMonitor::
 get_client_progname() const {
   return _client_progname;
 }
+
+/**
+ * Returns the process id of the client, or -1 if it is not known.
+ */
+INLINE int PStatMonitor::
+get_client_pid() const {
+  return _client_pid;
+}

+ 4 - 2
pandatool/src/pstatserver/pStatMonitor.cxx

@@ -38,10 +38,11 @@ PStatMonitor::
  * indicates the client's reported hostname and program name.
  */
 void PStatMonitor::
-hello_from(const string &hostname, const string &progname) {
+hello_from(const string &hostname, const string &progname, int pid) {
   _client_known = true;
   _client_hostname = hostname;
   _client_progname = progname;
+  _client_pid = pid;
   got_hello();
 }
 
@@ -52,12 +53,13 @@ hello_from(const string &hostname, const string &progname) {
  * effect.
  */
 void PStatMonitor::
-bad_version(const string &hostname, const string &progname,
+bad_version(const string &hostname, const string &progname, int pid,
             int client_major, int client_minor,
             int server_major, int server_minor) {
   _client_known = true;
   _client_hostname = hostname;
   _client_progname = progname;
+  _client_pid = 0;
   got_bad_version(client_major, client_minor,
                   server_major, server_minor);
 }

+ 5 - 1
pandatool/src/pstatserver/pStatMonitor.h

@@ -43,8 +43,10 @@ public:
   PStatMonitor(PStatServer *server);
   virtual ~PStatMonitor();
 
-  void hello_from(const std::string &hostname, const std::string &progname);
+  void hello_from(const std::string &hostname, const std::string &progname,
+                  int pid);
   void bad_version(const std::string &hostname, const std::string &progname,
+                   int pid,
                    int client_major, int client_minor,
                    int server_major, int server_minor);
   void set_client_data(PStatClientData *client_data);
@@ -63,6 +65,7 @@ public:
   INLINE bool is_client_known() const;
   INLINE std::string get_client_hostname() const;
   INLINE std::string get_client_progname() const;
+  INLINE int get_client_pid() const;
 
   PStatView &get_view(int thread_index);
   PStatView &get_level_view(int collector_index, int thread_index);
@@ -98,6 +101,7 @@ private:
   bool _client_known;
   std::string _client_hostname;
   std::string _client_progname;
+  int _client_pid;
 
   typedef pmap<int, PStatView> Views;
   Views _views;

+ 3 - 1
pandatool/src/pstatserver/pStatReader.cxx

@@ -195,11 +195,13 @@ handle_client_control_message(const PStatClientControlMessage &message) {
           (message._major_version == server_major_version &&
            message._minor_version > server_minor_version)) {
         _monitor->bad_version(message._client_hostname, message._client_progname,
+                              message._client_pid,
                               message._major_version, message._minor_version,
                               server_major_version, server_minor_version);
         _monitor->close();
       } else {
-        _monitor->hello_from(message._client_hostname, message._client_progname);
+        _monitor->hello_from(message._client_hostname, message._client_progname,
+                             message._client_pid);
       }
     }
     break;

+ 9 - 0
pandatool/src/pstatserver/pStatStripChart.I

@@ -156,6 +156,15 @@ pixel_to_height(int x) const {
   return _value_height * (double)(get_ysize() - x) / (double)get_ysize();
 }
 
+/**
+ * Returns true if get_title_text() has never yet returned an answer, false if
+ * it has.
+ */
+INLINE bool PStatStripChart::
+is_title_unknown() const {
+  return _title_unknown;
+}
+
 /**
  * Returns true if the indicated collector appears anywhere on the chart at
  * the current time, false otherwise.

+ 64 - 5
pandatool/src/pstatserver/pStatStripChart.cxx

@@ -284,12 +284,71 @@ get_title_text() {
 }
 
 /**
- * Returns true if get_title_text() has never yet returned an answer, false if
- * it has.
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
  */
-bool PStatStripChart::
-is_title_unknown() const {
-  return _title_unknown;
+std::string PStatStripChart::
+get_label_tooltip(int collector_index) const {
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (!client_data->has_collector(collector_index)) {
+    return std::string();
+  }
+
+  std::ostringstream text;
+  text << client_data->get_collector_fullname(collector_index);
+
+  double value;
+  if (collector_index == _collector_index) {
+    value = get_average_net_value();
+  }
+  else {
+    const PStatThreadData *thread_data = _view.get_thread_data();
+    int now_i, then_i;
+    if (!thread_data->get_elapsed_frames(then_i, now_i)) {
+      return text.str();
+    }
+    double now = _time_width + _start_time;
+    double then = now - pstats_average_time;
+
+    double net_value = 0.0f;
+    double net_time = 0.0f;
+
+    // We start with just the portion of frame then_i that actually does fall
+    // within our "then to now" window (usually some portion of it will).
+    const PStatFrameData &frame_data = thread_data->get_frame(then_i);
+    if (frame_data.get_end() > then) {
+      double this_time = (frame_data.get_end() - then);
+      _view.set_to_frame(frame_data);
+
+      const PStatViewLevel *level = _view.get_level(collector_index);
+      if (level != nullptr) {
+        net_value += level->get_net_value() * this_time;
+        net_time += this_time;
+      }
+    }
+    // Then we get all of each of the remaining frames.
+    for (int frame_number = then_i + 1;
+         frame_number <= now_i;
+         frame_number++) {
+      const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
+      double this_time = frame_data.get_net_time();
+      _view.set_to_frame(frame_data);
+
+      const PStatViewLevel *level = _view.get_level(collector_index);
+      if (level != nullptr) {
+        net_value += level->get_net_value() * this_time;
+        net_time += this_time;
+      }
+    }
+
+    if (net_time == 0) {
+      return text.str();
+    }
+    value = net_value / net_time;
+  }
+
+  text << " (" << format_number(value, get_guide_bar_units(), get_guide_bar_unit_name()) << ")";
+  return text.str();
 }
 
 /**

+ 3 - 1
pandatool/src/pstatserver/pStatStripChart.h

@@ -68,8 +68,9 @@ public:
   INLINE int height_to_pixel(double value) const;
   INLINE double pixel_to_height(int y) const;
 
-  std::string get_title_text();
   bool is_title_unknown() const;
+  std::string get_title_text();
+  std::string get_label_tooltip(int collector_index) const;
 
 protected:
   class ColorData {
@@ -89,6 +90,7 @@ protected:
   void compute_average_pixel_data(PStatStripChart::FrameData &result,
                                   int &then_i, int &now_i, double now);
   double get_net_value(int frame_number) const;
+  double get_net_value(int frame_number, int collector_index) const;
   double get_average_net_value() const;
 
   void changed_size(int xsize, int ysize);

+ 50 - 7
pandatool/src/text-stats/textMonitor.cxx

@@ -22,9 +22,10 @@
  *
  */
 TextMonitor::
-TextMonitor(TextStats *server, std::ostream *outStream, bool show_raw_data ) : PStatMonitor(server) {
-    _outStream = outStream;    //[PECI]
-    _show_raw_data = show_raw_data;
+TextMonitor(TextStats *server, std::ostream *outStream, bool show_raw_data, bool json) : PStatMonitor(server) {
+  _outStream = outStream;    //[PECI]
+  _show_raw_data = show_raw_data;
+  _json = json;
 }
 
 /**
@@ -73,6 +74,30 @@ got_bad_version(int client_major, int client_minor,
     << server_major << "." << server_minor << ".\n";
 }
 
+/**
+ * Called whenever a new Thread definition is received from the client.
+ * Generally, the client will send all of its threads over shortly after
+ * connecting, but there's no guarantee that they will all be received before
+ * the first frames are received.  The monitor should be prepared to accept
+ * new Thread definitions midstream.
+ */
+void TextMonitor::
+new_thread(int thread_index) {
+  if (_json) {
+    const PStatClientData *client_data = get_client_data();
+
+    int pid = get_client_pid();
+    if (pid < 0) {
+      pid = _dummy_pid;
+    }
+
+    (*_outStream)
+      << "{\"name\":\"thread_name\",\"ph\":\"M\",\"pid\":" << pid
+      << ",\"tid\":" << thread_index << ",\"args\":{\"name\":\""
+      << client_data->get_thread_name(thread_index) << "\"}},\n";
+  }
+}
+
 /**
  * Called as each frame's data is made available.  There is no gurantee the
  * frames will arrive in order, or that all of them will arrive at all.  The
@@ -84,12 +109,29 @@ new_data(int thread_index, int frame_number) {
   PStatView &view = get_view(thread_index);
   const PStatThreadData *thread_data = view.get_thread_data();
 
-  if (frame_number == thread_data->get_latest_frame_number()) {
-    view.set_to_frame(frame_number);
+  view.set_to_frame(frame_number);
+
+  if (true) {
+    const PStatClientData *client_data = get_client_data();
 
-    if (view.all_collectors_known()) {
-      const PStatClientData *client_data = get_client_data();
+    if (_json) {
+      int pid = get_client_pid();
+      if (pid < 0) {
+        pid = _dummy_pid;
+      }
 
+      const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
+      int num_events = frame_data.get_num_events();
+      for (int i = 0; i < num_events; ++i) {
+        int collector_index = frame_data.get_time_collector(i);
+        (*_outStream)
+          << "{\"name\":\"" << client_data->get_collector_fullname(collector_index)
+          << "\",\"ts\":" << (uint64_t)(frame_data.get_time(i) * 1000000)
+          << ",\"ph\":\"" << (frame_data.is_start(i) ? 'B' : 'E') << "\""
+          << ",\"tid\":" << thread_index << ",\"pid\":" << pid << "},\n";
+      }
+    }
+    else {
       (*_outStream) << "\rThread "
            << client_data->get_thread_name(thread_index)
            << " frame " << frame_number << ", "
@@ -149,6 +191,7 @@ new_data(int thread_index, int frame_number) {
 void TextMonitor::
 lost_connection() {
   nout << "Lost connection.\n";
+  ++_dummy_pid;
 }
 
 /**

+ 4 - 1
pandatool/src/text-stats/textMonitor.h

@@ -29,7 +29,7 @@ class TextStats;
  */
 class TextMonitor : public PStatMonitor {
 public:
-  TextMonitor(TextStats *server, std::ostream *outStream, bool show_raw_data);
+  TextMonitor(TextStats *server, std::ostream *outStream, bool show_raw_data, bool json = false);
   TextStats *get_server();
 
   virtual std::string get_monitor_name();
@@ -37,6 +37,7 @@ public:
   virtual void got_hello();
   virtual void got_bad_version(int client_major, int client_minor,
                                int server_major, int server_minor);
+  virtual void new_thread(int thread_index);
   virtual void new_data(int thread_index, int frame_number);
   virtual void lost_connection();
   virtual bool is_thread_safe();
@@ -47,6 +48,8 @@ public:
 private:
   std::ostream *_outStream; //[PECI]
   bool _show_raw_data;
+  bool _json;
+  int _dummy_pid = 0;
 };
 
 #include "textMonitor.I"

+ 17 - 2
pandatool/src/text-stats/textStats.cxx

@@ -50,6 +50,11 @@ TextStats() {
      "time per collector.",
      &TextStats::dispatch_none, &_show_raw_data, nullptr);
 
+  add_option
+    ("j", "", 0,
+     "Output data in JSON format.",
+     &TextStats::dispatch_none, &_json, nullptr);
+
   add_option
     ("o", "filename", 0,
      "Filename where to print. If not given then stderr is being used.",
@@ -66,7 +71,7 @@ TextStats() {
 PStatMonitor *TextStats::
 make_monitor() {
 
-  return new TextMonitor(this, _outFile, _show_raw_data);
+  return new TextMonitor(this, _outFile, _show_raw_data, _json);
 }
 
 
@@ -87,13 +92,23 @@ run() {
   nout << "Listening for connections.\n";
 
   if (_got_outputFileName) {
-    _outFile = new std::ofstream(_outputFileName.c_str(), std::ios::out);
+    _outFile = new std::ofstream(_outputFileName.c_str(), std::ios::out | std::ios::trunc);
   } else {
     _outFile = &(nout);
   }
 
+  if (_json) {
+    (*_outFile) << "[\n";
+  }
+
   main_loop(&user_interrupted);
   nout << "Exiting.\n";
+
+  if (_json) {
+    // Remove the last comma.
+    _outFile->seekp(-3, std::ios::cur);
+    (*_outFile) << "\n]\n";
+  }
 }
 
 

+ 1 - 0
pandatool/src/text-stats/textStats.h

@@ -37,6 +37,7 @@ public:
 private:
   int _port;
   bool _show_raw_data;
+  bool _json = false;
 
   // [PECI]
   bool _got_outputFileName;

+ 4 - 2
pandatool/src/win-stats/CMakeLists.txt

@@ -4,9 +4,10 @@ endif()
 
 set(WINSTATS_HEADERS
   winStatsChartMenu.h
+  winStatsFlameGraph.h
   winStatsGraph.h
   winStats.h
-  winStatsLabel.h
+  winStatsLabel.h winStatsLabel.I
   winStatsLabelStack.h
   winStatsMenuId.h
   winStatsMonitor.h winStatsMonitor.I
@@ -18,6 +19,7 @@ set(WINSTATS_HEADERS
 set(WINSTATS_SOURCES
   winStatsChartMenu.cxx
   winStats.cxx
+  winStatsFlameGraph.cxx
   winStatsGraph.cxx
   winStatsLabel.cxx
   winStatsLabelStack.cxx
@@ -29,7 +31,7 @@ set(WINSTATS_SOURCES
 
 composite_sources(win-stats WINSTATS_SOURCES)
 add_executable(win-stats ${WINSTATS_HEADERS} ${WINSTATS_SOURCES})
-target_link_libraries(win-stats p3progbase p3pstatserver)
+target_link_libraries(win-stats p3progbase p3pstatserver comctl32.lib)
 
 # This program is NOT actually called win-stats. It's just pstats.exe
 set_target_properties(win-stats PROPERTIES OUTPUT_NAME "pstats")

+ 14 - 0
pandatool/src/win-stats/winStats.cxx

@@ -20,6 +20,11 @@
 #define WIN32_LEAN_AND_MEAN 1
 #endif
 #include <windows.h>
+#include <commctrl.h>
+#include <shellscalingapi.h>
+
+// Enable common controls version 6, necessary for modern visual styles
+#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
 
 static const char *toplevel_class_name = "pstats";
 static WinStatsServer *server = nullptr;
@@ -82,6 +87,15 @@ create_toplevel_window(HINSTANCE application) {
 }
 
 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
+  // Initialize commctl32.dll.
+  INITCOMMONCONTROLSEX icc;
+  icc.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES;
+  icc.dwSize = sizeof(INITCOMMONCONTROLSEX);
+  InitCommonControlsEx(&icc);
+
+  // Signal DPI awareness.
+  SetProcessDPIAware();
+
   HINSTANCE application = GetModuleHandle(nullptr);
   HWND toplevel_window = create_toplevel_window(application);
 

+ 21 - 8
pandatool/src/win-stats/winStatsChartMenu.cxx

@@ -125,19 +125,32 @@ do_update() {
     }
   }
 
-  // Also a menu item for a piano roll (following a separator).
+  // Also menu items for flame graph and piano roll (following a separator).
   mii.fMask = MIIM_FTYPE;
   mii.fType = MFT_SEPARATOR;
   InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
 
-  WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false);
-  int menu_id = _monitor->get_menu_id(menu_def);
+  {
+    WinStatsMonitor::MenuDef menu_def(_thread_index, -2, false);
+    int menu_id = _monitor->get_menu_id(menu_def);
 
-  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
-  mii.fType = MFT_STRING;
-  mii.wID = menu_id;
-  mii.dwTypeData = "Piano Roll";
-  InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+    mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
+    mii.fType = MFT_STRING;
+    mii.wID = menu_id;
+    mii.dwTypeData = "Flame Graph";
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+  }
+
+  {
+    WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false);
+    int menu_id = _monitor->get_menu_id(menu_def);
+
+    mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
+    mii.fType = MFT_STRING;
+    mii.wID = menu_id;
+    mii.dwTypeData = "Piano Roll";
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+  }
 }
 
 /**

+ 639 - 0
pandatool/src/win-stats/winStatsFlameGraph.cxx

@@ -0,0 +1,639 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file winStatsFlameGraph.cxx
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#include "winStatsFlameGraph.h"
+#include "winStatsLabel.h"
+#include "winStatsMonitor.h"
+#include "pStatCollectorDef.h"
+
+#include <commctrl.h>
+
+static const int default_flame_graph_width = 800;
+static const int default_flame_graph_height = 150;
+
+bool WinStatsFlameGraph::_window_class_registered = false;
+const char * const WinStatsFlameGraph::_window_class_name = "flame";
+
+/**
+ *
+ */
+WinStatsFlameGraph::
+WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
+                   int collector_index) :
+  PStatFlameGraph(monitor, monitor->get_view(thread_index),
+                  thread_index, collector_index,
+                  monitor->get_pixel_scale() * default_flame_graph_width / 4,
+                  monitor->get_pixel_scale() * default_flame_graph_height / 4),
+  WinStatsGraph(monitor)
+{
+  _left_margin = _pixel_scale * 2;
+  _right_margin = _pixel_scale * 2;
+  _top_margin = _pixel_scale * 6;
+  _bottom_margin = _pixel_scale * 2;
+
+  // Let's show the units on the guide bar labels.  There's room.
+  set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+  _average_check_box = 0;
+
+  create_window();
+  clear_region();
+}
+
+/**
+ *
+ */
+WinStatsFlameGraph::
+~WinStatsFlameGraph() {
+}
+
+/**
+ * Called as each frame's data is made available.  There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all.  The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void WinStatsFlameGraph::
+new_data(int thread_index, int frame_number) {
+  if (is_title_unknown()) {
+    std::string window_title = get_title_text();
+    if (!is_title_unknown()) {
+      SetWindowText(_window, window_title.c_str());
+    }
+  }
+
+  if (!_pause) {
+    update();
+
+    std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+    if (_net_value_text != text) {
+      _net_value_text = text;
+      RECT rect;
+      GetClientRect(_window, &rect);
+      rect.bottom = _top_margin;
+      InvalidateRect(_window, &rect, TRUE);
+    }
+  }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void WinStatsFlameGraph::
+force_redraw() {
+  PStatFlameGraph::force_redraw();
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void WinStatsFlameGraph::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+  PStatFlameGraph::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void WinStatsFlameGraph::
+set_time_units(int unit_mask) {
+  int old_unit_mask = get_guide_bar_units();
+  if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+    unit_mask = unit_mask & (GBU_hz | GBU_ms);
+    unit_mask |= (old_unit_mask & GBU_show_units);
+    set_guide_bar_units(unit_mask);
+
+    RECT rect;
+    GetClientRect(_window, &rect);
+    rect.left = _right_margin;
+    InvalidateRect(_window, &rect, TRUE);
+  }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+void WinStatsFlameGraph::
+on_click_label(int collector_index) {
+  int prev_collector_index = get_collector_index();
+  if (collector_index == prev_collector_index && collector_index != 0) {
+    // Clicking on the top label means to go up to the parent level.
+    const PStatClientData *client_data =
+      WinStatsGraph::_monitor->get_client_data();
+    if (client_data->has_collector(collector_index)) {
+      const PStatCollectorDef &def =
+        client_data->get_collector_def(collector_index);
+      collector_index = def._parent_index;
+      set_collector_index(collector_index);
+    }
+  }
+  else {
+    // Clicking on any other label means to focus on that.
+    set_collector_index(collector_index);
+  }
+
+  // Change the root collector to show the full name.
+  if (prev_collector_index != collector_index) {
+    auto it = _labels.find(prev_collector_index);
+    if (it != _labels.end()) {
+      it->second->update_text(false);
+    }
+    it = _labels.find(collector_index);
+    if (it != _labels.end()) {
+      it->second->update_text(true);
+    }
+  }
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void WinStatsFlameGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void WinStatsFlameGraph::
+on_leave_label(int collector_index) {
+  if (collector_index == _highlighted_index && collector_index != -1) {
+    _highlighted_index = -1;
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsFlameGraph::
+get_label_tooltip(int collector_index) const {
+  return PStatFlameGraph::get_label_tooltip(collector_index);
+}
+
+/**
+ * Repositions the labels.
+ */
+void WinStatsFlameGraph::
+update_labels() {
+  if (_graph_window) {
+    PStatFlameGraph::update_labels();
+  }
+}
+
+/**
+ * Repositions a label.  If width is 0, the label should be deleted.
+ */
+void WinStatsFlameGraph::
+update_label(int collector_index, int row, int x, int width) {
+  WinStatsLabel *label;
+
+  auto it = _labels.find(collector_index);
+  if (it != _labels.end()) {
+    if (width == 0) {
+      delete it->second;
+      _labels.erase(it);
+      return;
+    }
+    label = it->second;
+  } else {
+    if (width == 0) {
+      return;
+    }
+    label = new WinStatsLabel(WinStatsGraph::_monitor, this, _thread_index, collector_index, false, false);
+    _labels[collector_index] = label;
+    label->setup(_graph_window);
+  }
+
+  label->set_pos(x, _ysize - 2 - row * label->get_height(), std::min(width, _xsize - 2));
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void WinStatsFlameGraph::
+normal_guide_bars() {
+  // We want vaguely 100 pixels between guide bars.
+  int num_bars = get_xsize() / (_pixel_scale * 25);
+
+  _guide_bars.clear();
+
+  double dist = get_horizontal_scale() / num_bars;
+
+  for (int i = 1; i < num_bars; ++i) {
+    _guide_bars.push_back(make_guide_bar(i * dist));
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Erases the chart area.
+ */
+void WinStatsFlameGraph::
+clear_region() {
+  RECT rect = { 0, 0, get_xsize(), get_ysize() };
+  FillRect(_bitmap_dc, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH));
+}
+
+/**
+ * Erases the chart area in preparation for drawing a bunch of bars.
+ */
+void WinStatsFlameGraph::
+begin_draw() {
+  clear_region();
+
+  // Draw in the guide bars.
+  int num_guide_bars = get_num_guide_bars();
+  for (int i = 0; i < num_guide_bars; i++) {
+    draw_guide_bar(_bitmap_dc, get_guide_bar(i));
+  }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void WinStatsFlameGraph::
+end_draw() {
+  InvalidateRect(_graph_window, nullptr, FALSE);
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void WinStatsFlameGraph::
+idle() {
+}
+
+/**
+ *
+ */
+LONG WinStatsFlameGraph::
+window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_LBUTTONDOWN:
+    if (_potential_drag_mode == DM_new_guide_bar) {
+      set_drag_mode(DM_new_guide_bar);
+      SetCapture(_graph_window);
+      return 0;
+    }
+    break;
+
+  case WM_COMMAND:
+    switch (LOWORD(wparam)) {
+    case BN_CLICKED:
+      if ((HWND)lparam == _average_check_box) {
+        int result = SendMessage(_average_check_box, BM_GETCHECK, 0, 0);
+        set_average_mode(result == BST_CHECKED);
+        return 0;
+      }
+      break;
+    }
+    break;
+
+  default:
+    break;
+  }
+
+  return WinStatsGraph::window_proc(hwnd, msg, wparam, lparam);
+}
+
+/**
+ *
+ */
+LONG WinStatsFlameGraph::
+graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_LBUTTONDOWN:
+    if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+      set_drag_mode(DM_guide_bar);
+      int16_t x = LOWORD(lparam);
+      _drag_start_x = x;
+      SetCapture(_graph_window);
+      return 0;
+    }
+    break;
+
+  case WM_MOUSEMOVE:
+    if (_drag_mode == DM_new_guide_bar) {
+      // We haven't created the new guide bar yet; we won't until the mouse
+      // comes within the graph's region.
+      int16_t x = LOWORD(lparam);
+      if (x >= 0 && x < get_xsize()) {
+        set_drag_mode(DM_guide_bar);
+        _drag_guide_bar = add_user_guide_bar(pixel_to_height(x));
+        return 0;
+      }
+    }
+    else if (_drag_mode == DM_guide_bar) {
+      int16_t x = LOWORD(lparam);
+      move_user_guide_bar(_drag_guide_bar, pixel_to_height(x));
+      return 0;
+    }
+    break;
+
+  case WM_LBUTTONUP:
+    if (_drag_mode == DM_guide_bar) {
+      int16_t x = LOWORD(lparam);
+      if (x < 0 || x >= get_xsize()) {
+        remove_user_guide_bar(_drag_guide_bar);
+      } else {
+        move_user_guide_bar(_drag_guide_bar, pixel_to_height(x));
+      }
+      set_drag_mode(DM_none);
+      ReleaseCapture();
+      return 0;
+    }
+    break;
+
+  case WM_LBUTTONDBLCLK:
+    {
+      // Clicking on whitespace in the graph goes to the parent.
+      on_click_label(get_collector_index());
+      return 0;
+    }
+    break;
+
+  default:
+    break;
+  }
+
+  return WinStatsGraph::graph_window_proc(hwnd, msg, wparam, lparam);
+}
+
+/**
+ * This is called during the servicing of WM_PAINT; it gives a derived class
+ * opportunity to do some further painting into the window (the outer window,
+ * not the graph window).
+ */
+void WinStatsFlameGraph::
+additional_window_paint(HDC hdc) {
+  // Draw in the labels for the guide bars.
+  SelectObject(hdc, WinStatsGraph::_monitor->get_font());
+  SetTextAlign(hdc, TA_LEFT | TA_BOTTOM);
+  SetBkMode(hdc, TRANSPARENT);
+
+  int y = _top_margin - _pixel_scale / 2;
+
+  int i;
+  int num_guide_bars = get_num_guide_bars();
+  for (i = 0; i < num_guide_bars; i++) {
+    draw_guide_label(hdc, y, get_guide_bar(i));
+  }
+
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_label(hdc, y, get_user_guide_bar(i));
+  }
+
+  RECT rect;
+  GetClientRect(_window, &rect);
+
+  // Now draw the "net value" label at the top.
+  SetTextAlign(hdc, TA_RIGHT | TA_BOTTOM);
+  SetTextColor(hdc, RGB(0, 0, 0));
+  TextOut(hdc, rect.right - _right_margin, y,
+          _net_value_text.data(), _net_value_text.length());
+}
+
+/**
+ * This is called during the servicing of WM_PAINT; it gives a derived class
+ * opportunity to do some further painting into the window (the outer window,
+ * not the graph window).
+ */
+void WinStatsFlameGraph::
+additional_graph_window_paint(HDC hdc) {
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (int i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_bar(hdc, get_user_guide_bar(i));
+  }
+}
+
+/**
+ * Based on the mouse position within the window's client area, look for
+ * draggable things the mouse might be hovering over and return the
+ * apprioprate DragMode enum or DM_none if nothing is indicated.
+ */
+WinStatsGraph::DragMode WinStatsFlameGraph::
+consider_drag_start(int mouse_x, int mouse_y, int width, int height) {
+  if (mouse_y >= _graph_top && mouse_y < _graph_top + get_ysize()) {
+    if (mouse_x >= _graph_left && mouse_x < _graph_left + get_xsize()) {
+      // See if the mouse is over a user-defined guide bar.
+      int x = mouse_x - _graph_left;
+      double from_height = pixel_to_height(x - 2);
+      double to_height = pixel_to_height(x + 2);
+      _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+      if (_drag_guide_bar >= 0) {
+        return DM_guide_bar;
+      }
+
+    } else if (mouse_x < _left_margin - 2 ||
+               mouse_x > width - _right_margin + 2) {
+      // The mouse is left or right of the graph; maybe create a new guide
+      // bar.
+      return DM_new_guide_bar;
+    }
+  }
+
+  // Don't upcall; there's no point resizing the margins.
+  return DM_none;
+}
+
+/**
+ * Repositions the graph child window within the parent window according to
+ * the _margin variables.
+ */
+void WinStatsFlameGraph::
+move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize) {
+  WinStatsGraph::move_graph_window(graph_left, graph_top, graph_xsize, graph_ysize);
+  if (_average_check_box != 0) {
+    SIZE size;
+    SendMessage(_average_check_box, BCM_GETIDEALSIZE, 0, (LPARAM)&size);
+
+    SetWindowPos(_average_check_box, 0,
+                 _left_margin, _top_margin - size.cy - _pixel_scale / 2,
+                 size.cx, size.cy,
+                 SWP_NOZORDER | SWP_SHOWWINDOW);
+    InvalidateRect(_average_check_box, nullptr, TRUE);
+  }
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void WinStatsFlameGraph::
+draw_guide_bar(HDC hdc, const PStatGraph::GuideBar &bar) {
+  int x = height_to_pixel(bar._height);
+
+  if (x > 0 && x < get_xsize() - 1) {
+    // Only draw it if it's not too close to either edge.
+    switch (bar._style) {
+    case GBS_target:
+      SelectObject(hdc, _light_pen);
+      break;
+
+    case GBS_user:
+      SelectObject(hdc, _user_guide_bar_pen);
+      break;
+
+    case GBS_normal:
+      SelectObject(hdc, _dark_pen);
+      break;
+    }
+    MoveToEx(hdc, x, 0, nullptr);
+    LineTo(hdc, x, get_ysize());
+  }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void WinStatsFlameGraph::
+draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar) {
+  switch (bar._style) {
+  case GBS_target:
+    SetTextColor(hdc, _light_color);
+    break;
+
+  case GBS_user:
+    SetTextColor(hdc, _user_guide_bar_color);
+    break;
+
+  case GBS_normal:
+    SetTextColor(hdc, _dark_color);
+    break;
+  }
+
+  int x = height_to_pixel(bar._height);
+  const std::string &label = bar._label;
+  SIZE size;
+  GetTextExtentPoint32(hdc, label.data(), label.length(), &size);
+
+  if (bar._style != GBS_user) {
+    double from_height = pixel_to_height(x - size.cx);
+    double to_height = pixel_to_height(x + size.cx);
+    if (find_user_guide_bar(from_height, to_height) >= 0) {
+      // Omit the label: there's a user-defined guide bar in the same space.
+      return;
+    }
+  }
+
+  int this_x = _graph_left + x - size.cx / 2;
+  if (x >= 0 && x < get_xsize()) {
+    TextOut(hdc, this_x, y,
+            label.data(), label.length());
+  }
+}
+
+/**
+ * Creates the window for this strip chart.
+ */
+void WinStatsFlameGraph::
+create_window() {
+  if (_window) {
+    return;
+  }
+
+  HINSTANCE application = GetModuleHandle(nullptr);
+  register_window_class(application);
+
+  std::string window_title = get_title_text();
+
+  RECT win_rect = {
+    0, 0,
+    _left_margin + get_xsize() + _right_margin,
+    _top_margin + get_ysize() + _bottom_margin
+  };
+
+  // compute window size based on desired client area size
+  AdjustWindowRect(&win_rect, graph_window_style, FALSE);
+
+  _window =
+    CreateWindow(_window_class_name, window_title.c_str(), graph_window_style,
+                 CW_USEDEFAULT, CW_USEDEFAULT,
+                 win_rect.right - win_rect.left,
+                 win_rect.bottom - win_rect.top,
+                 WinStatsGraph::_monitor->get_window(), nullptr, application, 0);
+  if (!_window) {
+    nout << "Could not create FlameGraph window!\n";
+    exit(1);
+  }
+
+  SetWindowLongPtr(_window, 0, (LONG_PTR)this);
+
+  _average_check_box =
+    CreateWindow(WC_BUTTON, "Average", WS_CHILD | BS_AUTOCHECKBOX,
+                 0, 0, 0, 0,
+                 _window, nullptr, application, 0);
+  SendMessage(_average_check_box, WM_SETFONT,
+              (WPARAM)WinStatsGraph::_monitor->get_font(), TRUE);
+
+  if (get_average_mode()) {
+    SendMessage(_average_check_box, BM_SETCHECK, BST_CHECKED, 0);
+  }
+
+  // Ensure that the window is on top of the stack.
+  SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0,
+               SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
+}
+
+/**
+ * Registers the window class for the FlameGraph window, if it has not already
+ * been registered.
+ */
+void WinStatsFlameGraph::
+register_window_class(HINSTANCE application) {
+  if (_window_class_registered) {
+    return;
+  }
+
+  WNDCLASS wc;
+
+  ZeroMemory(&wc, sizeof(WNDCLASS));
+  wc.style = 0;
+  wc.lpfnWndProc = (WNDPROC)static_window_proc;
+  wc.hInstance = application;
+  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
+  wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
+  wc.lpszMenuName = nullptr;
+  wc.lpszClassName = _window_class_name;
+
+  // Reserve space to associate the this pointer with the window.
+  wc.cbWndExtra = sizeof(WinStatsFlameGraph *);
+
+  if (!RegisterClass(&wc)) {
+    nout << "Could not register FlameGraph window class!\n";
+    exit(1);
+  }
+
+  _window_class_registered = true;
+}
+
+/**
+ *
+ */
+LONG WINAPI WinStatsFlameGraph::
+static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  WinStatsFlameGraph *self = (WinStatsFlameGraph *)GetWindowLongPtr(hwnd, 0);
+  if (self != nullptr && self->_window == hwnd) {
+    return self->window_proc(hwnd, msg, wparam, lparam);
+  } else {
+    return DefWindowProc(hwnd, msg, wparam, lparam);
+  }
+}

+ 80 - 0
pandatool/src/win-stats/winStatsFlameGraph.h

@@ -0,0 +1,80 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file winStatsFlameGraph.h
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#ifndef WINSTATSFLAMEGRAPH_H
+#define WINSTATSFLAMEGRAPH_H
+
+#include "pandatoolbase.h"
+
+#include "winStatsGraph.h"
+#include "pStatFlameGraph.h"
+
+class WinStatsLabel;
+
+/**
+ * A window that draws a flame chart, which shows the collectors explicitly
+ * stopping and starting, one frame at a time.
+ */
+class WinStatsFlameGraph : public PStatFlameGraph, public WinStatsGraph {
+public:
+  WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
+                     int collector_index=0);
+  virtual ~WinStatsFlameGraph();
+
+  virtual void new_data(int thread_index, int frame_number);
+  virtual void force_redraw();
+  virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+  virtual void set_time_units(int unit_mask);
+  virtual void on_click_label(int collector_index);
+  virtual void on_enter_label(int collector_index);
+  virtual void on_leave_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
+
+protected:
+  virtual void update_labels();
+  virtual void update_label(int collector_index, int row, int x, int width);
+  virtual void normal_guide_bars();
+
+  void clear_region();
+  virtual void begin_draw();
+  virtual void end_draw();
+  virtual void idle();
+
+  LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+  virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+  virtual void additional_window_paint(HDC hdc);
+  virtual void additional_graph_window_paint(HDC hdc);
+  virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
+                                       int width, int height);
+  virtual void move_graph_window(int graph_left, int graph_top,
+                                 int graph_xsize, int graph_ysize);
+
+private:
+  void draw_guide_bar(HDC hdc, const GuideBar &bar);
+  void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar);
+  void create_window();
+  static void register_window_class(HINSTANCE application);
+
+  static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+
+  std::string _net_value_text;
+  pmap<int, WinStatsLabel *> _labels;
+
+  HWND _average_check_box;
+
+  static bool _window_class_registered;
+  static const char * const _window_class_name;
+};
+
+#endif

+ 107 - 84
pandatool/src/win-stats/winStatsGraph.cxx

@@ -14,9 +14,11 @@
 #include "winStatsGraph.h"
 #include "winStatsMonitor.h"
 #include "winStatsLabelStack.h"
+#include "convert_srgb.h"
 
-bool WinStatsGraph::_graph_window_class_registered = false;
-const char * const WinStatsGraph::_graph_window_class_name = "graph";
+#include <commctrl.h>
+
+#define IDC_GRAPH 100
 
 DWORD WinStatsGraph::graph_window_style =
 WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE;
@@ -40,6 +42,8 @@ WinStatsGraph(WinStatsMonitor *monitor) :
   _bitmap_xsize = 0;
   _bitmap_ysize = 0;
 
+  _pixel_scale = monitor->get_pixel_scale();
+
   _dark_color = RGB(51, 51, 51);
   _light_color = RGB(154, 154, 154);
   _user_guide_bar_color = RGB(130, 150, 255);
@@ -66,11 +70,11 @@ WinStatsGraph::
   DeleteObject(_light_pen);
   DeleteObject(_user_guide_bar_pen);
 
-  Brushes::iterator bi;
-  for (bi = _brushes.begin(); bi != _brushes.end(); ++bi) {
-    HBRUSH brush = (*bi).second;
-    DeleteObject(brush);
+  for (auto &item : _brushes) {
+    DeleteObject(item.second.first);
+    DeleteObject(item.second.second);
   }
+  _brushes.clear();
 
   if (_graph_window) {
     DestroyWindow(_graph_window);
@@ -97,13 +101,6 @@ void WinStatsGraph::
 new_data(int thread_index, int frame_number) {
 }
 
-/**
- * Called when it is necessary to redraw the entire graph.
- */
-void WinStatsGraph::
-force_redraw() {
-}
-
 /**
  * Called when the user has resized the window, forcing a resize of the graph.
  */
@@ -150,7 +147,46 @@ user_guide_bars_changed() {
  * Called when the user single-clicks on a label.
  */
 void WinStatsGraph::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void WinStatsGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void WinStatsGraph::
+on_leave_label(int collector_index) {
+  if (collector_index == _highlighted_index && collector_index != -1) {
+    _highlighted_index = -1;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsGraph::
+get_label_tooltip(int collector_index) const {
+  return std::string();
+}
+
+/**
+ * Returns the window handle of the surrounding window.
+ */
+HWND WinStatsGraph::
+get_window() {
+  return _window;
 }
 
 /**
@@ -184,8 +220,8 @@ move_label_stack() {
     RECT rect;
     GetClientRect(_window, &rect);
 
-    rect.left += 8;
-    rect.right = _left_margin - 8;
+    rect.left += _pixel_scale * 2;
+    rect.right = _left_margin - _pixel_scale * 2;
     rect.bottom -= _bottom_margin;
 
     _label_stack.set_pos(rect.left, rect.top,
@@ -197,22 +233,27 @@ move_label_stack() {
  * Returns a brush suitable for drawing in the indicated collector's color.
  */
 HBRUSH WinStatsGraph::
-get_collector_brush(int collector_index) {
+get_collector_brush(int collector_index, bool highlight) {
   Brushes::iterator bi;
   bi = _brushes.find(collector_index);
   if (bi != _brushes.end()) {
-    return (*bi).second;
+    return highlight ? (*bi).second.second : (*bi).second.first;
   }
 
   // Ask the monitor what color this guy should be.
   LRGBColor rgb = _monitor->get_collector_color(collector_index);
-  int r = (int)(rgb[0] * 255.0f);
-  int g = (int)(rgb[1] * 255.0f);
-  int b = (int)(rgb[2] * 255.0f);
+  int r = (int)encode_sRGB_uchar((float)rgb[0]);
+  int g = (int)encode_sRGB_uchar((float)rgb[1]);
+  int b = (int)encode_sRGB_uchar((float)rgb[2]);
   HBRUSH brush = CreateSolidBrush(RGB(r, g, b));
 
-  _brushes[collector_index] = brush;
-  return brush;
+  int hr = (int)encode_sRGB_uchar((float)rgb[0] * 0.75f);
+  int hg = (int)encode_sRGB_uchar((float)rgb[1] * 0.75f);
+  int hb = (int)encode_sRGB_uchar((float)rgb[2] * 0.75f);
+  HBRUSH hbrush = CreateSolidBrush(RGB(hr, hg, hb));
+
+  _brushes[collector_index] = std::make_pair(brush, hbrush);
+  return highlight ? hbrush : brush;
 }
 
 /**
@@ -226,6 +267,20 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     close();
     break;
 
+  case WM_GETMINMAXINFO:
+    {
+      WINDOWINFO winfo;
+      GetWindowInfo(hwnd, &winfo);
+      MINMAXINFO &minmax = *(MINMAXINFO *)lparam;
+      minmax.ptMinTrackSize.x = (winfo.rcClient.left - winfo.rcWindow.left)
+                              + (winfo.rcWindow.right - winfo.rcClient.right)
+                              + (_right_margin + _left_margin);
+      minmax.ptMinTrackSize.y = (winfo.rcClient.top - winfo.rcWindow.top)
+                              + (winfo.rcWindow.bottom - winfo.rcClient.bottom)
+                              + (_bottom_margin + _top_margin);
+      return 0;
+    }
+
   case WM_SIZE:
     move_label_stack();
     InvalidateRect(hwnd, nullptr, TRUE);
@@ -319,8 +374,6 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       rect.bottom -= _bottom_margin;
 
       if (rect.right > rect.left && rect.bottom > rect.top) {
-        DrawEdge(hdc, &rect, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
-
         int graph_xsize = rect.right - rect.left;
         int graph_ysize = rect.bottom - rect.top;
         if (_bitmap_dc == 0 ||
@@ -339,6 +392,21 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       return 0;
     }
 
+  case WM_DRAWITEM:
+    if (wparam == IDC_GRAPH) {
+      const DRAWITEMSTRUCT &dis = *(DRAWITEMSTRUCT *)lparam;
+
+      // Repaint the graph by copying the backing pixmap in.
+      BitBlt(dis.hDC, 0, 0,
+             _bitmap_xsize, _bitmap_ysize,
+             _bitmap_dc, 0, 0,
+             SRCCOPY);
+
+      additional_graph_window_paint(dis.hDC);
+      return 0;
+    }
+    break;
+
   default:
     break;
   }
@@ -357,6 +425,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     force_redraw();
     break;
 
+  case WM_NCHITTEST:
+    // Necessary for mouse events to work; default returns HTTRANSPARENT
+    return HTCLIENT;
+
   case WM_LBUTTONDOWN:
     // Vector any uncaught WM_LBUTTONDOWN into the main window, so we can drag
     // margins, etc.
@@ -372,28 +444,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     ReleaseCapture();
     break;
 
-  case WM_PAINT:
-    {
-      // Repaint the graph by copying the backing pixmap in.
-      PAINTSTRUCT ps;
-      HDC hdc = BeginPaint(hwnd, &ps);
-
-      BitBlt(hdc, 0, 0,
-             _bitmap_xsize, _bitmap_ysize,
-             _bitmap_dc, 0, 0,
-             SRCCOPY);
-
-      additional_graph_window_paint(hdc);
-
-      EndPaint(hwnd, &ps);
-      return 0;
-    }
-
   default:
     break;
   }
 
-  return DefWindowProc(hwnd, msg, wparam, lparam);
+  return DefSubclassProc(hwnd, msg, wparam, lparam);
 }
 
 /**
@@ -506,61 +561,29 @@ create_graph_window() {
   }
 
   HINSTANCE application = GetModuleHandle(nullptr);
-  register_graph_window_class(application);
 
-  std::string window_title = "graph";
-  DWORD window_style = WS_CHILD | WS_CLIPSIBLINGS;
+  DWORD window_style = WS_CHILD | WS_CLIPSIBLINGS |
+                       SS_SUNKEN | SS_OWNERDRAW;
 
   _graph_window =
-    CreateWindow(_graph_window_class_name, window_title.c_str(), window_style,
-                 0, 0, 0, 0,
-                 _window, nullptr, application, 0);
+    CreateWindow(WC_STATIC, "", window_style, 0, 0, 0, 0,
+                 _window, (HMENU)IDC_GRAPH, application, 0);
   if (!_graph_window) {
     nout << "Could not create graph window!\n";
     exit(1);
   }
 
-  SetWindowLongPtr(_graph_window, 0, (LONG_PTR)this);
-}
-
-/**
- * Registers the window class for the stripChart window, if it has not already
- * been registered.
- */
-void WinStatsGraph::
-register_graph_window_class(HINSTANCE application) {
-  if (_graph_window_class_registered) {
-    return;
-  }
-
-  WNDCLASS wc;
-
-  ZeroMemory(&wc, sizeof(WNDCLASS));
-  wc.style = CS_DBLCLKS;
-  wc.lpfnWndProc = (WNDPROC)static_graph_window_proc;
-  wc.hInstance = application;
-  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
-  wc.hbrBackground = nullptr;
-  wc.lpszMenuName = nullptr;
-  wc.lpszClassName = _graph_window_class_name;
-
-  // Reserve space to associate the this pointer with the window.
-  wc.cbWndExtra = sizeof(WinStatsGraph *);
-
-  if (!RegisterClass(&wc)) {
-    nout << "Could not register graph window class!\n";
-    exit(1);
-  }
+  EnableWindow(_graph_window, TRUE);
 
-  _graph_window_class_registered = true;
+  SetWindowSubclass(_graph_window, &static_graph_subclass_proc, 1234, (DWORD_PTR)this);
 }
 
 /**
  *
  */
-LONG WINAPI WinStatsGraph::
-static_graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
-  WinStatsGraph *self = (WinStatsGraph *)GetWindowLongPtr(hwnd, 0);
+LRESULT WINAPI WinStatsGraph::
+static_graph_subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclass, DWORD_PTR ref_data) {
+  WinStatsGraph *self = (WinStatsGraph *)ref_data;
   if (self != nullptr && self->_graph_window == hwnd) {
     return self->graph_window_proc(hwnd, msg, wparam, lparam);
   } else {

+ 13 - 9
pandatool/src/win-stats/winStatsGraph.h

@@ -48,7 +48,7 @@ public:
 
   virtual void new_collector(int collector_index);
   virtual void new_data(int thread_index, int frame_number);
-  virtual void force_redraw();
+  virtual void force_redraw()=0;
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
   virtual void set_time_units(int unit_mask);
@@ -56,7 +56,12 @@ public:
   void set_pause(bool pause);
 
   void user_guide_bars_changed();
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
+  virtual void on_enter_label(int collector_index);
+  virtual void on_leave_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
+
+  HWND get_window();
 
 protected:
   void close();
@@ -64,7 +69,7 @@ protected:
   void setup_label_stack();
   void move_label_stack();
 
-  HBRUSH get_collector_brush(int collector_index);
+  HBRUSH get_collector_brush(int collector_index, bool highlight = false);
 
   LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
@@ -80,7 +85,7 @@ protected:
 
 protected:
   // Table of brushes for our various collectors.
-  typedef pmap<int, HBRUSH> Brushes;
+  typedef pmap<int, std::pair<HBRUSH, HBRUSH> > Brushes;
   Brushes _brushes;
 
   WinStatsMonitor *_monitor;
@@ -98,6 +103,7 @@ protected:
   int _bitmap_xsize, _bitmap_ysize;
   int _left_margin, _right_margin;
   int _top_margin, _bottom_margin;
+  int _pixel_scale;
 
   COLORREF _dark_color;
   COLORREF _light_color;
@@ -112,18 +118,16 @@ protected:
   double _drag_scale_start;
   int _drag_guide_bar;
 
+  int _highlighted_index = -1;
+
   bool _pause;
 
 private:
   void setup_bitmap(int xsize, int ysize);
   void release_bitmap();
   void create_graph_window();
-  static void register_graph_window_class(HINSTANCE application);
-
-  static LONG WINAPI static_graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
 
-  static bool _graph_window_class_registered;
-  static const char * const _graph_window_class_name;
+  static LRESULT WINAPI static_graph_subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclass, DWORD_PTR ref_data);
 
 protected:
   static DWORD graph_window_style;

Some files were not shown because too many files changed in this diff