Browse Source

Merge branch 'master' into shaderpipeline

rdb 5 years ago
parent
commit
5a8f76d529

+ 2 - 0
.github/workflows/ci.yml

@@ -4,6 +4,7 @@ on: [push, pull_request]
 jobs:
   cmake:
     name: CMake Buildsystem
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
 
     strategy:
       fail-fast: false
@@ -326,6 +327,7 @@ jobs:
         bash <(curl -s https://codecov.io/bash) -y ../.github/codecov.yml
 
   makepanda:
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     strategy:
       matrix:
         os: [ubuntu-16.04, windows-2016, macOS-latest]

+ 2 - 2
README.md

@@ -100,7 +100,7 @@ for you to install, depending on your distribution).
 The following command illustrates how to build Panda3D with some common
 options:
 ```bash
-python makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-gles2 --no-opencv
+python3 makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-gles2 --no-opencv
 ```
 
 You will probably see some warnings saying that it's unable to find several
@@ -113,7 +113,7 @@ If you are on Ubuntu, this command should cover the most frequently
 used third-party packages:
 
 ```bash
-sudo apt-get install build-essential pkg-config fakeroot python-dev libpng-dev libjpeg-dev libtiff-dev zlib1g-dev libssl-dev libx11-dev libgl1-mesa-dev libxrandr-dev libxxf86dga-dev libxcursor-dev bison flex libfreetype6-dev libvorbis-dev libeigen3-dev libopenal-dev libode-dev libbullet-dev nvidia-cg-toolkit libgtk2.0-dev libassimp-dev libopenexr-dev
+sudo apt-get install build-essential pkg-config fakeroot python3-dev libpng-dev libjpeg-dev libtiff-dev zlib1g-dev libssl-dev libx11-dev libgl1-mesa-dev libxrandr-dev libxxf86dga-dev libxcursor-dev bison flex libfreetype6-dev libvorbis-dev libeigen3-dev libopenal-dev libode-dev libbullet-dev nvidia-cg-toolkit libgtk2.0-dev libassimp-dev libopenexr-dev
 ```
 
 Once Panda3D has built, you can either install the .deb or .rpm package that

+ 134 - 15
direct/src/dist/FreezeTool.py

@@ -639,6 +639,51 @@ okMissing = [
     'direct.extensions_native.extensions_darwin',
     ]
 
+# Since around macOS 10.15, Apple's codesigning process has become more strict.
+# Appending data to the end of a Mach-O binary is now explicitly forbidden. The
+# solution is to embed our own segment into the binary so it can be properly
+# signed.
+mach_header_64_layout = '<IIIIIIII'
+
+# Each load command is guaranteed to start with the command identifier and
+# command size. We'll call this the "lc header".
+lc_header_layout = '<II'
+
+# Each Mach-O segment is made up of sections. We need to change both the segment
+# and section information, so we'll need to know the layout of a section as
+# well.
+section64_header_layout = '<16s16sQQIIIIIIII'
+
+# These are all of the load commands we'll need to modify parts of.
+LC_SEGMENT_64 = 0x19
+LC_DYLD_INFO_ONLY = 0x80000022
+LC_SYMTAB = 0x02
+LC_DYSYMTAB = 0x0B
+LC_FUNCTION_STARTS = 0x26
+LC_DATA_IN_CODE = 0x29
+
+lc_layouts = {
+    LC_SEGMENT_64: '<II16sQQQQIIII',
+    LC_DYLD_INFO_ONLY: '<IIIIIIIIIIII',
+    LC_SYMTAB: '<IIIIII',
+    LC_DYSYMTAB: '<IIIIIIIIIIIIIIIIIIII',
+    LC_FUNCTION_STARTS: '<IIII',
+    LC_DATA_IN_CODE: '<IIII',
+}
+
+# All of our modifications involve sliding some offsets, since we need to insert
+# our data in the middle of the binary (we can't just put the data at the end
+# since __LINKEDIT must be the last segment).
+lc_indices_to_slide = {
+    b'__PANDA': [4, 6],
+    b'__LINKEDIT': [3, 5],
+    LC_DYLD_INFO_ONLY: [2, 4, 8, 10],
+    LC_SYMTAB: [2, 4],
+    LC_DYSYMTAB: [14],
+    LC_FUNCTION_STARTS: [2],
+    LC_DATA_IN_CODE: [2],
+}
+
 class Freezer:
     class ModuleDef:
         def __init__(self, moduleName, filename = None,
@@ -1799,21 +1844,36 @@ class Freezer:
             # Align to page size, so that it can be mmapped.
             blob_align = 4096
 
-        # Add padding before the blob if necessary.
-        blob_offset = len(stub_data)
-        if (blob_offset & (blob_align - 1)) != 0:
-            pad = (blob_align - (blob_offset & (blob_align - 1)))
-            stub_data += (b'\0' * pad)
-            blob_offset += pad
-        assert (blob_offset % blob_align) == 0
-        assert blob_offset == len(stub_data)
-
         # Also determine the total blob size now.  Add padding to the end.
         blob_size = pool_offset + len(pool)
-        if blob_size & 31 != 0:
-            pad = (32 - (blob_size & 31))
+        if blob_size & (blob_align - 1) != 0:
+            pad = (blob_align - (blob_size & (blob_align - 1)))
             blob_size += pad
 
+        # TODO: Support creating custom sections in universal binaries.
+        append_blob = True
+        if self.platform.startswith('macosx') and len(bitnesses) == 1:
+            # If our deploy-stub has a __PANDA segment, we know we're meant to
+            # put our blob there rather than attach it to the end.
+            load_commands = self._parse_macho_load_commands(stub_data)
+            if b'__PANDA' in load_commands.keys():
+                append_blob = False
+
+        if self.platform.startswith("macosx") and not append_blob:
+            # Take this time to shift any Mach-O structures around to fit our
+            # blob. We don't need to worry about aligning the offset since the
+            # compiler already took care of that when creating the segment.
+            blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size)
+        else:
+            # Add padding before the blob if necessary.
+            blob_offset = len(stub_data)
+            if (blob_offset & (blob_align - 1)) != 0:
+                pad = (blob_align - (blob_offset & (blob_align - 1)))
+                stub_data += (b'\0' * pad)
+                blob_offset += pad
+            assert (blob_offset % blob_align) == 0
+            assert blob_offset == len(stub_data)
+
         # Calculate the offsets for the variables.  These are pointers,
         # relative to the beginning of the blob.
         field_offsets = {}
