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:
 jobs:
   cmake:
   cmake:
     name: CMake Buildsystem
     name: CMake Buildsystem
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
 
 
     strategy:
     strategy:
       fail-fast: false
       fail-fast: false
@@ -326,6 +327,7 @@ jobs:
         bash <(curl -s https://codecov.io/bash) -y ../.github/codecov.yml
         bash <(curl -s https://codecov.io/bash) -y ../.github/codecov.yml
 
 
   makepanda:
   makepanda:
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
     strategy:
     strategy:
       matrix:
       matrix:
         os: [ubuntu-16.04, windows-2016, macOS-latest]
         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
 The following command illustrates how to build Panda3D with some common
 options:
 options:
 ```bash
 ```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
 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:
 used third-party packages:
 
 
 ```bash
 ```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
 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',
     '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 Freezer:
     class ModuleDef:
     class ModuleDef:
         def __init__(self, moduleName, filename = None,
         def __init__(self, moduleName, filename = None,
@@ -1799,21 +1844,36 @@ class Freezer:
             # Align to page size, so that it can be mmapped.
             # Align to page size, so that it can be mmapped.
             blob_align = 4096
             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.
         # Also determine the total blob size now.  Add padding to the end.
         blob_size = pool_offset + len(pool)
         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
             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,
         # Calculate the offsets for the variables.  These are pointers,
         # relative to the beginning of the blob.
         # relative to the beginning of the blob.
         field_offsets = {}
         field_offsets = {}
@@ -1893,9 +1953,13 @@ class Freezer:
             blob += struct.pack('<Q', blob_offset)
             blob += struct.pack('<Q', blob_offset)
 
 
         with open(target, 'wb') as f:
         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)
         os.chmod(target, 0o755)
         return target
         return target
@@ -2153,7 +2217,9 @@ class Freezer:
                     symoff += nlist_size
                     symoff += nlist_size
                     name = strings[strx : strings.find(b'\0', strx)]
                     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.
                         # Find out in which segment this is.
                         for vmaddr, vmsize, fileoff in segments:
                         for vmaddr, vmsize, fileoff in segments:
                             # Is it defined in this segment?
                             # Is it defined in this segment?
@@ -2163,6 +2229,59 @@ class Freezer:
                                 return fileoff + rel
                                 return fileoff + rel
                         print("Could not find memory address for symbol %s" % (symbol_name))
                         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):
     def makeModuleDef(self, mangledName, code):
         result = ''
         result = ''
         result += 'static unsigned char %s[] = {' % (mangledName)
         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]
                     rootdir = wf.split(os.path.sep, 1)[0]
                     search_path.append(os.path.join(whl, rootdir, '.libs'))
                     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
                     # Also look for more specific per-package cases, defined in
                     # PACKAGE_LIB_DIRS at the top of this file.
                     # 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, [])
                     extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
                     for extra_dir in extra_dirs:
                     for extra_dir in extra_dirs:
                         search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
                         search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
+
             return search_path
             return search_path
 
 
         def create_runtime(appname, mainscript, use_console):
         def create_runtime(appname, mainscript, use_console):

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

@@ -94,12 +94,12 @@ class DirectObject:
         if hasattr(self, '_taskList'):
         if hasattr(self, '_taskList'):
             tasks = [task.name for task in self._taskList.values()]
             tasks = [task.name for task in self._taskList.values()]
         if len(events) or len(tasks):
         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')
             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))
             func('destroyed %s instance is still %s%s' % (self.__class__.__name__, estr, tstr))
 
 
     #snake_case alias:
     #snake_case alias:

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