@@ -1893,9 +1953,13 @@ class Freezer:
             blob += struct.pack('<Q', blob_offset)
 
         with open(target, 'wb') as f:
-            f.write(stub_data)
-            assert f.tell() == blob_offset
-            f.write(blob)
+            if append_blob:
+                f.write(stub_data)
+                assert f.tell() == blob_offset
+                f.write(blob)
+            else:
+                stub_data[blob_offset:blob_offset + blob_size] = blob
+                f.write(stub_data)
 
         os.chmod(target, 0o755)
         return target
@@ -2153,7 +2217,9 @@ class Freezer:
                     symoff += nlist_size
                     name = strings[strx : strings.find(b'\0', strx)]
 
-                    if name == b'_' + symbol_name:
+                    # If the entry's type has any bits at 0xe0 set, it's a debug
+                    # symbol, and will point us to the wrong place.
+                    if name == b'_' + symbol_name and type & 0xe0 == 0:
                         # Find out in which segment this is.
                         for vmaddr, vmsize, fileoff in segments:
                             # Is it defined in this segment?
@@ -2163,6 +2229,59 @@ class Freezer:
                                 return fileoff + rel
                         print("Could not find memory address for symbol %s" % (symbol_name))
 
+    def _parse_macho_load_commands(self, macho_data):
+        """Returns the list of load commands from macho_data."""
+        mach_header_64 = list(
+            struct.unpack_from(mach_header_64_layout, macho_data, 0))
+
+        num_load_commands = mach_header_64[4]
+
+        load_commands = {}
+
+        curr_lc_offset = struct.calcsize(mach_header_64_layout)
+        for i in range(num_load_commands):
+            lc = struct.unpack_from(lc_header_layout, macho_data, curr_lc_offset)
+            layout = lc_layouts.get(lc[0])
+            if layout:
+                # Make it a list since we want to mutate it.
+                lc = list(struct.unpack_from(layout, macho_data, curr_lc_offset))
+
+                if lc[0] == LC_SEGMENT_64:
+                    stripped_name = lc[2].rstrip(b'\0')
+                    if stripped_name in [b'__PANDA', b'__LINKEDIT']:
+                        load_commands[stripped_name] = (curr_lc_offset, lc)
+                else:
+                    load_commands[lc[0]] = (curr_lc_offset, lc)
+
+            curr_lc_offset += lc[1]
+
+        return load_commands
+
+    def _shift_macho_structures(self, macho_data, load_commands, blob_size):
+        """Given the stub and the size of our blob, make room for it and edit
+        all of the necessary structures to keep the binary valid. Returns the
+        offset where the blob should be placed."""
+
+        for lc_key in load_commands.keys():
+            for index in lc_indices_to_slide[lc_key]:
+                load_commands[lc_key][1][index] += blob_size
+
+            if lc_key == b'__PANDA':
+                section_header_offset = load_commands[lc_key][0] + struct.calcsize(lc_layouts[LC_SEGMENT_64])
+                section_header = list(struct.unpack_from(section64_header_layout, macho_data, section_header_offset))
+                section_header[3] = blob_size
+                struct.pack_into(section64_header_layout, macho_data, section_header_offset, *section_header)
+
+            layout = LC_SEGMENT_64 if lc_key in [b'__PANDA', b'__LINKEDIT'] else lc_key
+            struct.pack_into(lc_layouts[layout], macho_data, load_commands[lc_key][0], *load_commands[lc_key][1])
+
+        blob_offset = load_commands[b'__PANDA'][1][5]
+
+        # Write in some null bytes until we write in the actual blob.
+        macho_data[blob_offset:blob_offset] = b'\0' * blob_size
+
+        return blob_offset
+
     def makeModuleDef(self, mangledName, code):
         result = ''
         result += 'static unsigned char %s[] = {' % (mangledName)

+ 5 - 1
direct/src/dist/commands.py

@@ -618,12 +618,16 @@ class build_apps(setuptools.Command):
                     rootdir = wf.split(os.path.sep, 1)[0]
                     search_path.append(os.path.join(whl, rootdir, '.libs'))
 
+                    # Also look for eg. numpy.libs or Pillow.libs in the root
+                    whl_name = os.path.basename(whl).split('-', 1)[0]
+                    search_path.append(os.path.join(whl, whl_name + '.libs'))
+
                     # Also look for more specific per-package cases, defined in
                     # PACKAGE_LIB_DIRS at the top of this file.
-                    whl_name = os.path.basename(whl).split('-', 1)[0]
                     extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
                     for extra_dir in extra_dirs:
                         search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
+
             return search_path
 
         def create_runtime(appname, mainscript, use_console):

+ 5 - 5
direct/src/showbase/DirectObject.py

@@ -94,12 +94,12 @@ class DirectObject:
         if hasattr(self, '_taskList'):
             tasks = [task.name for task in self._taskList.values()]
         if len(events) or len(tasks):
-            estr = choice(len(events), 'listening to events: %s' % events, '')
-            andStr = choice(len(events) and len(tasks), ' and ', '')
-            tstr = choice(len(tasks), '%srunning tasks: %s' % (andStr, tasks), '')
+            estr = ('listening to events: %s' % events if len(events) else '')
+            andStr = (' and ' if len(events) and len(tasks) else '')
+            tstr = ('%srunning tasks: %s' % (andStr, tasks) if len(tasks) else '')
             notify = directNotify.newCategory('LeakDetect')
-            func = choice(getRepository()._crashOnProactiveLeakDetect,
-                          self.notify.error, self.notify.warning)
+            crash = getattr(getRepository(), '_crashOnProactiveLeakDetect', False)
+            func = (self.notify.error if crash else self.notify.warning)
             func('destroyed %s instance is still %s%s' % (self.__class__.__name__, estr, tstr))
 
     #snake_case alias:

+ 8 - 8
dtool/src/parser-inc/map

@@ -28,19 +28,19 @@ namespace std {
   template<class T> class allocator;
 }
 
-template<class Key, class Element, class Compare = less<Key>, class Allocator = std::allocator<pair<const Key, T> > >
+template<class Key, class T, class Compare = less<Key>, class Allocator = std::allocator<pair<const Key, T> > >
 class map {
 public:
   typedef Key key_type;
-  typedef Element data_type;
-  typedef Element mapped_type;
-  typedef pair<const Key, Element> value_type;
+  typedef T data_type;
+  typedef T mapped_type;
+  typedef pair<const Key, T> value_type;
   typedef Compare key_compare;
 
-  typedef Element *pointer;
-  typedef const Element *const_pointer;
-  typedef Element &reference;
-  typedef const Element &const_reference;
+  typedef T *pointer;
+  typedef const T *const_pointer;
+  typedef T &reference;
+  typedef const T &const_reference;
 
   class iterator;
   class const_iterator;

+ 1 - 1
makepanda/makepanda.py

@@ -168,7 +168,7 @@ def parseopts(args):
     # Options for which to display a deprecation warning.
     removedopts = [
         "use-touchinput", "no-touchinput", "no-awesomium", "no-directscripts",
-        "no-carbon", "universal", "no-physx", "no-rocket"
+        "no-carbon", "universal", "no-physx", "no-rocket", "host"
         ]
 
     # All recognized options.

+ 1 - 0
panda/src/cocoadisplay/cocoaGraphicsWindow.h

@@ -51,6 +51,7 @@ public:
   void handle_move_event();
   void handle_resize_event();
   void handle_minimize_event(bool minimized);
+  void handle_maximize_event(bool maximized);
   void handle_foreground_event(bool foreground);
   bool handle_close_request();
   void handle_close_event();

+ 52 - 2
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -405,6 +405,9 @@ open_window() {
   if (!_properties.has_minimized()) {
     _properties.set_minimized(false);
   }
+  if (!_properties.has_maximized()) {
+    _properties.set_maximized(false);
+  }
   if (!_properties.has_z_order()) {
     _properties.set_z_order(WindowProperties::Z_normal);
   }
@@ -713,7 +716,7 @@ close_window() {
 
   if (_window != nil) {
     [_window close];
-    
+
     // Process events once more so any pending NSEvents are cleared. Not doing
     // this causes the window to stick around after calling [_window close].
     process_events();
@@ -925,6 +928,14 @@ set_properties_now(WindowProperties &properties) {
     properties.clear_origin();
   }
 
+  if (properties.has_maximized() && _window != nil) {
+    _properties.set_maximized(properties.get_maximized());
+    if (properties.get_maximized() != !![_window isZoomed]) {
+      [_window zoom:nil];
+    }
+    properties.clear_maximized();
+  }
+
   if (properties.has_title() && _window != nil) {
     _properties.set_title(properties.get_title());
     [_window setTitle:[NSString stringWithUTF8String:properties.get_title().c_str()]];
@@ -1404,10 +1415,12 @@ handle_resize_event() {
 
   NSRect frame = [_view convertRect:[_view bounds] toView:nil];
 
+  WindowProperties properties;
+  bool changed = false;
+
   if (frame.size.width != _properties.get_x_size() ||
       frame.size.height != _properties.get_y_size()) {
 
-    WindowProperties properties;
     properties.set_size(frame.size.width, frame.size.height);
 
     if (cocoadisplay_cat.is_spam()) {
@@ -1415,6 +1428,18 @@ handle_resize_event() {
         << "Window changed size to (" << frame.size.width
        << ", " << frame.size.height << ")\n";
     }
+    changed = true;
+  }
+
+  if (_window != nil) {
+    bool is_maximized = [_window isZoomed];
+    if (is_maximized != _properties.get_maximized()) {
+      properties.set_maximized(is_maximized);
+      changed = true;
+    }
+  }
+
+  if (changed) {
     system_changed_properties(properties);
   }
 
@@ -1444,6 +1469,31 @@ handle_minimize_event(bool minimized) {
   system_changed_properties(properties);
 }
 
+/**
+ * Called by the window delegate when the window is maximized or
+ * demaximized.
+ */
+void CocoaGraphicsWindow::
+handle_maximize_event(bool maximized) {
+  if (maximized == _properties.get_maximized()) {
+    return;
+  }
+
+  if (cocoadisplay_cat.is_debug()) {
+    if (maximized) {
+      cocoadisplay_cat.debug() << "Window was maximized\n";
+    } else {
+      cocoadisplay_cat.debug() << "Window was demaximized\n";
+    }
+  }
+
+  WindowProperties properties;
+  properties.set_maximized(maximized);
+  system_changed_properties(properties);
+}
+
+
+
 /**
  * Called by the window delegate when the window has become the key window or
  * resigned that status.

+ 7 - 0
panda/src/display/config_display.cxx

@@ -328,6 +328,13 @@ ConfigVariableInt win_origin
 ConfigVariableBool fullscreen
 ("fullscreen", false);
 
+ConfigVariableBool maximized
+("maximized", false,
+ PRC_DESC("Start the window in a maximized state as handled by the window"
+          "manager.  In comparison to the fullscreen setting, this will"
+          "usually not remove the window decoration and not occupy the"
+          "whole screen space."));
+
 ConfigVariableBool undecorated
 ("undecorated", false,
  PRC_DESC("This specifies the default value of the 'undecorated' window "

+ 1 - 0
panda/src/display/config_display.h

@@ -73,6 +73,7 @@ extern EXPCL_PANDA_DISPLAY ConfigVariableBool old_alpha_blend;
 extern EXPCL_PANDA_DISPLAY ConfigVariableInt win_size;
 extern EXPCL_PANDA_DISPLAY ConfigVariableInt win_origin;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool fullscreen;
+extern EXPCL_PANDA_DISPLAY ConfigVariableBool maximized;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool undecorated;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool win_fixed_size;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool cursor_hidden;

+ 8 - 1
panda/src/display/graphicsStateGuardian.cxx

@@ -1983,7 +1983,14 @@ fetch_specified_texture(Shader::ShaderTexSpec &spec, SamplerState &sampler,
         Light *light_obj = light.node()->as_light();
         nassertr(light_obj != nullptr, nullptr);
 
-        PT(Texture) tex = get_shadow_map(light);
+        PT(Texture) tex;
+        LightLensNode *lln = DCAST(LightLensNode, light.node());
+        if (lln != nullptr && lln->_shadow_caster) {
+          tex = get_shadow_map(light);
+        } else {
+          tex = get_dummy_shadow_map((Texture::TextureType)spec._desired_type);
+        }
+
         if (tex != nullptr) {
           sampler = tex->get_default_sampler();
         }

+ 1 - 0
panda/src/display/graphicsWindow.cxx

@@ -54,6 +54,7 @@ GraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
   _properties.set_undecorated(false);
   _properties.set_fullscreen(false);
   _properties.set_minimized(false);
+  _properties.set_maximized(false);
   _properties.set_cursor_hidden(false);
 
   request_properties(WindowProperties::get_default());

+ 39 - 0
panda/src/display/windowProperties.I

@@ -407,6 +407,45 @@ clear_minimized() {
   _flags &= ~F_minimized;
 }
 
+/**
+ * Specifies whether the window should be created maximized (true), or normal
+ * (false).
+ */
+INLINE void WindowProperties::
+set_maximized(bool maximized) {
+  if (maximized) {
+    _flags |= F_maximized;
+  } else {
+    _flags &= ~F_maximized;
+  }
+  _specified |= S_maximized;
+}
+
+/**
+ * Returns true if the window is maximized.
+ */
+INLINE bool WindowProperties::
+get_maximized() const {
+  return (_flags & F_maximized) != 0;
+}
+
+/**
+ * Returns true if set_maximized() has been specified.
+ */
+INLINE bool WindowProperties::
+has_maximized() const {
+  return ((_specified & S_maximized) != 0);
+}
+
+/**
+ * Removes the maximized specification from the properties.
+ */
+INLINE void WindowProperties::
+clear_maximized() {
+  _specified &= ~S_maximized;
+  _flags &= ~F_maximized;
+}
+
 /**
  * Specifies whether the window should read the raw mouse devices.
  */

+ 7 - 0
panda/src/display/windowProperties.cxx

@@ -69,6 +69,7 @@ get_config_properties() {
   props.set_fullscreen(fullscreen);
   props.set_undecorated(undecorated);
   props.set_fixed_size(win_fixed_size);
+  props.set_maximized(maximized);
   props.set_cursor_hidden(cursor_hidden);
   if (!icon_filename.empty()) {
     props.set_icon_filename(icon_filename);
@@ -240,6 +241,9 @@ add_properties(const WindowProperties &other) {
   if (other.has_minimized()) {
     set_minimized(other.get_minimized());
   }
+  if (other.has_maximized()) {
+    set_maximized(other.get_maximized());
+  }
   if (other.has_raw_mice()) {
     set_raw_mice(other.get_raw_mice());
   }
@@ -296,6 +300,9 @@ output(ostream &out) const {
   if (has_minimized()) {
     out << (get_minimized() ? "minimized " : "!minimized ");
   }
+  if (has_maximized()) {
+    out << (get_maximized() ? "maximized " : "!maximized ");
+  }
   if (has_raw_mice()) {
     out << (get_raw_mice() ? "raw_mice " : "!raw_mice ");
   }

+ 9 - 0
panda/src/display/windowProperties.h

@@ -132,6 +132,13 @@ PUBLISHED:
   MAKE_PROPERTY2(minimized, has_minimized, get_minimized,
                             set_minimized, clear_minimized);
 
+  INLINE void set_maximized(bool maximized);
+  INLINE bool get_maximized() const;
+  INLINE bool has_maximized() const;
+  INLINE void clear_maximized();
+  MAKE_PROPERTY2(maximized, has_maximized, get_maximized,
+                            set_maximized, clear_maximized);
+
   INLINE void set_raw_mice(bool raw_mice);
   INLINE bool get_raw_mice() const;
   INLINE bool has_raw_mice() const;
@@ -202,6 +209,7 @@ private:
     S_mouse_mode           = 0x02000,
     S_parent_window        = 0x04000,
     S_raw_mice             = 0x08000,
+    S_maximized            = 0x10000,
   };
 
   // This bitmask represents the truefalse settings for various boolean flags
@@ -211,6 +219,7 @@ private:
     F_fullscreen     = S_fullscreen,
     F_foreground     = S_foreground,
     F_minimized      = S_minimized,
+    F_maximized      = S_maximized,
     F_open           = S_open,
     F_cursor_hidden  = S_cursor_hidden,
     F_fixed_size     = S_fixed_size,

+ 25 - 13
panda/src/pgraph/textureAttrib.cxx

@@ -109,12 +109,18 @@ add_on_stage(TextureStage *stage, Texture *tex, int override) const {
   nassertr(tex != nullptr, this);
 
   TextureAttrib *attrib = new TextureAttrib(*this);
-  Stages::iterator si = attrib->_on_stages.insert(StageNode(stage)).first;
-  (*si)._override = override;
-  (*si)._texture = tex;
-  (*si)._implicit_sort = attrib->_next_implicit_sort;
-  (*si)._has_sampler = false;
-  ++(attrib->_next_implicit_sort);
+  auto result = attrib->_on_stages.insert(StageNode(stage));
+  StageNode &sn = *result.first;
+  sn._override = override;
+  sn._texture = tex;
+  sn._has_sampler = false;
+
+  // Only bump this if it doesn't already have the highest implicit sort.
+  // This prevents replacing a texture from creating a unique TextureAttrib.
+  if (result.second || sn._implicit_sort + 1 != attrib->_next_implicit_sort) {
+    sn._implicit_sort = attrib->_next_implicit_sort;
+    ++(attrib->_next_implicit_sort);
+  }
 
   return return_new(attrib);
 }
@@ -128,13 +134,19 @@ add_on_stage(TextureStage *stage, Texture *tex, const SamplerState &sampler, int
   nassertr(tex != nullptr, this);
 
   TextureAttrib *attrib = new TextureAttrib(*this);
-  Stages::iterator si = attrib->_on_stages.insert(StageNode(stage)).first;
-  (*si)._override = override;
-  (*si)._texture = tex;
-  (*si)._sampler = sampler;
-  (*si)._implicit_sort = attrib->_next_implicit_sort;
-  (*si)._has_sampler = true;
-  ++(attrib->_next_implicit_sort);
+  auto result = attrib->_on_stages.insert(StageNode(stage));
+  StageNode &sn = *result.first;
+  sn._override = override;
+  sn._texture = tex;
+  sn._sampler = sampler;
+  sn._has_sampler = true;
+
+  // Only bump this if it doesn't already have the highest implicit sort.
+  // This prevents replacing a texture from creating a unique TextureAttrib.
+  if (result.second || sn._implicit_sort + 1 != attrib->_next_implicit_sort) {
+    sn._implicit_sort = attrib->_next_implicit_sort;
+    ++(attrib->_next_implicit_sort);
+  }
 
   return return_new(attrib);
 }

+ 9 - 0
panda/src/pgui/pgItem.h

@@ -141,6 +141,15 @@ PUBLISHED:
   INLINE const std::string &get_id() const;
   INLINE void set_id(const std::string &id);
 
+  MAKE_PROPERTY(name, get_name, set_name);
+  MAKE_PROPERTY2(frame, has_frame, get_frame, set_frame, clear_frame);
+  MAKE_PROPERTY(state, get_state, set_state);
+  MAKE_PROPERTY(active, get_active, set_active);
+  MAKE_PROPERTY(focus, get_focus, set_focus);
+  MAKE_PROPERTY(background_focus, get_background_focus, set_background_focus);
+  MAKE_PROPERTY(suppress_flags, get_suppress_flags, set_suppress_flags);
+  MAKE_PROPERTY(id, get_id, set_id);
+
   INLINE static std::string get_enter_prefix();
   INLINE static std::string get_exit_prefix();
   INLINE static std::string get_within_prefix();

+ 60 - 14
panda/src/windisplay/winGraphicsWindow.cxx

@@ -306,22 +306,29 @@ set_properties_now(WindowProperties &properties) {
     LPoint2i bottom_right = top_left + _properties.get_size();
 
     DWORD window_style = make_style(_properties);
+    DWORD current_style = GetWindowLong(_hWnd, GWL_STYLE);
     SetWindowLong(_hWnd, GWL_STYLE, window_style);
 
-    // Now calculate the proper size and origin with the new window style.
-    RECT view_rect;
-    SetRect(&view_rect, top_left[0], top_left[1],
-            bottom_right[0], bottom_right[1]);
-    WINDOWINFO wi;
-    GetWindowInfo(_hWnd, &wi);
-    AdjustWindowRectEx(&view_rect, wi.dwStyle, FALSE, wi.dwExStyle);
-
-    // We need to call this to ensure that the style change takes effect.
-    SetWindowPos(_hWnd, HWND_NOTOPMOST, view_rect.left, view_rect.top,
-                 view_rect.right - view_rect.left,
-                 view_rect.bottom - view_rect.top,
-                 SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED |
-                 SWP_NOSENDCHANGING | SWP_SHOWWINDOW);
+    // If we switched to/from undecorated, calculate the new size.
+    if (((window_style ^ current_style) & WS_CAPTION) != 0) {
+      RECT view_rect;
+      SetRect(&view_rect, top_left[0], top_left[1],
+              bottom_right[0], bottom_right[1]);
+      WINDOWINFO wi;
+      GetWindowInfo(_hWnd, &wi);
+      AdjustWindowRectEx(&view_rect, wi.dwStyle, FALSE, wi.dwExStyle);
+
+      SetWindowPos(_hWnd, HWND_NOTOPMOST, view_rect.left, view_rect.top,
+                   view_rect.right - view_rect.left,
+                   view_rect.bottom - view_rect.top,
+                   SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED |
+                   SWP_NOSENDCHANGING | SWP_SHOWWINDOW);
+    } else {
+      // We need to call this to ensure that the style change takes effect.
+      SetWindowPos(_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0,
+                   SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE |
+                   SWP_FRAMECHANGED | SWP_NOSENDCHANGING | SWP_SHOWWINDOW);
+    }
   }
 
   if (properties.has_title()) {
@@ -402,6 +409,23 @@ set_properties_now(WindowProperties &properties) {
     properties.clear_minimized();
   }
 
+  if (properties.has_maximized()) {
+    if (_properties.get_maximized() != properties.get_maximized()) {
+      if (properties.get_maximized()) {
+        ShowWindow(_hWnd, SW_MAXIMIZE);
+      } else {
+        ShowWindow(_hWnd, SW_RESTORE);
+      }
+      _properties.set_maximized(properties.get_maximized());
+
+      if (_properties.get_minimized()) {
+        // Immediately minimize it again
+        ShowWindow(_hWnd, SW_MINIMIZE);
+      }
+    }
+    properties.clear_maximized();
+  }
+
   if (properties.has_fullscreen()) {
     if (properties.get_fullscreen() && !is_fullscreen()) {
       if (do_fullscreen_switch()){
@@ -507,6 +531,7 @@ open_window() {
   }
   bool want_foreground = (!_properties.has_foreground() || _properties.get_foreground());
   bool want_minimized = (_properties.has_minimized() && _properties.get_minimized()) && !want_foreground;
+  bool want_maximized = (_properties.has_maximized() && _properties.get_maximized()) && want_foreground;
 
   HWND old_foreground_window = GetForegroundWindow();
 
@@ -533,6 +558,9 @@ open_window() {
   if (want_minimized) {
     ShowWindow(_hWnd, SW_MINIMIZE);
     ShowWindow(_hWnd, SW_MINIMIZE);
+  } else if (want_maximized) {
+    ShowWindow(_hWnd, SW_MAXIMIZE);
+    ShowWindow(_hWnd, SW_MAXIMIZE);
   } else {
     ShowWindow(_hWnd, SW_SHOWNORMAL);
     ShowWindow(_hWnd, SW_SHOWNORMAL);
@@ -854,6 +882,21 @@ handle_reshape() {
       << "," << properties.get_y_size() << ")\n";
   }
 
+  // Check whether the window has been maximized or unmaximized.
+  WINDOWPLACEMENT pl;
+  pl.length = sizeof(WINDOWPLACEMENT);
+  if (GetWindowPlacement(_hWnd, &pl)) {
+    if (pl.showCmd == SW_SHOWMAXIMIZED || (pl.flags & WPF_RESTORETOMAXIMIZED) != 0) {
+      properties.set_maximized(true);
+    } else {
+      properties.set_maximized(false);
+    }
+  }
+  else if (windisplay_cat.is_debug()) {
+    windisplay_cat.debug()
+      << "GetWindowPlacement() failed in handle_reshape.  Ignoring.\n";
+  }
+
   adjust_z_order();
   system_changed_properties(properties);
 }
@@ -1083,6 +1126,9 @@ calculate_metrics(bool fullscreen, DWORD window_style, WINDOW_METRICS &metrics,
 bool WinGraphicsWindow::
 open_graphic_window() {
   DWORD window_style = make_style(_properties);
+  if (_properties.get_maximized()) {
+    window_style |= WS_MAXIMIZE;
+  }
 
   wstring title;
   if (_properties.has_title()) {

+ 2 - 0
panda/src/x11display/x11GraphicsPipe.cxx

@@ -363,6 +363,8 @@ x11GraphicsPipe(const std::string &display) :
   _net_wm_state_add = XInternAtom(_display, "_NET_WM_STATE_ADD", false);
   _net_wm_state_remove = XInternAtom(_display, "_NET_WM_STATE_REMOVE", false);
   _net_wm_bypass_compositor = XInternAtom(_display, "_NET_WM_BYPASS_COMPOSITOR", false);
+  _net_wm_state_maximized_vert = XInternAtom(_display, "_NET_WM_STATE_MAXIMIZED_VERT", false);
+  _net_wm_state_maximized_horz = XInternAtom(_display, "_NET_WM_STATE_MAXIMIZED_HORZ", false);
 }
 
 /**

+ 2 - 0
panda/src/x11display/x11GraphicsPipe.h

@@ -177,6 +177,8 @@ public:
   Atom _net_wm_state_add;
   Atom _net_wm_state_remove;
   Atom _net_wm_bypass_compositor;
+  Atom _net_wm_state_maximized_vert;
+  Atom _net_wm_state_maximized_horz;
 
   // Extension functions.
   typedef int (*pfn_XcursorGetDefaultSize)(X11_Display *);

+ 75 - 1
panda/src/x11display/x11GraphicsWindow.cxx

@@ -309,6 +309,9 @@ process_events() {
   WindowProperties properties;
   bool changed_properties = false;
 
+  XPropertyEvent property_event;
+  bool got_net_wm_state_change = false;
+
   while (XCheckIfEvent(_display, &event, check_event, (char *)this)) {
     if (got_keyrelease_event) {
       // If a keyrelease event is immediately followed by a matching keypress
@@ -362,6 +365,19 @@ process_events() {
     case ReparentNotify:
       break;
 
+    case PropertyNotify:
+      //std::cout << "PropertyNotify event: atom = " << event.xproperty.atom << std::endl;
+      x11GraphicsPipe *x11_pipe;
+      DCAST_INTO_V(x11_pipe, _pipe);
+      if (event.xproperty.atom == x11_pipe->_net_wm_state) {
+        // currently we're only interested in the net_wm_state type of
+        // changes and only need to gather property informations once at
+        // the end after the while loop
+        property_event = event.xproperty;
+        got_net_wm_state_change = true;
+      }
+      break;
+
     case ConfigureNotify:
       // When resizing or moving the window, multiple ConfigureNotify events
       // may be sent in rapid succession.  We only respond to the last one.
@@ -585,6 +601,45 @@ process_events() {
       }
   }
 
+  if (got_net_wm_state_change) {
+    // some wm state properties have been changed, check their values
+    // once in this part instead of multiple times in the while loop
+
+    // Check if this window is maximized or not
+    bool is_maximized = false;
+    Atom wmState = property_event.atom;
+    Atom type;
+    int format;
+    unsigned long nItem, bytesAfter;
+    unsigned char *new_window_properties = NULL;
+    // gather all properties from the active dispplay and window
+    XGetWindowProperty(_display, _xwindow, wmState, 0, LONG_MAX, false, AnyPropertyType, &type, &format, &nItem, &bytesAfter, &new_window_properties);
+    if (nItem > 0) {
+      x11GraphicsPipe *x11_pipe;
+      DCAST_INTO_V(x11_pipe, _pipe);
+      // run through all found items
+      for (unsigned long iItem = 0; iItem < nItem; ++iItem) {
+        unsigned long item = reinterpret_cast<unsigned long *>(new_window_properties)[iItem];
+        // check if the item is one of the maximized states
+        if (item == x11_pipe->_net_wm_state_maximized_horz ||
+            item == x11_pipe->_net_wm_state_maximized_vert) {
+          // The window was maximized
+          is_maximized = true;
+        }
+      }
+    }
+
+    // Debug entry
+    if (x11display_cat.is_debug()) {
+      x11display_cat.debug()
+        << "set maximized to: " << is_maximized << "\n";
+    }
+
+    // Now make sure the property will get stored correctly
+    properties.set_maximized(is_maximized);
+    changed_properties = true;
+  }
+
   if (changed_properties) {
     system_changed_properties(properties);
   }
@@ -791,6 +846,12 @@ set_properties_now(WindowProperties &properties) {
     properties.clear_fullscreen();
   }
 
+  // Same for maximized.
+  if (properties.has_maximized()) {
+    _properties.set_maximized(properties.get_maximized());
+    properties.clear_maximized();
+  }
+
   // The size and position of an already-open window are changed via explicit
   // X calls.  These may still get intercepted by the window manager.  Rather
   // than changing _properties immediately, we'll wait for the ConfigureNotify
@@ -1101,7 +1162,8 @@ open_window() {
     KeyPressMask | KeyReleaseMask |
     EnterWindowMask | LeaveWindowMask |
     PointerMotionMask |
-    FocusChangeMask | StructureNotifyMask;
+    FocusChangeMask | StructureNotifyMask |
+    PropertyChangeMask;
 
   // Initialize window attributes
   XSetWindowAttributes wa;
@@ -1269,6 +1331,18 @@ set_wm_properties(const WindowProperties &properties, bool already_mapped) {
   SetAction set_data[max_set_data];
   int next_set_data = 0;
 
+  if (properties.has_maximized()) {
+    if (properties.get_maximized()) {
+      state_data[next_state_data++] = x11_pipe->_net_wm_state_maximized_vert;
+      set_data[next_set_data++] = SetAction(x11_pipe->_net_wm_state_maximized_vert, 1);
+      state_data[next_state_data++] = x11_pipe->_net_wm_state_maximized_horz;
+      set_data[next_set_data++] = SetAction(x11_pipe->_net_wm_state_maximized_horz, 1);
+    } else {
+      set_data[next_set_data++] = SetAction(x11_pipe->_net_wm_state_maximized_vert, 0);
+      set_data[next_set_data++] = SetAction(x11_pipe->_net_wm_state_maximized_horz, 0);
+    }
+  }
+
   if (properties.has_fullscreen()) {
     if (properties.get_fullscreen()) {
       // For a "fullscreen" request, we pass this through, hoping the window

+ 20 - 0
tests/display/test_winprops.py

@@ -66,3 +66,23 @@ def test_winprops_size_property():
     # Test clear
     props.size = None
     assert not props.has_size()
+
+
+def test_winprops_maximized_property():
+    props = WindowProperties()
+
+    # Test get
+    props.set_maximized(True)
+    assert props.maximized == True
+
+    # Test has
+    props.clear_maximized()
+    assert props.maximized is None
+
+    # Test set
+    props.maximized = True
+    assert props.get_maximized() == True
+
+    # Test clear
+    props.maximized = None
+    assert not props.has_maximized()

+ 18 - 0
tests/pgraph/test_cullfaceattrib.py

@@ -0,0 +1,18 @@
+from panda3d.core import CullFaceAttrib
+
+
+def test_cullfaceattrib_compare():
+    clockwise1 = CullFaceAttrib.make()
+    clockwise2 = CullFaceAttrib.make()
+    reverse1 = CullFaceAttrib.make_reverse()
+    reverse2 = CullFaceAttrib.make_reverse()
+
+    assert clockwise1.compare_to(clockwise2) == 0
+    assert clockwise2.compare_to(clockwise1) == 0
+
+    assert reverse1.compare_to(reverse2) == 0
+    assert reverse2.compare_to(reverse1) == 0
+
+    assert reverse1.compare_to(clockwise1) != 0
+    assert clockwise1.compare_to(reverse1) != 0
+    assert reverse1.compare_to(clockwise1) == -clockwise1.compare_to(reverse1)

+ 34 - 0
tests/pgraph/test_lightattrib.py

@@ -107,3 +107,37 @@ def test_lightattrib_compare():
     assert lattr1.compare_to(lattr2) != 0
     assert lattr2.compare_to(lattr1) != 0
     assert lattr2.compare_to(lattr1) == -lattr1.compare_to(lattr2)
+
+    # An on light is not the same as an off light
+    lattr1 = core.LightAttrib.make().add_on_light(spot)
+    lattr2 = core.LightAttrib.make().add_off_light(spot)
+    assert lattr1.compare_to(lattr2) != 0
+    assert lattr2.compare_to(lattr1) != 0
+    assert lattr2.compare_to(lattr1) == -lattr1.compare_to(lattr2)
+
+    # If both have the same off light, they are equal
+    lattr1 = core.LightAttrib.make().add_off_light(spot)
+    lattr2 = core.LightAttrib.make().add_off_light(spot)
+    assert lattr1.compare_to(lattr2) == 0
+    assert lattr2.compare_to(lattr1) == 0
+
+    # Off light should not be equal to empty
+    lattr1 = core.LightAttrib.make().add_off_light(spot)
+    lattr2 = core.LightAttrib.make_all_off()
+    assert lattr1.compare_to(lattr2) != 0
+    assert lattr2.compare_to(lattr1) != 0
+    assert lattr2.compare_to(lattr1) == -lattr1.compare_to(lattr2)
+
+    # Off light should not be equal to all-off
+    lattr1 = core.LightAttrib.make().add_off_light(spot)
+    lattr2 = core.LightAttrib.make_all_off()
+    assert lattr1.compare_to(lattr2) != 0
+    assert lattr2.compare_to(lattr1) != 0
+    assert lattr2.compare_to(lattr1) == -lattr1.compare_to(lattr2)
+
+    # Different off lights shouldn't be equal either, of course
+    lattr1 = core.LightAttrib.make().add_off_light(spot)
+    lattr2 = core.LightAttrib.make().add_off_light(point)
+    assert lattr1.compare_to(lattr2) != 0
+    assert lattr2.compare_to(lattr1) != 0
+    assert lattr2.compare_to(lattr1) == -lattr1.compare_to(lattr2)

+ 5 - 1
tests/pgraph/test_shaderattrib.py

@@ -48,10 +48,14 @@ def test_shaderattrib_compare():
     assert shattr1.compare_to(shattr2) == 0
     assert shattr2.compare_to(shattr1) == 0
 
-    shattr2 = core.ShaderAttrib.make().set_flag(core.ShaderAttrib.F_subsume_alpha_test, True)
+    shattr2 = core.ShaderAttrib.make().set_flag(core.ShaderAttrib.F_subsume_alpha_test, False)
     assert shattr1.compare_to(shattr2) != 0
     assert shattr2.compare_to(shattr1) != 0
 
     shattr1 = core.ShaderAttrib.make().set_flag(core.ShaderAttrib.F_subsume_alpha_test, False)
+    assert shattr1.compare_to(shattr2) == 0
+    assert shattr2.compare_to(shattr1) == 0
+
+    shattr2 = core.ShaderAttrib.make().set_flag(core.ShaderAttrib.F_subsume_alpha_test, True)
     assert shattr1.compare_to(shattr2) != 0
     assert shattr2.compare_to(shattr1) != 0

+ 59 - 0
tests/pgraph/test_textureattrib.py

@@ -9,6 +9,19 @@ tex2 = core.Texture("tex2")
 tex3 = core.Texture("tex3")
 
 
+def test_textureattrib_compose_empty():
+    # Tests a case in which a child node does not alter the original.
+    tattr1 = core.TextureAttrib.make()
+    tattr1 = tattr1.add_on_stage(stage1, tex1)
+
+    tattr2 = core.TextureAttrib.make()
+
+    tattr3 = tattr1.compose(tattr2)
+    assert tattr3.get_num_on_stages() == 1
+
+    assert stage1 in tattr3.on_stages
+
+
 def test_textureattrib_compose_add():
     # Tests a case in which a child node adds another texture.
     tattr1 = core.TextureAttrib.make()
@@ -24,6 +37,21 @@ def test_textureattrib_compose_add():
     assert stage2 in tattr3.on_stages
 
 
+def test_textureattrib_compose_override():
+    # Tests a case in which a child node overrides a texture.
+    tattr1 = core.TextureAttrib.make()
+    tattr1 = tattr1.add_on_stage(stage1, tex1)
+
+    tattr2 = core.TextureAttrib.make()
+    tattr2 = tattr2.add_on_stage(stage1, tex2)
+
+    tattr3 = tattr1.compose(tattr2)
+    assert tattr3.get_num_on_stages() == 1
+
+    assert stage1 in tattr3.on_stages
+    assert tattr3.get_on_texture(stage1) == tex2
+
+
 def test_textureattrib_compose_subtract():
     # Tests a case in which a child node disables a texture.
     tattr1 = core.TextureAttrib.make()
@@ -77,6 +105,37 @@ def test_textureattrib_compose_alloff():
     assert tattr3.has_all_off()
 
 
+def test_textureattrib_implicit_sort():
+    # Tests that two TextureStages with same sort retain insertion order.
+    tattr1 = core.TextureAttrib.make()
+    tattr1 = tattr1.add_on_stage(stage1, tex1)
+    tattr1 = tattr1.add_on_stage(stage2, tex2)
+
+    assert tattr1.get_on_stage(0) == stage1
+    assert tattr1.get_on_stage(1) == stage2
+
+    tattr2 = core.TextureAttrib.make()
+    tattr2 = tattr2.add_on_stage(stage2, tex2)
+    tattr2 = tattr2.add_on_stage(stage1, tex1)
+
+    assert tattr2.get_on_stage(0) == stage2
+    assert tattr2.get_on_stage(1) == stage1
+
+    assert tattr1.compare_to(tattr2) == -tattr2.compare_to(tattr1)
+
+
+def test_textureattrib_replace():
+    # Test that replacing a texture doesn't create a unique TextureAttrib.
+    tattr1 = core.TextureAttrib.make()
+    tattr1 = tattr1.add_on_stage(stage1, tex1)
+
+    tattr2 = tattr1.add_on_stage(stage1, tex1)
+
+    assert tattr1.get_num_on_stages() == 1
+    assert tattr2.get_num_on_stages() == 1
+    assert tattr1.compare_to(tattr2) == 0
+
+
 def test_textureattrib_compare():
     tattr1 = core.TextureAttrib.make()
     tattr2 = core.TextureAttrib.make()