@@ -28,19 +28,19 @@ namespace std {
   template<class T> class allocator;
   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 {
 class map {
 public:
 public:
   typedef Key key_type;
   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 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 iterator;
   class const_iterator;
   class const_iterator;

+ 1 - 1
makepanda/makepanda.py

@@ -168,7 +168,7 @@ def parseopts(args):
     # Options for which to display a deprecation warning.
     # Options for which to display a deprecation warning.
     removedopts = [
     removedopts = [
         "use-touchinput", "no-touchinput", "no-awesomium", "no-directscripts",
         "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.
     # All recognized options.

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

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

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

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

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

@@ -328,6 +328,13 @@ ConfigVariableInt win_origin
 ConfigVariableBool fullscreen
 ConfigVariableBool fullscreen
 ("fullscreen", false);
 ("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
 ConfigVariableBool undecorated
 ("undecorated", false,
 ("undecorated", false,
  PRC_DESC("This specifies the default value of the 'undecorated' window "
  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_size;
 extern EXPCL_PANDA_DISPLAY ConfigVariableInt win_origin;
 extern EXPCL_PANDA_DISPLAY ConfigVariableInt win_origin;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool fullscreen;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool fullscreen;
+extern EXPCL_PANDA_DISPLAY ConfigVariableBool maximized;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool undecorated;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool undecorated;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool win_fixed_size;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool win_fixed_size;
 extern EXPCL_PANDA_DISPLAY ConfigVariableBool cursor_hidden;
 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();
         Light *light_obj = light.node()->as_light();
         nassertr(light_obj != nullptr, nullptr);
         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) {
         if (tex != nullptr) {
           sampler = tex->get_default_sampler();
           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_undecorated(false);
   _properties.set_fullscreen(false);
   _properties.set_fullscreen(false);
   _properties.set_minimized(false);
   _properties.set_minimized(false);
+  _properties.set_maximized(false);
   _properties.set_cursor_hidden(false);
   _properties.set_cursor_hidden(false);
 
 
   request_properties(WindowProperties::get_default());
   request_properties(WindowProperties::get_default());

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

@@ -407,6 +407,45 @@ clear_minimized() {
   _flags &= ~F_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.
  * 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_fullscreen(fullscreen);
   props.set_undecorated(undecorated);
   props.set_undecorated(undecorated);
   props.set_fixed_size(win_fixed_size);
   props.set_fixed_size(win_fixed_size);
+  props.set_maximized(maximized);
   props.set_cursor_hidden(cursor_hidden);
   props.set_cursor_hidden(cursor_hidden);
   if (!icon_filename.empty()) {
   if (!icon_filename.empty()) {
     props.set_icon_filename(icon_filename);
     props.set_icon_filename(icon_filename);
@@ -240,6 +241,9 @@ add_properties(const WindowProperties &other) {
   if (other.has_minimized()) {
   if (other.has_minimized()) {
     set_minimized(other.get_minimized());
     set_minimized(other.get_minimized());
   }
   }
+  if (other.has_maximized()) {
+    set_maximized(other.get_maximized());
+  }
   if (other.has_raw_mice()) {
   if (other.has_raw_mice()) {
     set_raw_mice(other.get_raw_mice());
     set_raw_mice(other.get_raw_mice());
   }
   }
@@ -296,6 +300,9 @@ output(ostream &out) const {
   if (has_minimized()) {
   if (has_minimized()) {
     out << (get_minimized() ? "minimized " : "!minimized ");
     out << (get_minimized() ? "minimized " : "!minimized ");
   }
   }
+  if (has_maximized()) {
+    out << (get_maximized() ? "maximized " : "!maximized ");
+  }
   if (has_raw_mice()) {
   if (has_raw_mice()) {
     out << (get_raw_mice() ? "raw_mice " : "!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,
   MAKE_PROPERTY2(minimized, has_minimized, get_minimized,
                             set_minimized, clear_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 void set_raw_mice(bool raw_mice);
   INLINE bool get_raw_mice() const;
   INLINE bool get_raw_mice() const;
   INLINE bool has_raw_mice() const;
   INLINE bool has_raw_mice() const;
@@ -202,6 +209,7 @@ private:
     S_mouse_mode           = 0x02000,
     S_mouse_mode           = 0x02000,
     S_parent_window        = 0x04000,
     S_parent_window        = 0x04000,
     S_raw_mice             = 0x08000,
     S_raw_mice             = 0x08000,
+    S_maximized            = 0x10000,
   };
   };
 
 
   // This bitmask represents the truefalse settings for various boolean flags
   // This bitmask represents the truefalse settings for various boolean flags
@@ -211,6 +219,7 @@ private:
     F_fullscreen     = S_fullscreen,
     F_fullscreen     = S_fullscreen,
     F_foreground     = S_foreground,
     F_foreground     = S_foreground,
     F_minimized      = S_minimized,
     F_minimized      = S_minimized,
+    F_maximized      = S_maximized,
     F_open           = S_open,
     F_open           = S_open,
     F_cursor_hidden  = S_cursor_hidden,
     F_cursor_hidden  = S_cursor_hidden,
     F_fixed_size     = S_fixed_size,
     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);
   nassertr(tex != nullptr, this);
 
 
   TextureAttrib *attrib = new TextureAttrib(*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);
   return return_new(attrib);
 }
 }
@@ -128,13 +134,19 @@ add_on_stage(TextureStage *stage, Texture *tex, const SamplerState &sampler, int
   nassertr(tex != nullptr, this);
   nassertr(tex != nullptr, this);
 
 
   TextureAttrib *attrib = new TextureAttrib(*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);
   return return_new(attrib);
 }
 }

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

@@ -141,6 +141,15 @@ PUBLISHED:
   INLINE const std::string &get_id() const;
   INLINE const std::string &get_id() const;
   INLINE void set_id(const std::string &id);
   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_enter_prefix();
   INLINE static std::string get_exit_prefix();
   INLINE static std::string get_exit_prefix();
   INLINE static std::string get_within_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();
     LPoint2i bottom_right = top_left + _properties.get_size();
 
 
     DWORD window_style = make_style(_properties);
     DWORD window_style = make_style(_properties);
+    DWORD current_style = GetWindowLong(_hWnd, GWL_STYLE);
     SetWindowLong(_hWnd, GWL_STYLE, window_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()) {
   if (properties.has_title()) {
@@ -402,6 +409,23 @@ set_properties_now(WindowProperties &properties) {
     properties.clear_minimized();
     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.has_fullscreen()) {
     if (properties.get_fullscreen() && !is_fullscreen()) {
     if (properties.get_fullscreen() && !is_fullscreen()) {
       if (do_fullscreen_switch()){
       if (do_fullscreen_switch()){
@@ -507,6 +531,7 @@ open_window() {
   }
   }
   bool want_foreground = (!_properties.has_foreground() || _properties.get_foreground());
   bool want_foreground = (!_properties.has_foreground() || _properties.get_foreground());
   bool want_minimized = (_properties.has_minimized() && _properties.get_minimized()) && !want_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();
   HWND old_foreground_window = GetForegroundWindow();
 
 
@@ -533,6 +558,9 @@ open_window() {
   if (want_minimized) {
   if (want_minimized) {
     ShowWindow(_hWnd, SW_MINIMIZE);
     ShowWindow(_hWnd, SW_MINIMIZE);
     ShowWindow(_hWnd, SW_MINIMIZE);
     ShowWindow(_hWnd, SW_MINIMIZE);
+  } else if (want_maximized) {
+    ShowWindow(_hWnd, SW_MAXIMIZE);
+    ShowWindow(_hWnd, SW_MAXIMIZE);
   } else {
   } else {
     ShowWindow(_hWnd, SW_SHOWNORMAL);
     ShowWindow(_hWnd, SW_SHOWNORMAL);
     ShowWindow(_hWnd, SW_SHOWNORMAL);
     ShowWindow(_hWnd, SW_SHOWNORMAL);
@@ -854,6 +882,21 @@ handle_reshape() {
       << "," << properties.get_y_size() << ")\n";
       << "," << 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();
   adjust_z_order();
   system_changed_properties(properties);
   system_changed_properties(properties);
 }
 }
@@ -1083,6 +1126,9 @@ calculate_metrics(bool fullscreen, DWORD window_style, WINDOW_METRICS &metrics,
 bool WinGraphicsWindow::
 bool WinGraphicsWindow::
 open_graphic_window() {
 open_graphic_window() {
   DWORD window_style = make_style(_properties);
   DWORD window_style = make_style(_properties);
+  if (_properties.get_maximized()) {
+    window_style |= WS_MAXIMIZE;
+  }
 
 
   wstring title;
   wstring title;
   if (_properties.has_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_add = XInternAtom(_display, "_NET_WM_STATE_ADD", false);
   _net_wm_state_remove = XInternAtom(_display, "_NET_WM_STATE_REMOVE", 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_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_add;
   Atom _net_wm_state_remove;
   Atom _net_wm_state_remove;
   Atom _net_wm_bypass_compositor;
   Atom _net_wm_bypass_compositor;
+  Atom _net_wm_state_maximized_vert;
+  Atom _net_wm_state_maximized_horz;
 
 
   // Extension functions.
   // Extension functions.
   typedef int (*pfn_XcursorGetDefaultSize)(X11_Display *);
   typedef int (*pfn_XcursorGetDefaultSize)(X11_Display *);

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

@@ -309,6 +309,9 @@ process_events() {
   WindowProperties properties;
   WindowProperties properties;
   bool changed_properties = false;
   bool changed_properties = false;
 
 
+  XPropertyEvent property_event;
+  bool got_net_wm_state_change = false;
+
   while (XCheckIfEvent(_display, &event, check_event, (char *)this)) {
   while (XCheckIfEvent(_display, &event, check_event, (char *)this)) {
     if (got_keyrelease_event) {
     if (got_keyrelease_event) {
       // If a keyrelease event is immediately followed by a matching keypress
       // If a keyrelease event is immediately followed by a matching keypress
@@ -362,6 +365,19 @@ process_events() {
     case ReparentNotify:
     case ReparentNotify:
       break;
       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:
     case ConfigureNotify:
       // When resizing or moving the window, multiple ConfigureNotify events
       // When resizing or moving the window, multiple ConfigureNotify events
       // may be sent in rapid succession.  We only respond to the last one.
       // 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) {
   if (changed_properties) {
     system_changed_properties(properties);
     system_changed_properties(properties);
   }
   }
@@ -791,6 +846,12 @@ set_properties_now(WindowProperties &properties) {
     properties.clear_fullscreen();
     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
   // 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
   // X calls.  These may still get intercepted by the window manager.  Rather
   // than changing _properties immediately, we'll wait for the ConfigureNotify
   // than changing _properties immediately, we'll wait for the ConfigureNotify
@@ -1101,7 +1162,8 @@ open_window() {
     KeyPressMask | KeyReleaseMask |
     KeyPressMask | KeyReleaseMask |
     EnterWindowMask | LeaveWindowMask |
     EnterWindowMask | LeaveWindowMask |
     PointerMotionMask |
     PointerMotionMask |
-    FocusChangeMask | StructureNotifyMask;
+    FocusChangeMask | StructureNotifyMask |
+    PropertyChangeMask;
 
 
   // Initialize window attributes
   // Initialize window attributes
   XSetWindowAttributes wa;
   XSetWindowAttributes wa;
@@ -1269,6 +1331,18 @@ set_wm_properties(const WindowProperties &properties, bool already_mapped) {
   SetAction set_data[max_set_data];
   SetAction set_data[max_set_data];
   int next_set_data = 0;
   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.has_fullscreen()) {
     if (properties.get_fullscreen()) {
     if (properties.get_fullscreen()) {
       // For a "fullscreen" request, we pass this through, hoping the window
       // 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
     # Test clear
     props.size = None
     props.size = None
     assert not props.has_size()
     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 lattr1.compare_to(lattr2) != 0
     assert lattr2.compare_to(lattr1) != 0
     assert lattr2.compare_to(lattr1) != 0
     assert lattr2.compare_to(lattr1) == -lattr1.compare_to(lattr2)
     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 shattr1.compare_to(shattr2) == 0
     assert shattr2.compare_to(shattr1) == 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 shattr1.compare_to(shattr2) != 0
     assert shattr2.compare_to(shattr1) != 0
     assert shattr2.compare_to(shattr1) != 0
 
 
     shattr1 = core.ShaderAttrib.make().set_flag(core.ShaderAttrib.F_subsume_alpha_test, False)
     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 shattr1.compare_to(shattr2) != 0
     assert shattr2.compare_to(shattr1) != 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")
 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():
 def test_textureattrib_compose_add():
     # Tests a case in which a child node adds another texture.
     # Tests a case in which a child node adds another texture.
     tattr1 = core.TextureAttrib.make()
     tattr1 = core.TextureAttrib.make()
@@ -24,6 +37,21 @@ def test_textureattrib_compose_add():
     assert stage2 in tattr3.on_stages
     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():
 def test_textureattrib_compose_subtract():
     # Tests a case in which a child node disables a texture.
     # Tests a case in which a child node disables a texture.
     tattr1 = core.TextureAttrib.make()
     tattr1 = core.TextureAttrib.make()
@@ -77,6 +105,37 @@ def test_textureattrib_compose_alloff():
     assert tattr3.has_all_off()
     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():
 def test_textureattrib_compare():
     tattr1 = core.TextureAttrib.make()
     tattr1 = core.TextureAttrib.make()
     tattr2 = core.TextureAttrib.make()
     tattr2 = core.TextureAttrib.make()