소스 검색

Merge remote-tracking branch 'origin/master' into input-overhaul

rdb 8 년 전
부모
커밋
94385e865f
65개의 변경된 파일6622개의 추가작업 그리고 154개의 파일을 삭제
  1. 5 0
      README.md
  2. 51 0
      contrib/src/rplight/config_rplight.cxx
  3. 40 0
      contrib/src/rplight/config_rplight.h
  4. 185 0
      contrib/src/rplight/gpuCommand.I
  5. 87 0
      contrib/src/rplight/gpuCommand.cxx
  6. 89 0
      contrib/src/rplight/gpuCommand.h
  7. 82 0
      contrib/src/rplight/gpuCommandList.cxx
  8. 54 0
      contrib/src/rplight/gpuCommandList.h
  9. 233 0
      contrib/src/rplight/iesDataset.cxx
  10. 68 0
      contrib/src/rplight/iesDataset.h
  11. 140 0
      contrib/src/rplight/internalLightManager.I
  12. 441 0
      contrib/src/rplight/internalLightManager.cxx
  13. 102 0
      contrib/src/rplight/internalLightManager.h
  14. 13 0
      contrib/src/rplight/p3rplight_composite1.cxx
  15. 239 0
      contrib/src/rplight/pointerSlotStorage.h
  16. 243 0
      contrib/src/rplight/pssmCameraRig.I
  17. 396 0
      contrib/src/rplight/pssmCameraRig.cxx
  18. 125 0
      contrib/src/rplight/pssmCameraRig.h
  19. 406 0
      contrib/src/rplight/rpLight.I
  20. 137 0
      contrib/src/rplight/rpLight.cxx
  21. 130 0
      contrib/src/rplight/rpLight.h
  22. 82 0
      contrib/src/rplight/rpPointLight.I
  23. 90 0
      contrib/src/rplight/rpPointLight.cxx
  24. 63 0
      contrib/src/rplight/rpPointLight.h
  25. 74 0
      contrib/src/rplight/rpSpotLight.I
  26. 80 0
      contrib/src/rplight/rpSpotLight.cxx
  27. 71 0
      contrib/src/rplight/rpSpotLight.h
  28. 159 0
      contrib/src/rplight/shadowAtlas.I
  29. 186 0
      contrib/src/rplight/shadowAtlas.cxx
  30. 80 0
      contrib/src/rplight/shadowAtlas.h
  31. 192 0
      contrib/src/rplight/shadowManager.I
  32. 157 0
      contrib/src/rplight/shadowManager.cxx
  33. 91 0
      contrib/src/rplight/shadowManager.h
  34. 262 0
      contrib/src/rplight/shadowSource.I
  35. 41 0
      contrib/src/rplight/shadowSource.cxx
  36. 93 0
      contrib/src/rplight/shadowSource.h
  37. 93 0
      contrib/src/rplight/tagStateManager.I
  38. 202 0
      contrib/src/rplight/tagStateManager.cxx
  39. 93 0
      contrib/src/rplight/tagStateManager.h
  40. 5 0
      direct/src/gui/DirectEntry.py
  41. 2 0
      dtool/src/dtoolbase/dtool_platform.h
  42. 43 11
      makepanda/makepanda.py
  43. 60 17
      makepanda/makepandacore.py
  44. 442 0
      panda/src/android/android_native_app_glue.c
  45. 354 0
      panda/src/android/android_native_app_glue.h
  46. 0 1
      panda/src/cocoadisplay/cocoaGraphicsPipe.mm
  47. 3 0
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  48. 4 0
      panda/src/collide/collisionVisualizer.I
  49. 28 6
      panda/src/collide/collisionVisualizer.cxx
  50. 3 0
      panda/src/collide/collisionVisualizer.h
  51. 2 0
      panda/src/display/graphicsEngine.cxx
  52. 30 7
      panda/src/dxgsg9/dxTextureContext9.cxx
  53. 44 5
      panda/src/egg2pg/eggSaver.cxx
  54. 2 4
      panda/src/event/asyncTask.cxx
  55. 12 8
      panda/src/gobj/geomVertexData.cxx
  56. 9 12
      panda/src/gobj/shader.I
  57. 15 15
      panda/src/gobj/shader.cxx
  58. 9 12
      panda/src/gobj/shader.h
  59. 3 0
      panda/src/pgraph/lightAttrib.cxx
  60. 30 7
      panda/src/pgraphnodes/shaderGenerator.cxx
  61. 2 2
      panda/src/pipeline/thread.I
  62. 2 2
      panda/src/pipeline/thread.h
  63. 63 45
      panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx
  64. 6 0
      panda/src/x11display/x11GraphicsWindow.cxx
  65. 74 0
      tests/pgraph/test_lightattrib.py

+ 5 - 0
README.md

@@ -28,6 +28,8 @@ into an existing Python installation is using the following command:
 pip install --pre --extra-index-url https://archive.panda3d.org/ panda3d
 pip install --pre --extra-index-url https://archive.panda3d.org/ panda3d
 ```
 ```
 
 
+If this command fails, please make sure your version of pip is up-to-date.
+
 If you prefer to install the full SDK with all tools, the latest development
 If you prefer to install the full SDK with all tools, the latest development
 builds can be obtained from this page:
 builds can be obtained from this page:
 
 
@@ -51,6 +53,9 @@ depending on whether you are on a 32-bit or 64-bit system:
 https://www.panda3d.org/download/panda3d-1.9.4/panda3d-1.9.4-tools-win32.zip
 https://www.panda3d.org/download/panda3d-1.9.4/panda3d-1.9.4-tools-win32.zip
 https://www.panda3d.org/download/panda3d-1.9.4/panda3d-1.9.4-tools-win64.zip
 https://www.panda3d.org/download/panda3d-1.9.4/panda3d-1.9.4-tools-win64.zip
 
 
+(It is also possible to build using MSVC 2015 and 2017, which requires a
+different set of thirdparty libraries, but that is not described here.)
+
 After acquiring these dependencies, you may simply build Panda3D from the
 After acquiring these dependencies, you may simply build Panda3D from the
 command prompt using the following command:
 command prompt using the following command:
 
 

+ 51 - 0
contrib/src/rplight/config_rplight.cxx

@@ -0,0 +1,51 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "config_rplight.h"
+
+#include "rpLight.h"
+#include "rpPointLight.h"
+
+#include "dconfig.h"
+
+Configure(config_rplight);
+NotifyCategoryDef(rplight, "");
+
+ConfigureFn(config_rplight) {
+  init_librplight();
+}
+
+void
+init_librplight() {
+  static bool initialized = false;
+  if (initialized) {
+    return;
+  }
+  initialized = true;
+
+  // RPLight::init_type();
+  // RPPointLight::init_type();
+}

+ 40 - 0
contrib/src/rplight/config_rplight.h

@@ -0,0 +1,40 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef CONFIG_RPLIGHT_H
+#define CONFIG_RPLIGHT_H
+
+#include "pandabase.h"
+#include "notifyCategoryProxy.h"
+#include "configVariableDouble.h"
+#include "configVariableString.h"
+#include "configVariableInt.h"
+
+NotifyCategoryDecl(rplight, EXPORT_CLASS, EXPORT_TEMPL);
+
+extern void init_librplight();
+
+#endif // CONFIG_RPLIGHT_H

+ 185 - 0
contrib/src/rplight/gpuCommand.I

@@ -0,0 +1,185 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "stdint.h"
+
+/**
+ * @brief Appends an integer to the GPUCommand.
+ * @details This adds an integer to the back of the GPUCommand. Depending on the
+ *   setting in convert_int_to_float, this will either just convert the int to a
+ *   float by casting it, or just do a bitwise copy.
+ *
+ * @param v The integer to append.
+ */
+inline void GPUCommand::push_int(int v) {
+  push_float(convert_int_to_float(v));
+}
+
+/**
+ * @brief Internal method to convert an integer to float
+ * @details This methods gets called by the GPUCommand::push_int, and manages
+ *   storing an integer in a floating point variable. There are two options,
+ *   which are documented inside of the method.
+ *
+ * @param v Integer to convert
+ * @return Float-representation of that integer, either casted or binary converted.s
+ */
+inline float GPUCommand::convert_int_to_float(int v) const {
+
+  #if !PACK_INT_AS_FLOAT
+    // Just round to float, can cause rounding issues tho
+    return (float)v;
+
+  #else
+    assert(sizeof(float) == 4); // We really need this for packing! Better
+                  // throw an error if the compiler uses more
+                  // than 4 bytes.
+    // Simple binary conversion, assuming sizeof(int) == sizeof(float)
+    union { int32_t _int; float _float; } converter = { (int32_t)v };
+    return converter._float;
+  #endif
+}
+
+/**
+ * @brief Appends a float to the GPUCommand.
+ * @details This adds an integer to the back of the GPUCommand. Its used by all
+ *   other push_xxx methods, and simply stores the value, then increments the write
+ *   pointer. When the amount of floats exceeds the capacity of the GPUCommand,
+ *   an error will be printed, and the method returns without doing anything else.
+ *
+ * @param v The float to append.
+ */
+inline void GPUCommand::push_float(float v) {
+  if (_current_index >= GPU_COMMAND_ENTRIES) {
+    gpucommand_cat.error() << "Out of bounds! Exceeded command size of " << GPU_COMMAND_ENTRIES << endl;
+    return;
+  }
+  _data[_current_index++] = v;
+}
+
+/**
+ * @brief Appends a 3-component floating point vector to the GPUCommand.
+ * @details This appends a 3-component floating point vector to the command.
+ *   It basically just calls push_float() for every component, in the order
+ *   x, y, z, which causes the vector to occupy the space of 3 floats.
+ *
+ * @param v Int-Vector to append.
+ */
+inline void GPUCommand::push_vec3(const LVecBase3 &v) {
+  push_float(v.get_x());
+  push_float(v.get_y());
+  push_float(v.get_z());
+}
+
+
+/**
+ * @brief Appends a 3-component integer vector to the GPUCommand.
+ * @details This appends a 3-component integer vector to the command.
+ *   It basically just calls push_int() for every component, in the order
+ *   x, y, z, which causes the vector to occupy the space of 3 floats.
+ *
+ * @param v Int-Vector to append.
+ */
+inline void GPUCommand::push_vec3(const LVecBase3i &v) {
+  push_int(v.get_x());
+  push_int(v.get_y());
+  push_int(v.get_z());
+}
+
+/**
+ * @brief Appends a 4-component floating point vector to the GPUCommand.
+ * @details This appends a 4-component floating point vector to the command.
+ *   It basically just calls push_float() for every component, in the order
+ *   x, y, z, which causes the vector to occupy the space of 3 floats.
+ *
+ * @param v Int-Vector to append.
+ */
+inline void GPUCommand::push_vec4(const LVecBase4 &v) {
+  push_float(v.get_x());
+  push_float(v.get_y());
+  push_float(v.get_z());
+  push_float(v.get_w());
+}
+
+/**
+ * @brief Appends a 4-component integer vector to the GPUCommand.
+ * @details This appends a 4-component integer vector to the command.
+ *   It basically just calls push_int() for every component, in the order
+ *   x, y, z, w, which causes the vector to occupy the space of 4 floats.
+ *
+ * @param v Int-Vector to append.
+ */
+inline void GPUCommand::push_vec4(const LVecBase4i &v) {
+  push_int(v.get_x());
+  push_int(v.get_y());
+  push_int(v.get_z());
+  push_int(v.get_w());
+}
+
+/**
+ * @brief Appends a floating point 3x3 matrix to the GPUCommand.
+ * @details This appends a floating point 3x3 matrix to the GPUCommand, by
+ *   pushing all components in row-order to the command. This occupies a space of
+ *   9 floats.
+ *
+ * @param v Matrix to append
+ */
+inline void GPUCommand::push_mat3(const LMatrix3 &v) {
+  for (size_t i = 0; i < 3; ++i) {
+    for (size_t j = 0; j < 3; ++j) {
+      push_float(v.get_cell(i, j));
+    }
+  }
+}
+
+/**
+ * @brief Appends a floating point 4x4 matrix to the GPUCommand.
+ * @details This appends a floating point 4x4 matrix to the GPUCommand, by
+ *   pushing all components in row-order to the command. This occupies a space of
+ *   16 floats.
+ *
+ * @param v Matrix to append
+ */
+inline void GPUCommand::push_mat4(const LMatrix4 &v) {
+  for (size_t i = 0; i < 4; ++i) {
+    for (size_t j = 0; j < 4; ++j) {
+      push_float(v.get_cell(i, j));
+    }
+  }
+}
+
+/**
+ * @brief Returns whether integers are packed as floats.
+ * @details This returns how integer are packed into the data stream. If the
+ *   returned value is true, then integers are packed using their binary
+ *   representation converted to floating point format. If the returned value
+ *   is false, then integers are packed by simply casting them to float,
+ *   e.g. val = (float)i;
+ * @return The integer representation flag
+ */
+inline bool GPUCommand::get_uses_integer_packing() {
+  return PACK_INT_AS_FLOAT;
+}

+ 87 - 0
contrib/src/rplight/gpuCommand.cxx

@@ -0,0 +1,87 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "gpuCommand.h"
+
+#include <iostream>
+#include <iomanip>
+#include <stdlib.h>
+
+
+NotifyCategoryDef(gpucommand, "");
+
+/**
+ * @brief Constructs a new GPUCommand with the given command type.
+ * @details This will construct a new GPUCommand of the given command type.
+ *   The command type should be of GPUCommand::CommandType, and determines
+ *   what data the GPUCommand contains, and how it will be handled.
+ *
+ * @param command_type The type of the GPUCommand
+ */
+GPUCommand::GPUCommand(CommandType command_type) {
+  _command_type = command_type;
+  _current_index = 0;
+  memset(_data, 0x0, sizeof(float) * GPU_COMMAND_ENTRIES);
+
+  // Store the command type as the first entry
+  push_int(command_type);
+}
+
+/**
+ * @brief Prints out the GPUCommand to the console
+ * @details This method prints the type, size, and data of the GPUCommand to the
+ *   console. This helps for debugging the contents of the GPUCommand. Keep
+ *   in mind that integers might be shown in their binary float representation,
+ *   depending on the setting in the GPUCommand::convert_int_to_float method.
+ */
+void GPUCommand::write(ostream &out) const {
+  out << "GPUCommand(type=" << _command_type << ", size=" << _current_index << ", data = {" << endl;
+  for (size_t k = 0; k < GPU_COMMAND_ENTRIES; ++k) {
+    out << std::setw(12) << std::fixed << std::setprecision(5) << _data[k] << " ";
+    if (k % 6 == 5 || k == GPU_COMMAND_ENTRIES - 1) out << endl;
+  }
+  out << "})" << endl;
+}
+
+/**
+ * @brief Writes the GPU command to a given target.
+ * @details This method writes all the data of the GPU command to a given target.
+ *   The target should be a pointer to memory being big enough to hold the
+ *   data. Presumably #dest will be a handle to texture memory.
+ *   The command_index controls the offset where the data will be written
+ *   to.
+ *
+ * @param dest Handle to the memory to write the command to
+ * @param command_index Offset to write the command to. The command will write
+ *   its data to command_index * GPU_COMMAND_ENTRIES. When writing
+ *   the GPUCommand in a GPUCommandList, the command_index will
+ *   most likely be the index of the command in the list.
+ */
+void GPUCommand::write_to(const PTA_uchar &dest, size_t command_index) {
+  size_t command_size = GPU_COMMAND_ENTRIES * sizeof(float);
+  size_t offset = command_index * command_size;
+  memcpy(dest.p() + offset, &_data, command_size);
+}

+ 89 - 0
contrib/src/rplight/gpuCommand.h

@@ -0,0 +1,89 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef GPUCOMMAND_H
+#define GPUCOMMAND_H
+
+#include "pandabase.h"
+#include "luse.h"
+
+NotifyCategoryDecl(gpucommand, EXPORT_CLASS, EXPORT_TEMPL);
+
+#define GPU_COMMAND_ENTRIES 32
+
+// Packs integers by storing their binary representation in floats
+// This only works if the command and light buffer is 32bit floating point.
+#define PACK_INT_AS_FLOAT 0
+
+/**
+ * @brief Class for storing data to be transferred to the GPU.
+ * @details This class can be seen like a packet, to be transferred to the GPU.
+ *   It has a command type, which tells the GPU what to do once it recieved this
+ *   "packet". It stores a limited amount of floating point components.
+ */
+class GPUCommand {
+PUBLISHED:
+  /**
+   * The different types of GPUCommands. Each type has a special case in
+   * the command queue processor. When adding new types, those need to
+   * be handled in the command target, too.
+   */
+  enum CommandType {
+    CMD_invalid = 0,
+    CMD_store_light = 1,
+    CMD_remove_light = 2,
+    CMD_store_source = 3,
+    CMD_remove_sources = 4,
+  };
+
+  GPUCommand(CommandType command_type);
+
+  inline void push_int(int v);
+  inline void push_float(float v);
+  inline void push_vec3(const LVecBase3 &v);
+  inline void push_vec3(const LVecBase3i &v);
+  inline void push_vec4(const LVecBase4 &v);
+  inline void push_vec4(const LVecBase4i &v);
+  inline void push_mat3(const LMatrix3 &v);
+  inline void push_mat4(const LMatrix4 &v);
+
+  inline static bool get_uses_integer_packing();
+
+  void write_to(const PTA_uchar &dest, size_t command_index);
+  void write(ostream &out) const;
+
+private:
+
+  inline float convert_int_to_float(int v) const;
+
+  CommandType _command_type;
+  size_t _current_index;
+  float _data[GPU_COMMAND_ENTRIES];
+};
+
+#include "gpuCommand.I"
+
+#endif // GPUCOMMAND_H

+ 82 - 0
contrib/src/rplight/gpuCommandList.cxx

@@ -0,0 +1,82 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "gpuCommandList.h"
+
+
+/**
+ * @brief Constructs a new GPUCommandList
+ * @details This constructs a new GPUCommandList. By default, there are no commands
+ *   in the list.
+ */
+GPUCommandList::GPUCommandList() {
+}
+
+/**
+ * @brief Pushes a GPUCommand to the command list.
+ * @details This adds a new GPUCommand to the list of commands to be processed.
+ *
+ * @param cmd The command to add
+ */
+void GPUCommandList::add_command(const GPUCommand& cmd) {
+  _commands.push(cmd);
+}
+
+/**
+ * @brief Returns the number of commands in this list.
+ * @details This returns the amount of commands which are currently stored in this
+ *   list, and are waiting to get processed.
+ * @return Amount of commands
+ */
+size_t GPUCommandList::get_num_commands() {
+  return _commands.size();
+}
+
+/**
+ * @brief Writes the first n-commands to a destination.
+ * @details This takes the first #limit commands, and writes them to the
+ *   destination using GPUCommand::write_to. See GPUCommand::write_to for
+ *   further information about #dest. The limit controls after how much
+ *   commands the processing will be stopped. All commands which got processed
+ *   will get removed from the list.
+ *
+ * @param dest Destination to write to, see GPUCommand::write_to
+ * @param limit Maximum amount of commands to process
+ *
+ * @return Amount of commands processed, between 0 and #limit.
+ */
+size_t GPUCommandList::write_commands_to(const PTA_uchar &dest, size_t limit) {
+  size_t num_commands_written = 0;
+
+  while (num_commands_written < limit && !_commands.empty()) {
+    // Write the first command to the stream, and delete it afterwards
+    _commands.front().write_to(dest, num_commands_written);
+    _commands.pop();
+    num_commands_written ++;
+  }
+
+  return num_commands_written;
+}

+ 54 - 0
contrib/src/rplight/gpuCommandList.h

@@ -0,0 +1,54 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef GPUCOMMANDLIST_H
+#define GPUCOMMANDLIST_H
+
+#include "pandabase.h"
+#include "gpuCommand.h"
+
+#include <queue>
+
+/**
+ * @brief Class to store a list of commands.
+ * @details This is a class to store a list of GPUCommands. It provides
+ *   functionality to only provide the a given amount of commands at one time.
+ */
+class GPUCommandList {
+PUBLISHED:
+  GPUCommandList();
+
+  void add_command(const GPUCommand& cmd);
+  size_t get_num_commands();
+  size_t write_commands_to(const PTA_uchar &dest, size_t limit = 32);
+
+  MAKE_PROPERTY(num_commands, get_num_commands);
+
+protected:
+  queue<GPUCommand> _commands;
+};
+
+#endif // GPUCOMMANDLIST_H

+ 233 - 0
contrib/src/rplight/iesDataset.cxx

@@ -0,0 +1,233 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "iesDataset.h"
+
+#define _USE_MATH_DEFINES
+#include <math.h>
+
+NotifyCategoryDef(iesdataset, "")
+
+/**
+ * @brief Constructs a new empty dataset.
+ * @details This constructs a new IESDataset with no data set.
+ */
+IESDataset::IESDataset() {
+}
+
+/**
+ * @brief Sets the vertical angles of the dataset.
+ * @details This sets the list of vertical angles of the dataset.
+ *
+ * @param vertical_angles Vector of all vertical angles.
+ */
+void IESDataset::set_vertical_angles(const PTA_float &vertical_angles) {
+  nassertv(vertical_angles.size() > 0);
+	_vertical_angles = vertical_angles;
+}
+
+/**
+ * @brief Sets the horizontal angles of the dataset.
+ * @details This sets the list of horizontal angles of the dataset.
+ *
+ * @param horizontal_angles Vector of all horizontal angles.
+ */
+void IESDataset::set_horizontal_angles(const PTA_float &horizontal_angles) {
+  nassertv(horizontal_angles.size() > 0);
+	_horizontal_angles = horizontal_angles;
+}
+
+/**
+ * @brief Sets the candela values.
+ * @details This sets the candela values of the dataset. They should be an
+ *   interleaved 2D array with the dimensions vertical_angles x horizontal_angles.
+ *   They also should be normalized by dividing by the maximum entry.
+ * @param candela_values Interleaved 2D-vector of candela values.
+ */
+void IESDataset::set_candela_values(const PTA_float &candela_values) {
+	nassertv(candela_values.size() == _horizontal_angles.size() * _vertical_angles.size());
+	_candela_values = candela_values;
+}
+
+/**
+ * @brief Internal method to access the candela data.
+ * @details This lookups a candela value in the candela values. It converts a
+ *   two dimensional index to a onedimensional index and then returns the candela
+ *   value at that position.
+ *
+ * @param vertical_angle_idx Index of the vertical angle
+ * @param horizontal_angle_idx Index of the horizontal angle
+ *
+ * @return Candela value between 0 .. 1
+ */
+float IESDataset::get_candela_value_from_index(size_t vertical_angle_idx, size_t horizontal_angle_idx) const {
+	size_t index = vertical_angle_idx + horizontal_angle_idx * _vertical_angles.size();
+	nassertr(index >= 0 && index < _candela_values.size(), 0.0);
+	return _candela_values[index];
+}
+
+/**
+ * @brief Samples the dataset at the given position
+ * @details This looks up a value in the dataset, by specifying a horizontal and
+ *   vertical angle. This is used for generating the LUT. The vertical and horizontal
+ *   angle should be inside of the bounds of the vertical and horizontal angle arrays.
+ *
+ * @param vertical_angle Vertical angle, from 0 .. 90 or 0 .. 180 depending on the dataset
+ * @param horizontal_angle Horizontal angle, from 0 .. 180 or 0 .. 360 depending on the dataset.
+ *
+ * @return Candela value between 0 .. 1
+ */
+float IESDataset::get_candela_value(float vertical_angle, float horizontal_angle) const {
+
+  // Special case for datasets without horizontal angles
+  if (_horizontal_angles.size() == 1) {
+    return get_vertical_candela_value(0, vertical_angle);
+  }
+
+  float max_angle = _horizontal_angles[_horizontal_angles.size() - 1];
+
+  // Wrap angle to fit from 0 .. 360 degree. Most profiles only distribute
+  // candela values from 0 .. 180 or even 0 .. 90. We have to mirror the
+  // values at those borders (so 2 times for 180 degree and 4 times for 90 degree)
+  horizontal_angle = fmod(horizontal_angle, 2.0f * max_angle);
+  if (horizontal_angle > max_angle) {
+    horizontal_angle = 2.0 * max_angle - horizontal_angle;
+  }
+
+  // Simlar to the vertical step, we now try interpolating a horizontal angle,
+  // but we need to evaluate the vertical value for each row instead of fetching
+  // the value directly
+  for (size_t horizontal_index = 1; horizontal_index < _horizontal_angles.size(); ++horizontal_index) {
+    float curr_angle = _horizontal_angles[horizontal_index];
+
+    if (curr_angle >= horizontal_angle) {
+
+      // Get previous angle data
+      float prev_angle = _horizontal_angles[horizontal_index - 1];
+      float prev_value = get_vertical_candela_value(horizontal_index - 1, vertical_angle);
+      float curr_value = get_vertical_candela_value(horizontal_index, vertical_angle);
+
+      // Interpolate lineary
+      float lerp = (horizontal_angle - prev_angle) / (curr_angle - prev_angle);
+
+      // Should never occur, but to be safe:
+      if (lerp < 0.0 || lerp > 1.0) {
+        iesdataset_cat.error() << "Invalid horizontal lerp: " << lerp
+                     << ", requested angle was " << horizontal_angle
+                     << ", prev = " << prev_angle << ", cur = " << curr_angle
+                     << endl;
+      }
+
+      return curr_value * lerp + prev_value * (1-lerp);
+    }
+  }
+
+  return 0.0;
+}
+
+/**
+ * @brief Fetches a vertical candela value
+ * @details Fetches a vertical candela value, using a given horizontal position.
+ *   This does an 1D interpolation in the candela values array.
+ *
+ * @param horizontal_angle_idx The index of the horizontal angle in the horizontal
+ *   angle array.
+ * @param vertical_angle The vertical angle. Interpolation will be done if the
+ *   vertical angle is not in the vertical angles array.
+ *
+ * @return Candela value between 0 .. 1
+ */
+float IESDataset::get_vertical_candela_value(size_t horizontal_angle_idx, float vertical_angle) const {
+  nassertr(horizontal_angle_idx >= 0 && horizontal_angle_idx < _horizontal_angles.size(), 0.0);
+
+  // Lower bound
+  if (vertical_angle < 0.0) return 0.0;
+
+  // Upper bound
+  if (vertical_angle > _vertical_angles[_vertical_angles.size() - 1] ) return 0.0;
+
+  // Find lowest enclosing angle
+  for (size_t vertical_index = 1; vertical_index < _vertical_angles.size(); ++vertical_index) {
+    float curr_angle = _vertical_angles[vertical_index];
+
+    // Found value
+    if (curr_angle > vertical_angle) {
+
+      // Get previous angle data
+      float prev_angle = _vertical_angles[vertical_index - 1];
+      float prev_value = get_candela_value_from_index(vertical_index - 1, horizontal_angle_idx);
+      float curr_value = get_candela_value_from_index(vertical_index, horizontal_angle_idx);
+
+      // Interpolate lineary
+      float lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle);
+
+      // Should never occur, but to be safe:
+      if (lerp < 0.0 || lerp > 1.0) {
+        iesdataset_cat.error() << "ERROR: Invalid vertical lerp: " << lerp
+                     << ", requested angle was " << vertical_angle
+                     << ", prev = " << prev_angle << ", cur = " << curr_angle
+                     << endl;
+      }
+
+      return curr_value * lerp + prev_value * (1-lerp);
+    }
+  }
+  return 0.0;
+}
+
+/**
+ * @brief Generates the IES LUT
+ * @details This generates the LUT into a given dataset texture. The x-axis
+ *   referes to the vertical_angle, whereas the y-axis refers to the
+ *   horizontal angle.
+ *
+ * @param dest_tex Texture to write the LUT into
+ * @param z Layer to write the LUT into, in case the texture is a 3D Texture or
+ *   2D Texture Array.
+ */
+void IESDataset::generate_dataset_texture_into(Texture* dest_tex, size_t z) const {
+
+  size_t resolution_vertical = dest_tex->get_y_size();
+  size_t resolution_horizontal = dest_tex->get_x_size();
+
+  // Candla values are stored flippped - vertical angles in the x - Axis
+  // and horizontal angles in the y - Axis
+  PNMImage dest = PNMImage(resolution_vertical, resolution_horizontal, 1, 65535);
+
+  for (size_t vert = 0; vert < resolution_vertical; ++vert) {
+    for (size_t horiz = 0; horiz < resolution_horizontal; ++horiz) {
+      float vert_angle = (float)vert / (float)(resolution_vertical-1);
+      vert_angle = cos(vert_angle * M_PI) * 90.0 + 90.0;
+      float horiz_angle = (float)horiz / (float)(resolution_horizontal-1) * 360.0;
+      float candela = get_candela_value(vert_angle, horiz_angle);
+      dest.set_xel(vert, horiz, candela);
+    }
+  }
+
+
+  dest_tex->load(dest, z, 0);
+}

+ 68 - 0
contrib/src/rplight/iesDataset.h

@@ -0,0 +1,68 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef IESDATASET_H
+#define IESDATASET_H
+
+#include "pandabase.h"
+#include "pta_float.h"
+#include "pointerToArray.h"
+#include "texture.h"
+#include "pnmImage.h"
+
+NotifyCategoryDecl(iesdataset, EXPORT_CLASS, EXPORT_TEMPL);
+
+
+/**
+ * @brief This class generates a LUT from IES data.
+ * @details This class is used by the IESLoader to generate a LUT texture which
+ *   is used in the shaders to perform IES lighting. It takes a set of vertical
+ *   and horizontal angles, as well as a set of candela values, which then are
+ *   lineary interpolated onto a 2D LUT Texture.
+ */
+class IESDataset {
+PUBLISHED:
+  IESDataset();
+
+  void set_vertical_angles(const PTA_float &vertical_angles);
+  void set_horizontal_angles(const PTA_float &horizontal_angles);
+  void set_candela_values(const PTA_float &candela_values);
+
+  void generate_dataset_texture_into(Texture* dest_tex, size_t z) const;
+
+public:
+
+  float get_candela_value(float vertical_angle, float horizontal_angle) const;
+  float get_candela_value_from_index(size_t vertical_angle_idx, size_t horizontal_angle_idx) const;
+  float get_vertical_candela_value(size_t horizontal_angle_idx, float vertical_angle) const;
+
+private:
+  PTA_float _vertical_angles;
+  PTA_float _horizontal_angles;
+  PTA_float _candela_values;
+};
+
+#endif // IESDATASET_H

+ 140 - 0
contrib/src/rplight/internalLightManager.I

@@ -0,0 +1,140 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+/**
+ * @brief Returns the maximum light index
+ * @details This returns the maximum light index (also called slot). Any lights
+ *   after that slot are guaranteed to be zero-lights. This is useful when
+ *   iterating over the list of lights, because iteration can be stopped when
+ *   the maximum light index is reached.
+ *
+ *   The maximum light index points to the last slot which is used. If no lights
+ *   are attached, -1 is returned. If one light is attached at slot 0, the index
+ *   is 0, if two are attached at the slots 0 and 1, the index is 1, and so on.
+ *
+ *   If, for example, two lights are attached at the slots 2 and 5, then the
+ *   index will be 5. Keep in mind that the max-index is not an indicator for
+ *   how many lights are attached. Also, zero lights still may occur when iterating
+ *   over the light lists
+ *
+ * @return Maximum light index
+ */
+inline int InternalLightManager::get_max_light_index() const {
+  return _lights.get_max_index();
+}
+
+/**
+ * @brief Returns the amount of stored lights.
+ * @details This returns the amount of stored lights. This behaves unlike
+ *   InternalLightManager::get_max_light_index, and instead returns the true
+ *   amount of lights, which is completely unrelated to the amount of used slots.
+ *
+ * @return Amount of stored lights
+ */
+inline size_t InternalLightManager::get_num_lights() const {
+  return _lights.get_num_entries();
+}
+
+/**
+ * @brief Returns the amount of shadow sources.
+ * @details This returns the total amount of stored shadow sources. This does
+ *   not denote the amount of updated sources, but instead takes into account
+ *   all sources, even those out of frustum.
+ * @return Amount of shadow sources.
+ */
+inline size_t InternalLightManager::get_num_shadow_sources() const {
+  return _shadow_sources.get_num_entries();
+}
+
+/**
+ * @brief Sets the handle to the shadow manager
+ * @details This sets the handle to the global shadow manager. It is usually
+ *   constructed on the python side, so we need to get a handle to it.
+ *
+ *   The manager should be a handle to a ShadowManager instance, and will be
+ *   stored somewhere on the python side most likely. The light manager does not
+ *   keep a reference to it, so the python side should make sure to keep one.
+ *
+ *   Be sure to call this before the InternalLightManager::update() method is
+ *   called, otherwise an assertion will get triggered.
+ *
+ * @param mgr The ShadowManager instance
+ */
+inline void InternalLightManager::set_shadow_manager(ShadowManager* mgr) {
+  _shadow_manager = mgr;
+}
+
+/**
+ * @brief Sets a handle to the command list
+ * @details This sets a handle to the global GPUCommandList. This is required to
+ *   emit GPUCommands, which are used for attaching and detaching lights, as well
+ *   as shadow source updates.
+ *
+ *   The cmd_list should be a handle to a GPUCommandList handle, and will be
+ *   stored somewhere on the python side most likely. The light manager does not
+ *   keep a reference to it, so the python side should make sure to keep one.
+ *
+ *   Be sure to call this before the InternalLightManager::update() method is
+ *   called, otherwise an assertion will get triggered.
+ *
+ * @param cmd_list The GPUCommandList instance
+ */
+inline void InternalLightManager::set_command_list(GPUCommandList *cmd_list) {
+  _cmd_list = cmd_list;
+}
+
+/**
+ * @brief Sets the camera position
+ * @details This sets the camera position, which will be used to determine which
+ *   shadow sources have to get updated
+ *
+ * @param mat View projection mat
+ */
+inline void InternalLightManager::set_camera_pos(const LPoint3 &pos) {
+  _camera_pos = pos;
+}
+
+/**
+ * @brief Sets the maximum shadow update distance
+ * @details This controls the maximum distance until which shadows are updated.
+ *   If a shadow source is past that distance, it is ignored and no longer recieves
+ *   updates until it is in range again
+ *
+ * @param dist Distance in world space units
+ */
+inline void InternalLightManager::set_shadow_update_distance(PN_stdfloat dist) {
+  _shadow_update_distance = dist;
+}
+
+/**
+ * @brief Returns the internal used ShadowManager
+ * @details This returns a handle to the internally used shadow manager
+ * @return Shadow manager
+ */
+inline ShadowManager* InternalLightManager::get_shadow_manager() const {
+  return _shadow_manager;
+}

+ 441 - 0
contrib/src/rplight/internalLightManager.cxx

@@ -0,0 +1,441 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "internalLightManager.h"
+
+#include <algorithm>
+
+NotifyCategoryDef(lightmgr, "");
+
+
+/**
+ * @brief Constructs the light manager
+ * @details This constructs the light manager, initializing the light and shadow
+ *   storage. You should set a command list and shadow manager before calling
+ *   InternalLightManager::update. s
+ */
+InternalLightManager::InternalLightManager() {
+  _shadow_update_distance = 100.0;
+  _cmd_list = nullptr;
+  _shadow_manager = nullptr;
+}
+
+/**
+ * @brief Adds a new light.
+ * @details This adds a new light to the list of lights. This will throw an
+ *   error and return if the light is already attached. You may only call
+ *   this after the ShadowManager was already set.
+ *
+ *   While the light is attached, the light manager keeps a reference to it, so
+ *   the light does not get destructed.
+ *
+ *   This also setups the shadows on the light, in case shadows are enabled.
+ *   While a light is attached, you can not change whether it casts shadows or not.
+ *   To do so, detach the light, change the setting, and re-add the light.
+ *
+ *   In case no free light slot is available, an error will be printed and no
+ *   action will be performed.
+ *
+ *   If no shadow manager was set, an assertion will be triggered.
+ *
+ * @param light The light to add.
+ */
+void InternalLightManager::add_light(PT(RPLight) light) {
+  nassertv(_shadow_manager != nullptr); // Shadow manager not set yet!
+
+  // Don't attach the light in case its already attached
+  if (light->has_slot()) {
+    lightmgr_cat.error() << "could not add light because it already is attached! "
+               << "Detach the light first, then try it again." << endl;
+    return;
+  }
+
+  // Find a free slot
+  size_t slot;
+  if (!_lights.find_slot(slot)) {
+    lightmgr_cat.error() << "Light limit of " << MAX_LIGHT_COUNT << " reached, "
+               << "all light slots used!" << endl;
+    return;
+  }
+
+  // Reference the light because we store it, to avoid it getting destructed
+  // on the python side while we still work with it. The reference will be
+  // removed when the light gets detached.
+  light->ref();
+
+  // Reserve the slot
+  light->assign_slot(slot);
+  _lights.reserve_slot(slot, light);
+
+  // Setup the shadows in case the light uses them
+  if (light->get_casts_shadows()) {
+    setup_shadows(light);
+  }
+
+  // Store the light on the gpu, to make sure the GPU directly knows about it.
+  // We could wait until the next update cycle, but then we might be one frame
+  // too late already.
+  gpu_update_light(light);
+}
+
+/**
+ * @brief Internal method to setup shadows for a light
+ * @details This method gets called by the InternalLightManager::add_light method
+ *   to setup a lights shadow sources, in case shadows are enabled on that light.
+ *
+ *   It finds a slot for all shadow sources of the ilhgt, and inits the shadow
+ *   sources as well. If no slot could be found, an error is printed an nothing
+ *   happens.
+ *
+ * @param light The light to init the shadow sources for
+ */
+void InternalLightManager::setup_shadows(RPLight* light) {
+
+  // Init the lights shadow sources, and also call update once to make sure
+  // the sources are properly initialized
+  light->init_shadow_sources();
+  light->update_shadow_sources();
+
+  // Find consecutive slots, this is important for PointLights so we can just
+  // store the first index of the source, and get the other slots by doing
+  // first_index + 1, +2 and so on.
+  size_t base_slot;
+  size_t num_sources = light->get_num_shadow_sources();
+  if (!_shadow_sources.find_consecutive_slots(base_slot, num_sources)) {
+    lightmgr_cat.error() << "Failed to find slot for shadow sources! "
+               << "Shadow-Source limit of " << MAX_SHADOW_SOURCES
+               << " reached!" << endl;
+    return;
+  }
+
+  // Init all sources
+  for (int i = 0; i < num_sources; ++i) {
+    ShadowSource* source = light->get_shadow_source(i);
+
+    // Set the source as dirty, so it gets updated in the beginning
+    source->set_needs_update(true);
+
+    // Assign the slot to the source. Since we got consecutive slots, we can
+    // just do base_slot + N.
+    size_t slot = base_slot + i;
+    _shadow_sources.reserve_slot(slot, source);
+    source->set_slot(slot);
+  }
+}
+
+/**
+ * @brief Removes a light
+ * @details This detaches a light. This prevents it from being rendered, and also
+ *   cleans up all resources used by that light. If no reference is kept on the
+ *   python side, the light will also get destructed.
+ *
+ *   If the light was not previously attached with InternalLightManager::add_light,
+ *   an error will be triggered and nothing happens.
+ *
+ *   In case the light was set to cast shadows, all shadow sources are cleaned
+ *   up, and their regions in the shadow atlas are freed.
+ *
+ *   All resources used by the light in the light and shadow storage are also
+ *   cleaned up, by emitting cleanup GPUCommands.
+ *
+ *   If no shadow manager was set, an assertion will be triggered.
+ *
+ * @param light [description]
+ */
+void InternalLightManager::remove_light(PT(RPLight) light) {
+  nassertv(_shadow_manager != nullptr);
+
+  if (!light->has_slot()) {
+    lightmgr_cat.error() << "Could not detach light, light was not attached!" << endl;
+    return;
+  }
+
+  // Free the lights slot in the light storage
+  _lights.free_slot(light->get_slot());
+
+  // Tell the GPU we no longer need the lights data
+  gpu_remove_light(light);
+
+  // Mark the light as detached. After this call, we can not call get_slot
+  // anymore, so its important we do this after we unregistered the light
+  // from everywhere.
+  light->remove_slot();
+
+  // Clear shadow related stuff, in case the light casts shadows
+  if (light->get_casts_shadows()) {
+
+    // Free the slots of all sources, and also unregister their regions from
+    // the shadow atlas.
+    for (size_t i = 0; i < light->get_num_shadow_sources(); ++i) {
+      ShadowSource* source = light->get_shadow_source(i);
+      if (source->has_slot()) {
+        _shadow_sources.free_slot(source->get_slot());
+      }
+      if (source->has_region()) {
+        _shadow_manager->get_atlas()->free_region(source->get_region());
+        source->clear_region();
+      }
+    }
+
+    // Remove all sources of the light by emitting a consecutive remove command
+    gpu_remove_consecutive_sources(light->get_shadow_source(0),
+                     light->get_num_shadow_sources());
+
+    // Finally remove all shadow sources. This is important in case the light
+    // will be re-attached. Otherwise an assertion will get triggered.
+    light->clear_shadow_sources();
+  }
+
+  // Since we referenced the light when we stored it, we have to decrease
+  // the reference now. In case no reference was kept on the python side,
+  // the light will get destructed soon.
+  light->unref();
+}
+
+/**
+ * @brief Internal method to remove consecutive sources from the GPU.
+ * @details This emits a GPUCommand to consecutively remove shadow sources from
+ *   the GPU. This is called when a light gets removed, to free the space its
+ *   shadow sources took. Its not really required, because as long as the light
+ *   is not used, there is no reference to the sources. However, it can't hurt to
+ *   cleanup the memory.
+ *
+ *   All sources starting at first_source->get_slot() until
+ *   first_source->get_slot() + num_sources will get cleaned up.
+ *
+ * @param first_source First source of the light
+ * @param num_sources Amount of consecutive sources to clear
+ */
+void InternalLightManager::gpu_remove_consecutive_sources(ShadowSource *first_source,
+                              size_t num_sources) {
+  nassertv(_cmd_list != nullptr);    // No command list set yet
+  nassertv(first_source->has_slot()); // Source has no slot!
+  GPUCommand cmd_remove(GPUCommand::CMD_remove_sources);
+  cmd_remove.push_int(first_source->get_slot());
+  cmd_remove.push_int(num_sources);
+  _cmd_list->add_command(cmd_remove);
+}
+
+/**
+ * @brief Internal method to remove a light from the GPU.
+ * @details This emits a GPUCommand to clear a lights data. This sets the data
+ *   to all zeros, marking that no light is stored anymore.
+ *
+ *   This throws an assertion in case the light is not currently attached. Be
+ *   sure to call this before detaching the light.
+ *
+ * @param light The light to remove, must be attached.
+ */
+void InternalLightManager::gpu_remove_light(RPLight* light) {
+  nassertv(_cmd_list != nullptr);  // No command list set yet
+  nassertv(light->has_slot());  // Light has no slot!
+  GPUCommand cmd_remove(GPUCommand::CMD_remove_light);
+  cmd_remove.push_int(light->get_slot());
+  _cmd_list->add_command(cmd_remove);
+}
+
+/**
+ * @brief Updates a lights data on the GPU
+ * @details This method emits a GPUCommand to update a lights data. This can
+ *   be used to initially store the lights data, or to update the data whenever
+ *   the light changed.
+ *
+ *   This throws an assertion in case the light is not currently attached. Be
+ *   sure to call this after attaching the light.
+ *
+ * @param light The light to update
+ */
+void InternalLightManager::gpu_update_light(RPLight* light) {
+  nassertv(_cmd_list != nullptr);  // No command list set yet
+  nassertv(light->has_slot());  // Light has no slot!
+  GPUCommand cmd_update(GPUCommand::CMD_store_light);
+  cmd_update.push_int(light->get_slot());
+  light->write_to_command(cmd_update);
+  light->set_needs_update(false);
+  _cmd_list->add_command(cmd_update);
+}
+
+/**
+ * @brief Updates a shadow source data on the GPU
+ * @details This emits a GPUCommand to update a given shadow source, storing all
+ *   data of the source on the GPU. This can also be used to initially store a
+ *   ShadowSource, since all data will be overridden.
+ *
+ *   This throws an assertion if the source has no slot yet.
+ *
+ * @param source The source to update
+ */
+void InternalLightManager::gpu_update_source(ShadowSource* source) {
+  nassertv(_cmd_list != nullptr);  // No command list set yet
+  nassertv(source->has_slot()); // Source has no slot!
+  GPUCommand cmd_update(GPUCommand::CMD_store_source);
+  cmd_update.push_int(source->get_slot());
+  source->write_to_command(cmd_update);
+  _cmd_list->add_command(cmd_update);
+}
+
+/**
+ * @brief Internal method to update all lights
+ * @details This is called by the main update method, and iterates over the list
+ *   of lights. If a light is marked as dirty, it will recieve an update of its
+ *   data and its shadow sources.
+ */
+void InternalLightManager::update_lights() {
+  for (auto iter = _lights.begin(); iter != _lights.end(); ++iter) {
+    RPLight* light = *iter;
+    if (light && light->get_needs_update()) {
+      if (light->get_casts_shadows()) {
+        light->update_shadow_sources();
+      }
+      gpu_update_light(light);
+    }
+  }
+}
+
+/**
+ * @brief Compares shadow sources by their priority
+ * @details Returns if a has a greater priority than b. This depends on the
+ *   resolution of the source, and also if the source has a region or not.
+ *   This method can be passed to std::sort.
+ *
+ * @param a First source
+ * @param b Second source
+ *
+ * @return true if a is more important than b, else false
+ */
+bool InternalLightManager::compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const {
+
+  // Make sure that sources which already have a region (but maybe outdated)
+  // come after sources which have no region at all.
+  if (a->has_region() != b->has_region()) {
+    return b->has_region();
+  }
+
+  // Compare sources based on their distance to the camera
+  PN_stdfloat dist_a = (_camera_pos - a->get_bounds().get_center()).length_squared();
+  PN_stdfloat dist_b = (_camera_pos - a->get_bounds().get_center()).length_squared();
+
+  // XXX: Should also compare based on source size, so that huge sources recieve
+  // more updates
+
+  return dist_b > dist_a;
+}
+
+/**
+ * @brief Internal method to update all shadow sources
+ * @details This updates all shadow sources which are marked dirty. It will sort
+ *   the list of all dirty shadow sources by their resolution, take the first
+ *   n entries, and update them. The amount of sources processed depends on the
+ *   max_updates of the ShadowManager.
+ */
+void InternalLightManager::update_shadow_sources() {
+
+  // Find all dirty shadow sources and make a list of them
+  vector<ShadowSource*> sources_to_update;
+   for (auto iter = _shadow_sources.begin(); iter != _shadow_sources.end(); ++iter) {
+    ShadowSource* source = *iter;
+    if (source) {
+      const BoundingSphere& bounds = source->get_bounds();
+
+      // Check if source is in range
+      PN_stdfloat distance_to_camera = (_camera_pos - bounds.get_center()).length() - bounds.get_radius();
+      if (distance_to_camera < _shadow_update_distance) {
+        if (source->get_needs_update()) {
+          sources_to_update.push_back(source);
+        }
+      } else {
+
+        // Free regions of sources which are out of the update radius,
+        // to make space for other regions
+        if (source->has_region()) {
+          _shadow_manager->get_atlas()->free_region(source->get_region());
+          source->clear_region();
+        }
+      }
+    }
+
+  }
+
+  // Sort the sources based on their importance, so that sources with a bigger
+  // priority come first. This helps to get a better packing on the shadow atlas.
+  // However, we also need to prioritize sources which have no current region,
+  // because no shadows are worse than outdated-shadows.
+  std::sort(sources_to_update.begin(), sources_to_update.end(), [this](const ShadowSource* a, const ShadowSource* b) {
+    return this->compare_shadow_sources(a, b);
+  });
+
+  // Get a handle to the atlas, will be frequently used
+  ShadowAtlas *atlas = _shadow_manager->get_atlas();
+
+  // Free the regions of all sources which will get updated. We have to take into
+  // account that only a limited amount of sources can get updated per frame.
+  size_t update_slots = min(sources_to_update.size(),
+                _shadow_manager->get_num_update_slots_left());
+  for(size_t i = 0; i < update_slots; ++i) {
+    if (sources_to_update[i]->has_region()) {
+       atlas->free_region(sources_to_update[i]->get_region());
+    }
+  }
+
+  // Find an atlas spot for all regions which are supposed to get an update
+  for (size_t i = 0; i < update_slots; ++i) {
+    ShadowSource *source = sources_to_update[i];
+
+    if(!_shadow_manager->add_update(source)) {
+      // In case the ShadowManager lied about the number of updates left
+      lightmgr_cat.error() << "ShadowManager ensured update slot, but slot is taken!" << endl;
+      break;
+    }
+
+    // We have an update slot, and are guaranteed to get updated as soon
+    // as possible, so we can start getting a new atlas position.
+    size_t region_size = atlas->get_required_tiles(source->get_resolution());
+    LVecBase4i new_region = atlas->find_and_reserve_region(region_size, region_size);
+    LVecBase4 new_uv_region = atlas->region_to_uv(new_region);
+    source->set_region(new_region, new_uv_region);
+
+    // Mark the source as updated
+    source->set_needs_update(false);
+    gpu_update_source(source);
+  }
+}
+
+/**
+ * @brief Main update method
+ * @details This is the main update method of the InternalLightManager. It
+ *   processes all lights and shadow sources, updates them, and notifies the
+ *   GPU about it. This should be called on a per-frame basis.
+ *
+ *   If the InternalLightManager was not initialized yet, an assertion is thrown.
+ */
+void InternalLightManager::update() {
+  nassertv(_shadow_manager != nullptr); // Not initialized yet!
+  nassertv(_cmd_list != nullptr);     // Not initialized yet!
+
+  update_lights();
+  update_shadow_sources();
+}

+ 102 - 0
contrib/src/rplight/internalLightManager.h

@@ -0,0 +1,102 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef INTERNALLIGHTMANAGER_H
+#define INTERNALLIGHTMANAGER_H
+
+#include "referenceCount.h"
+#include "rpLight.h"
+#include "shadowSource.h"
+#include "shadowAtlas.h"
+#include "shadowManager.h"
+#include "pointerSlotStorage.h"
+#include "gpuCommandList.h"
+
+#define MAX_LIGHT_COUNT 65535
+#define MAX_SHADOW_SOURCES 2048
+
+NotifyCategoryDecl(lightmgr, EXPORT_CLASS, EXPORT_TEMPL);
+
+/**
+ * @brief Internal class used for handling lights and shadows.
+ * @details This is the internal class used by the pipeline to handle all
+ *   lights and shadows. It stores references to the lights, manages handling
+ *   the light and shadow slots, and also communicates with the GPU with the
+ *   GPUCommandQueue to store light and shadow source data.
+ */
+class InternalLightManager {
+PUBLISHED:
+  InternalLightManager();
+
+  void add_light(PT(RPLight) light);
+  void remove_light(PT(RPLight) light);
+
+  void update();
+  inline void set_camera_pos(const LPoint3 &pos);
+  inline void set_shadow_update_distance(PN_stdfloat dist);
+
+  inline int get_max_light_index() const;
+  MAKE_PROPERTY(max_light_index, get_max_light_index);
+
+  inline size_t get_num_lights() const;
+  MAKE_PROPERTY(num_lights, get_num_lights);
+
+  inline size_t get_num_shadow_sources() const;
+  MAKE_PROPERTY(num_shadow_sources, get_num_shadow_sources);
+
+  inline void set_shadow_manager(ShadowManager* mgr);
+  inline ShadowManager* get_shadow_manager() const;
+  MAKE_PROPERTY(shadow_manager, get_shadow_manager, set_shadow_manager);
+
+  inline void set_command_list(GPUCommandList *cmd_list);
+
+protected:
+
+  void gpu_update_light(RPLight* light);
+  void gpu_update_source(ShadowSource* source);
+  void gpu_remove_light(RPLight* light);
+  void gpu_remove_consecutive_sources(ShadowSource *first_source, size_t num_sources);
+
+  void setup_shadows(RPLight* light);
+  bool compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const;
+
+  void update_lights();
+  void update_shadow_sources();
+
+  GPUCommandList* _cmd_list;
+  ShadowManager* _shadow_manager;
+
+  PointerSlotStorage<RPLight*, MAX_LIGHT_COUNT> _lights;
+  PointerSlotStorage<ShadowSource*, MAX_SHADOW_SOURCES> _shadow_sources;
+
+  LPoint3 _camera_pos;
+  float _shadow_update_distance;
+
+};
+
+#include "internalLightManager.I"
+
+#endif // INTERNALLIGHTMANAGER_H

+ 13 - 0
contrib/src/rplight/p3rplight_composite1.cxx

@@ -0,0 +1,13 @@
+#include "config_rplight.cxx"
+#include "gpuCommand.cxx"
+#include "gpuCommandList.cxx"
+#include "iesDataset.cxx"
+#include "internalLightManager.cxx"
+#include "pssmCameraRig.cxx"
+#include "rpLight.cxx"
+#include "rpPointLight.cxx"
+#include "rpSpotLight.cxx"
+#include "shadowAtlas.cxx"
+#include "shadowManager.cxx"
+#include "shadowSource.cxx"
+#include "tagStateManager.cxx"

+ 239 - 0
contrib/src/rplight/pointerSlotStorage.h

@@ -0,0 +1,239 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef POINTERSLOTSTORAGE_H
+#define POINTERSLOTSTORAGE_H
+
+
+#ifdef CPPPARSER
+
+// Dummy implementation for interrogate
+template <typename T, int SIZE>
+class PointerSlotStorage {};
+
+#else // CPPPARSER
+
+
+#include "pandabase.h"
+
+// Apple has an outdated libstdc++, so pull the class from TR1.
+#if defined(__GLIBCXX__) && __GLIBCXX__ <= 20070719
+#include <tr1/array>
+using std::tr1::array;
+#else
+#include <array>
+#endif
+
+/**
+ * @brief Class to keep a list of pointers and nullpointers.
+ * @details This class stores a fixed size list of pointers, whereas pointers
+ *   may be a nullptr as well. It provides functionality to find free slots,
+ *   and also to find free consecutive slots, as well as taking care of reserving slots.
+ *
+ * @tparam T* Pointer-Type
+ * @tparam SIZE Size of the storage
+ */
+template <typename T, int SIZE>
+class PointerSlotStorage {
+public:
+  /**
+   * @brief Constructs a new PointerSlotStorage
+   * @details This constructs a new PointerSlotStorage, with all slots
+   *   initialized to a nullptr.
+   */
+  PointerSlotStorage() {
+#if defined(__GLIBCXX__) && __GLIBCXX__ <= 20070719
+    _data.assign(nullptr);
+#else
+    _data.fill(nullptr);
+#endif
+    _max_index = 0;
+    _num_entries = 0;
+  }
+
+  /**
+   * @brief Returns the maximum index of the container
+   * @details This returns the greatest index of any element which is not zero.
+   *   This can be useful for iterating the container, since all elements
+   *   coming after the returned index are guaranteed to be a nullptr.
+   *
+   *   If no elements are in this container, -1 is returned.
+   * @return Maximum index of the container
+   */
+  int get_max_index() const {
+    return _max_index;
+  }
+
+  /**
+   * @brief Returns the amount of elements of the container
+   * @details This returns the amount of elements in the container which are
+   *   no nullptr.
+   * @return Amount of elements
+   */
+  size_t get_num_entries() const {
+    return _num_entries;
+  }
+
+  /**
+   * @brief Finds a free slot
+   * @details This finds the first slot which is a nullptr and returns it.
+   *   This is most likely useful in combination with reserve_slot.
+   *
+   *   When no slot found was found, slot will be undefined, and false will
+   *   be returned.
+   *
+   * @param slot Output-Variable, slot will be stored there
+   * @return true if a slot was found, otherwise false
+   */
+  bool find_slot(size_t &slot) const {
+    for (size_t i = 0; i < SIZE; ++i) {
+      if (_data[i] == nullptr) {
+        slot = i;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * @brief Finds free consecutive slots
+   * @details This behaves like find_slot, but it tries to find a slot
+   *   after which <num_consecutive-1> free slots follow as well.
+   *
+   *   When no slot found was found, slot will be undefined, and false will
+   *   be returned.
+   *
+   * @param slot Output-Variable, index of the first slot of the consecutive
+   *   slots will be stored there.
+   * @param num_consecutive Amount of consecutive slots to find, including the
+   *   first slot.
+   *
+   * @return true if consecutive slots were found, otherwise false.
+   */
+  bool find_consecutive_slots(size_t &slot, size_t num_consecutive) const {
+    nassertr(num_consecutive > 0, false);
+
+    // Fall back to default search algorithm in case the parameters are equal
+    if (num_consecutive == 1) {
+      return find_slot(slot);
+    }
+
+    // Try to find consecutive slots otherwise
+    for (size_t i = 0; i < SIZE; ++i) {
+      bool any_taken = false;
+      for (size_t k = 0; !any_taken && k < num_consecutive; ++k) {
+        any_taken = _data[i + k] != nullptr;
+      }
+      if (!any_taken) {
+        slot = i;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * @brief Frees an allocated slot
+   * @details This frees an allocated slot. If the slot was already freed
+   *   before, this method throws an assertion.
+   *
+   * @param slot Slot to free
+   */
+  void free_slot(size_t slot) {
+    nassertv(slot >= 0 && slot < SIZE);
+    nassertv(_data[slot] != nullptr); // Slot was already empty!
+    _data[slot] = nullptr;
+    _num_entries--;
+
+    // Update maximum index
+    if (slot == _max_index) {
+      while (_max_index >= 0 && !_data[_max_index--]);
+    }
+  }
+
+  /**
+   * @brief Frees consecutive allocated slots
+   * @details This behaves like PointerSlotStorage::free_slot, but deletes
+   *   consecutive slots.
+   *
+   * @param slot Start of the consecutive slots to free
+   * @param num_consecutive Number of consecutive slots
+   */
+  void free_consecutive_slots(size_t slot, size_t num_consecutive) {
+    for (size_t i = slot; i < slot + num_consecutive; ++i) {
+      free_slot(i);
+    }
+  }
+
+  /**
+   * @brief Reserves a slot
+   * @details This reserves a slot by storing a pointer in it. If the slot
+   *   was already taken, throws an assertion.
+   *   If the ptr is a nullptr, also throws an assertion.
+   *   If the slot was out of bounds, also throws an assertion.
+   *
+   * @param slot Slot to reserve
+   * @param ptr Pointer to store
+   */
+  void reserve_slot(size_t slot, T ptr) {
+    nassertv(slot >= 0 && slot < SIZE);
+    nassertv(_data[slot] == nullptr); // Slot already taken!
+    nassertv(ptr != nullptr); // nullptr passed as argument!
+    _max_index = max(_max_index, (int)slot);
+    _data[slot] = ptr;
+    _num_entries++;
+  }
+
+  typedef array<T, SIZE> InternalContainer;
+
+  /**
+   * @brief Returns an iterator to the begin of the container
+   * @details This returns an iterator to the beginning of the container
+   * @return Begin-Iterator
+   */
+  typename InternalContainer::iterator begin() {
+    return _data.begin();
+  }
+
+  /**
+   * @brief Returns an iterator to the end of the container
+   * @details This returns an iterator to the end of the iterator. This only
+   *   iterates to PointerSlotStorage::get_max_index()
+   * @return [description]
+   */
+  typename InternalContainer::iterator end() {
+    return _data.begin() + _max_index + 1;
+  }
+
+private:
+  int _max_index;
+  size_t _num_entries;
+  InternalContainer _data;
+};
+
+#endif // CPPPARSER
+
+#endif // POINTERSLOTSTORAGE_H

+ 243 - 0
contrib/src/rplight/pssmCameraRig.I

@@ -0,0 +1,243 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+/**
+ * @brief Sets the maximum pssm distance.
+ * @details This sets the maximum distance in world space until which shadows
+ *   are rendered. After this distance, no shadows will be rendered.
+ *
+ *   If the distance is below zero, an assertion is triggered.
+ *
+ * @param distance Maximum distance in world space
+ */
+inline void PSSMCameraRig::set_pssm_distance(float distance) {
+  nassertv(distance > 0.0 && distance < 100000.0);
+  _pssm_distance = distance;
+}
+
+/**
+ * @brief Sets the suns distance
+ * @details This sets the distance the cameras will have from the cameras frustum.
+ *   This prevents far objects from having no shadows, which can occur when these
+ *   objects are between the cameras frustum and the sun, but not inside of the
+ *   cameras frustum. Setting the sun distance high enough will move the cameras
+ *   away from the camera frustum, being able to cover those distant objects too.
+ *
+ *   If the sun distance is set too high, artifacts will occur due to the reduced
+ *   range of depth. If a value below zero is passed, an assertion will get
+ *   triggered.
+ *
+ * @param distance The sun distance
+ */
+inline void PSSMCameraRig::set_sun_distance(float distance) {
+  nassertv(distance > 0.0 && distance < 100000.0);
+  _sun_distance = distance;
+}
+
+/**
+ * @brief Sets the logarithmic factor
+ * @details This sets the logarithmic factor, which is the core of the algorithm.
+ *   PSSM splits the camera frustum based on a linear and a logarithmic factor.
+ *   While a linear factor provides a good distribution, it often is not applicable
+ *   for wider distances. A logarithmic distribution provides a better distribution
+ *   at distance, but suffers from splitting in the near areas.
+ *
+ *   The logarithmic factor mixes the logarithmic and linear split distribution,
+ *   to get the best of both. A greater factor will make the distribution more
+ *   logarithmic, while a smaller factor will make it more linear.
+ *
+ *   If the factor is below zero, an ssertion is triggered.
+ *
+ * @param factor The logarithmic factor
+ */
+inline void PSSMCameraRig::set_logarithmic_factor(float factor) {
+  nassertv(factor > 0.0);
+  _logarithmic_factor = factor;
+}
+
+/**
+ * @brief Sets whether to use a fixed film size
+ * @details This controls if a fixed film size should be used. This will cause
+ *   the camera rig to cache the current film size, and only change it in case
+ *   it gets too small. This provides less flickering when moving, because the
+ *   film size will stay roughly constant. However, to prevent the cached film
+ *   size getting too big, one should call PSSMCameraRig::reset_film_size
+ *   once in a while, otherwise there might be a lot of wasted space.
+ *
+ * @param flag Whether to use a fixed film size
+ */
+inline void PSSMCameraRig::set_use_fixed_film_size(bool flag) {
+  _use_fixed_film_size = flag;
+}
+
+/**
+ * @brief Sets the resolution of each split
+ * @details This sets the resolution of each split. Currently it is equal for
+ *   each split. This is required when using PSSMCameraRig::set_use_stable_csm,
+ *   to compute how bix a texel is.
+ *
+ *   It has to match the y-resolution of the pssm shadow map. If an invalid
+ *   resolution is triggered, an assertion is thrown.
+ *
+ * @param resolution The resolution of each split.
+ */
+inline void PSSMCameraRig::set_resolution(size_t resolution) {
+  nassertv(resolution >= 0 && resolution < 65535);
+  _resolution = resolution;
+}
+
+/**
+ * @brief Sets whether to use stable CSM snapping.
+ * @details This option controls if stable CSM snapping should be used. When the
+ *   option is enabled, all splits will snap to their texels, so that when moving,
+ *   no flickering will occur. However, this only works when the splits do not
+ *   change their film size, rotation and angle.
+ *
+ * @param flag Whether to use stable CSM snapping
+ */
+inline void PSSMCameraRig::set_use_stable_csm(bool flag) {
+  _use_stable_csm = flag;
+}
+
+/**
+ * @brief Sets the border bias for each split
+ * @details This sets the border bias for every split. This increases each
+ *   splits frustum by multiplying it by (1 + bias), and helps reducing artifacts
+ *   at the borders of the splits. Artifacts can occur when the bias is too low,
+ *   because then the filtering will go over the bounds of the split, producing
+ *   invalid results.
+ *
+ *   If the bias is below zero, an assertion is thrown.
+ *
+ * @param bias Border bias
+ */
+inline void PSSMCameraRig::set_border_bias(float bias) {
+  nassertv(bias >= 0.0);
+  _border_bias = bias;
+}
+
+/**
+ * @brief Resets the film size cache
+ * @details In case PSSMCameraRig::set_use_fixed_film_size is used, this resets
+ *   the film size cache. This might lead to a small "jump" in the shadows,
+ *   because the film size changes, however it leads to a better shadow distribution.
+ *
+ *   This is the case because when using a fixed film size, the cache will get
+ *   bigger and bigger, whenever the camera moves to a grazing angle. However,
+ *   when moving back to a normal angle, the film size cache still stores this
+ *   big angle, and thus the splits will have a much bigger film size than actualy
+ *   required. To prevent this, call this method once in a while, so an optimal
+ *   distribution is ensured.
+ */
+inline void PSSMCameraRig::reset_film_size_cache() {
+  for (size_t i = 0; i < _max_film_sizes.size(); ++i) {
+    _max_film_sizes[i].fill(0);
+  }
+}
+
+/**
+ * @brief Returns the n-th camera
+ * @details This returns the n-th camera of the camera rig, which can be used
+ *   for various stuff like showing its frustum, passing it as a shader input,
+ *   and so on.
+ *
+ *   The first camera is the camera which is the camera of the first split,
+ *   which is the split closest to the camera. All cameras follow in descending
+ *   order until to the last camera, which is the split furthest away from the
+ *   camera.
+ *
+ *   If an invalid index is passed, an assertion is thrown.
+ *
+ * @param index Index of the camera.
+ * @return [description]
+ */
+inline NodePath PSSMCameraRig::get_camera(size_t index) {
+  nassertr(index >= 0 && index < _cam_nodes.size(), NodePath());
+  return _cam_nodes[index];
+}
+
+/**
+ * @brief Internal method to compute the distance of a split
+ * @details This is the internal method to perform the weighting of the
+ *   logarithmic and linear distribution. It computes the distance to the
+ *   camera from which a given split starts, by weighting the logarithmic and
+ *   linear factor.
+ *
+ *   The return value is a value ranging from 0 .. 1. To get the distance in
+ *   world space, the value has to get multiplied with the maximum shadow distance.
+ *
+ * @param split_index The index of the split
+ * @return Distance of the split, ranging from 0 .. 1
+ */
+inline float PSSMCameraRig::get_split_start(size_t split_index) {
+  float x = (float)split_index / (float)_cam_nodes.size();
+  return (exp(_logarithmic_factor*x)-1) / (exp(_logarithmic_factor)-1);
+}
+
+/**
+ * @brief Internal method for interpolating a point along the camera frustum
+ * @details This method takes a given distance in the 0 .. 1 range, whereas
+ *   0 denotes the camera near plane, and 1 denotes the camera far plane,
+ *   and lineary interpolates between them.
+ *
+ * @param origin Edge of the frustum
+ * @param depth Distance in the 0 .. 1 range
+ *
+ * @return interpolated point in world space
+ */
+inline LPoint3 PSSMCameraRig::get_interpolated_point(CoordinateOrigin origin, float depth) {
+  nassertr(depth >= 0.0 && depth <= 1.0, LPoint3());
+  return _curr_near_points[origin] * (1.0 - depth) + _curr_far_points[origin] * depth;
+}
+
+/**
+ * @brief Returns a handle to the MVP array
+ * @details This returns a handle to the array of view-projection matrices
+ *   of the different splits. This can be used for computing shadows. The array
+ *   is a PTALMatrix4 and thus can be directly bound to a shader.
+ *
+ * @return view-projection matrix array
+ */
+inline const PTA_LMatrix4 &PSSMCameraRig::get_mvp_array() {
+  return _camera_mvps;
+}
+
+/**
+ * @brief Returns a handle to the near and far planes array
+ * @details This returns a handle to the near and far plane array. Each split
+ *   has an entry in the array, whereas the x component of the vecto denotes the
+ *   near plane, and the y component denotes the far plane of the split.
+ *
+ *   This is required because the near and far planes of the splits change
+ *   constantly. To access them in a shader, the shader needs access to the
+ *   array.
+ *
+ * @return Array of near and far planes
+ */
+inline const PTA_LVecBase2 &PSSMCameraRig::get_nearfar_array() {
+  return _camera_nearfar;
+}

+ 396 - 0
contrib/src/rplight/pssmCameraRig.cxx

@@ -0,0 +1,396 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "pssmCameraRig.h"
+
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include "orthographicLens.h"
+
+
+PStatCollector PSSMCameraRig::_update_collector("App:Show code:RP_PSSM_update");
+
+/**
+ * @brief Constructs a new PSSM camera rig
+ * @details This constructs a new camera rig, with a given amount of splits.
+ *   The splits can not be changed later on. Splits are also called Cascades.
+ *
+ *   An assertion will be triggered if the splits are below zero.
+ *
+ * @param num_splits Amount of PSSM splits
+ */
+PSSMCameraRig::PSSMCameraRig(size_t num_splits) {
+  nassertv(num_splits > 0);
+  _num_splits = num_splits;
+  _pssm_distance = 100.0;
+  _sun_distance = 500.0;
+  _use_fixed_film_size = false;
+  _use_stable_csm = true;
+  _logarithmic_factor = 1.0;
+  _resolution = 512;
+  _border_bias = 0.1;
+  _camera_mvps = PTA_LMatrix4::empty_array(num_splits);
+  _camera_nearfar = PTA_LVecBase2::empty_array(num_splits);
+  init_cam_nodes();
+}
+
+/**
+ * @brief Destructs the camera rig
+ * @details This destructs the camera rig, cleaning up all used resources.
+ */
+PSSMCameraRig::~PSSMCameraRig() {
+  // TODO: Detach all cameras and call remove_node. Most likely this is not
+  // an issue tho, because the camera rig will never get destructed.
+}
+
+/**
+ * @brief Internal method to init the cameras
+ * @details This method constructs all cameras and their required lens nodes
+ *   for all splits. It also resets the film size array.
+ */
+void PSSMCameraRig::init_cam_nodes() {
+  _cam_nodes.reserve(_num_splits);
+  _max_film_sizes.resize(_num_splits);
+  _cameras.resize(_num_splits);
+  for (size_t i = 0; i < _num_splits; ++i)
+  {
+    // Construct a new lens
+    Lens *lens = new OrthographicLens();
+    lens->set_film_size(1, 1);
+    lens->set_near_far(1, 1000);
+
+    // Construct a new camera
+    _cameras[i] = new Camera("pssm-cam-" + format_string(i), lens);
+    _cam_nodes.push_back(NodePath(_cameras[i]));
+    _max_film_sizes[i].fill(0);
+  }
+}
+
+/**
+ * @brief Reparents the camera rig
+ * @details This reparents all cameras to the given parent. Usually the parent
+ *   will be ShowBase.render. The parent should be the same node where the
+ *   main camera is located in, too.
+ *
+ *   If an empty parrent is passed, an assertion will get triggered.
+ *
+ * @param parent Parent node path
+ */
+void PSSMCameraRig::reparent_to(NodePath parent) {
+  nassertv(!parent.is_empty());
+  for (size_t i = 0; i < _num_splits; ++i) {
+    _cam_nodes[i].reparent_to(parent);
+  }
+  _parent = parent;
+}
+
+/**
+ * @brief Internal method to compute the view-projection matrix of a camera
+ * @details This returns the view-projection matrix of the given split. No bounds
+ *   checking is done. If an invalid index is passed, undefined behaviour occurs.
+ *
+ * @param split_index Index of the split
+ * @return view-projection matrix of the split
+ */
+LMatrix4 PSSMCameraRig::compute_mvp(size_t split_index) {
+  LMatrix4 transform = _parent.get_transform(_cam_nodes[split_index])->get_mat();
+  return transform * _cameras[split_index]->get_lens()->get_projection_mat();
+}
+
+/**
+ * @brief Internal method used for stable CSM
+ * @details This method is used when stable CSM is enabled. It ensures that each
+ *   source only moves in texel-steps, thus preventing flickering. This works by
+ *   projecting the point (0, 0, 0) to NDC space, making sure that it gets projected
+ *   to a texel center, and then projecting that texel back.
+ *
+ *   This only works if the camera does not rotate, change its film size, or change
+ *   its angle.
+ *
+ * @param mat view-projection matrix of the camera
+ * @param resolution resolution of the split
+ *
+ * @return Offset to add to the camera position to achieve stable snapping
+ */
+LVecBase3 PSSMCameraRig::get_snap_offset(const LMatrix4& mat, size_t resolution) {
+  // Transform origin to camera space
+  LPoint4 base_point = mat.get_row(3) * 0.5 + 0.5;
+
+  // Compute the snap offset
+  float texel_size = 1.0 / (float)(resolution);
+  float offset_x = fmod(base_point.get_x(), texel_size);
+  float offset_y = fmod(base_point.get_y(), texel_size);
+
+  // Reproject the offset back, for that we need the inverse MVP
+  LMatrix4 inv_mat(mat);
+  inv_mat.invert_in_place();
+  LVecBase3 new_base_point = inv_mat.xform_point(LVecBase3(
+      (base_point.get_x() - offset_x) * 2.0 - 1.0,
+      (base_point.get_y() - offset_y) * 2.0 - 1.0,
+      base_point.get_z() * 2.0 - 1.0
+    ));
+  return -new_base_point;
+}
+
+/**
+ * @brief Computes the average of a list of points
+ * @details This computes the average over a given set of points in 3D space.
+ *   It returns the average of those points, namely sum_of_points / num_points.
+ *
+ *   It is designed to work with a frustum, which is why it takes two arrays
+ *   with a dimension of 4. Usually the first array are the camera near points,
+ *   and the second array are the camera far points.
+ *
+ * @param starts First array of points
+ * @param ends Second array of points
+ * @return Average of points
+ */
+LPoint3 get_average_of_points(LVecBase3 const (&starts)[4], LVecBase3 const (&ends)[4]) {
+  LPoint3 mid_point(0, 0, 0);
+  for (size_t k = 0; k < 4; ++k) {
+    mid_point += starts[k];
+    mid_point += ends[k];
+  }
+  return mid_point / 8.0;
+}
+
+/**
+ * @brief Finds the minimum and maximum extends of the given projection
+ * @details This projects each point of the given array of points using the
+ *   cameras view-projection matrix, and computes the minimum and maximum
+ *   of the projected points.
+ *
+ * @param min_extent Will store the minimum extent of the projected points in NDC space
+ * @param max_extent Will store the maximum extent of the projected points in NDC space
+ * @param transform The transformation matrix of the camera
+ * @param proj_points The array of points to project
+ * @param cam The camera to be used to project the points
+ */
+void find_min_max_extents(LVecBase3 &min_extent, LVecBase3 &max_extent, const LMatrix4 &transform, LVecBase3 const (&proj_points)[8], Camera *cam) {
+
+  min_extent.fill(1e10);
+  max_extent.fill(-1e10);
+  LPoint2 screen_points[8];
+
+  // Now project all points to the screen space of the current camera and also
+  // find the minimum and maximum extents
+  for (size_t k = 0; k < 8; ++k) {
+    LVecBase4 point(proj_points[k], 1);
+    LPoint4 proj_point = transform.xform(point);
+    LPoint3 proj_point_3d(proj_point.get_x(), proj_point.get_y(), proj_point.get_z());
+    cam->get_lens()->project(proj_point_3d, screen_points[k]);
+
+    // Find min / max extents
+    if (screen_points[k].get_x() > max_extent.get_x()) max_extent.set_x(screen_points[k].get_x());
+    if (screen_points[k].get_y() > max_extent.get_y()) max_extent.set_y(screen_points[k].get_y());
+
+    if (screen_points[k].get_x() < min_extent.get_x()) min_extent.set_x(screen_points[k].get_x());
+    if (screen_points[k].get_y() < min_extent.get_y()) min_extent.set_y(screen_points[k].get_y());
+
+    // Find min / max projected depth to adjust far plane
+    if (proj_point.get_y() > max_extent.get_z()) max_extent.set_z(proj_point.get_y());
+    if (proj_point.get_y() < min_extent.get_z()) min_extent.set_z(proj_point.get_y());
+  }
+}
+
+/**
+ * @brief Computes a film size from a given minimum and maximum extend
+ * @details This takes a minimum and maximum extent in NDC space and computes
+ *   the film size and film offset needed to cover that extent.
+ *
+ * @param film_size Output film size, can be used for Lens::set_film_size
+ * @param film_offset Output film offset, can be used for Lens::set_film_offset
+ * @param min_extent Minimum extent
+ * @param max_extent Maximum extent
+ */
+inline void get_film_properties(LVecBase2 &film_size, LVecBase2 &film_offset, const LVecBase3 &min_extent, const LVecBase3 &max_extent) {
+  float x_center = (min_extent.get_x() + max_extent.get_x()) * 0.5;
+  float y_center = (min_extent.get_y() + max_extent.get_y()) * 0.5;
+  float x_size = max_extent.get_x() - x_center;
+  float y_size = max_extent.get_y() - y_center;
+  film_size.set(x_size, y_size);
+  film_offset.set(x_center * 0.5, y_center * 0.5);
+}
+
+/**
+ * @brief Merges two arrays
+ * @details This takes two arrays which each 4 members and produces an array
+ *   with both arrays contained.
+ *
+ * @param dest Destination array
+ * @param array1 First array
+ * @param array2 Second array
+ */
+inline void merge_points_interleaved(LVecBase3 (&dest)[8], LVecBase3 const (&array1)[4], LVecBase3 const (&array2)[4]) {
+  for (size_t k = 0; k < 4; ++k) {
+    dest[k] = array1[k];
+    dest[k+4] = array2[k];
+  }
+}
+
+
+/**
+ * @brief Internal method to compute the splits
+ * @details This is the internal update method to update the PSSM splits.
+ *   It distributes the camera splits over the frustum, and updates the
+ *   MVP array aswell as the nearfar array.
+ *
+ * @param transform Main camera transform
+ * @param max_distance Maximum pssm distance, relative to the camera far plane
+ * @param light_vector Sun-Vector
+ */
+void PSSMCameraRig::compute_pssm_splits(const LMatrix4& transform, float max_distance, const LVecBase3& light_vector) {
+  nassertv(!_parent.is_empty());
+
+  // PSSM Distance should never be smaller than camera far plane.
+  nassertv(max_distance <= 1.0);
+
+  float filmsize_bias = 1.0 + _border_bias;
+
+  // Compute the positions of all cameras
+  for (size_t i = 0; i < _cam_nodes.size(); ++i) {
+    float split_start = get_split_start(i) * max_distance;
+    float split_end = get_split_start(i + 1) * max_distance;
+
+    LVecBase3 start_points[4];
+    LVecBase3 end_points[4];
+    LVecBase3 proj_points[8];
+
+    // Get split bounding box, and collect all points which define the frustum
+    for (size_t k = 0; k < 4; ++k) {
+      start_points[k] = get_interpolated_point((CoordinateOrigin)k, split_start);
+      end_points[k] = get_interpolated_point((CoordinateOrigin)k, split_end);
+      proj_points[k] = start_points[k];
+      proj_points[k + 4] = end_points[k];
+    }
+
+    // Compute approximate split mid point
+    LPoint3 split_mid = get_average_of_points(start_points, end_points);
+    LPoint3 cam_start = split_mid + light_vector * _sun_distance;
+
+    // Reset the film size, offset and far-plane
+    Camera* cam = DCAST(Camera, _cam_nodes[i].node());
+    cam->get_lens()->set_film_size(1, 1);
+    cam->get_lens()->set_film_offset(0, 0);
+    cam->get_lens()->set_near_far(1, 100);
+
+    // Find a good initial position
+    _cam_nodes[i].set_pos(cam_start);
+    _cam_nodes[i].look_at(split_mid);
+
+    LVecBase3 best_min_extent, best_max_extent;
+
+    // Find minimum and maximum extents of the points
+    LMatrix4 merged_transform = _parent.get_transform(_cam_nodes[i])->get_mat();
+    find_min_max_extents(best_min_extent, best_max_extent, merged_transform, proj_points, cam);
+
+    // Find the film size to cover all points
+    LVecBase2 film_size, film_offset;
+    get_film_properties(film_size, film_offset, best_min_extent, best_max_extent);
+
+    if (_use_fixed_film_size) {
+      // In case we use a fixed film size, store the maximum film size, and
+      // only change the film size if a new maximum is there
+      if (_max_film_sizes[i].get_x() < film_size.get_x()) _max_film_sizes[i].set_x(film_size.get_x());
+      if (_max_film_sizes[i].get_y() < film_size.get_y()) _max_film_sizes[i].set_y(film_size.get_y());
+
+      cam->get_lens()->set_film_size(_max_film_sizes[i] * filmsize_bias);
+    } else {
+      // If we don't use a fixed film size, we can just set the film size
+      // on the lens.
+      cam->get_lens()->set_film_size(film_size * filmsize_bias);
+    }
+
+    // Compute new film offset
+    cam->get_lens()->set_film_offset(film_offset);
+    cam->get_lens()->set_near_far(10, best_max_extent.get_z());
+    _camera_nearfar[i] = LVecBase2(10, best_max_extent.get_z());
+
+    // Compute the camera MVP
+    LMatrix4 mvp = compute_mvp(i);
+
+    // Stable CSM Snapping
+    if (_use_stable_csm) {
+      LPoint3 snap_offset = get_snap_offset(mvp, _resolution);
+      _cam_nodes[i].set_pos(_cam_nodes[i].get_pos() + snap_offset);
+
+      // Compute the new mvp, since we changed the snap offset
+      mvp = compute_mvp(i);
+    }
+
+    _camera_mvps.set_element(i, mvp);
+  }
+}
+
+
+/**
+ * @brief Updates the PSSM camera rig
+ * @details This updates the rig with an updated camera position, and a given
+ *   light vector. This should be called on a per-frame basis. It will reposition
+ *   all camera sources to fit the frustum based on the pssm distribution.
+ *
+ *   The light vector should be the vector from the light source, not the
+ *   vector to the light source.
+ *
+ * @param cam_node Target camera node
+ * @param light_vector The vector from the light to any point
+ */
+void PSSMCameraRig::update(NodePath cam_node, const LVecBase3 &light_vector) {
+  nassertv(!cam_node.is_empty());
+  _update_collector.start();
+
+  // Get camera node transform
+  LMatrix4 transform = cam_node.get_transform()->get_mat();
+
+  // Get Camera and Lens pointers
+  Camera* cam = DCAST(Camera, cam_node.get_child(0).node());
+  nassertv(cam != nullptr);
+  Lens* lens = cam->get_lens();
+
+  // Extract near and far points:
+  lens->extrude(LPoint2(-1, 1),  _curr_near_points[UpperLeft],  _curr_far_points[UpperLeft]);
+  lens->extrude(LPoint2(1, 1),   _curr_near_points[UpperRight], _curr_far_points[UpperRight]);
+  lens->extrude(LPoint2(-1, -1), _curr_near_points[LowerLeft],  _curr_far_points[LowerLeft]);
+  lens->extrude(LPoint2(1, -1),  _curr_near_points[LowerRight], _curr_far_points[LowerRight]);
+
+  // Construct MVP to project points to world space
+  LMatrix4 mvp = transform * lens->get_view_mat();
+
+  // Project all points to world space
+  for (size_t i = 0; i < 4; ++i) {
+    LPoint4 ws_near = mvp.xform(_curr_near_points[i]);
+    LPoint4 ws_far = mvp.xform(_curr_far_points[i]);
+    _curr_near_points[i].set(ws_near.get_x(), ws_near.get_y(), ws_near.get_z());
+    _curr_far_points[i].set(ws_far.get_x(), ws_far.get_y(), ws_far.get_z());
+  }
+
+  // Do the actual PSSM
+  compute_pssm_splits( transform, _pssm_distance / lens->get_far(), light_vector );
+
+  _update_collector.stop();
+}
+

+ 125 - 0
contrib/src/rplight/pssmCameraRig.h

@@ -0,0 +1,125 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef PSSMCAMERARIG_H
+#define PSSMCAMERARIG_H
+
+#include "pandabase.h"
+#include "luse.h"
+#include "camera.h"
+#include "nodePath.h"
+#include "pStatCollector.h"
+
+#include <vector>
+
+/**
+ * @brief Main class used for handling PSSM
+ * @details This is the main class for supporting PSSM, it is used by the PSSM
+ *   plugin to compute the position of the splits.
+ *
+ *   It supports handling a varying amount of cameras, and fitting those cameras
+ *   into the main camera frustum, to render distant shadows. It also supports
+ *   various optimizations for fitting the frustum, e.g. rotating the sources
+ *   to get a better coverage.
+ *
+ *   It also provides methods to get arrays of data about the used cameras
+ *   view-projection matrices and their near and far plane, which is required for
+ *   processing the data in the shadow sampling shader.
+ *
+ *   In this class, there is often referred to "Splits" or also called "Cascades".
+ *   These denote the different cameras which are used to split the frustum,
+ *   and are a common term related to the PSSM algorithm.
+ *
+ *   To understand the functionality of this class, a detailed knowledge of the
+ *   PSSM algorithm is helpful.
+ */
+class PSSMCameraRig {
+PUBLISHED:
+  PSSMCameraRig(size_t num_splits);
+  ~PSSMCameraRig();
+
+  inline void set_pssm_distance(float distance);
+  inline void set_sun_distance(float distance);
+  inline void set_use_fixed_film_size(bool flag);
+  inline void set_resolution(size_t resolution);
+  inline void set_use_stable_csm(bool flag);
+  inline void set_logarithmic_factor(float factor);
+  inline void set_border_bias(float bias);
+
+  void update(NodePath cam_node, const LVecBase3 &light_vector);
+  inline void reset_film_size_cache();
+
+  inline NodePath get_camera(size_t index);
+
+  void reparent_to(NodePath parent);
+  inline const PTA_LMatrix4 &get_mvp_array();
+  inline const PTA_LVecBase2 &get_nearfar_array();
+
+public:
+  // Used to access the near and far points in the array
+  enum CoordinateOrigin {
+    UpperLeft = 0,
+    UpperRight,
+    LowerLeft,
+    LowerRight
+  };
+
+protected:
+  void init_cam_nodes();
+  void compute_pssm_splits(const LMatrix4& transform, float max_distance,
+               const LVecBase3 &light_vector);
+  inline float get_split_start(size_t split_index);
+  LMatrix4 compute_mvp(size_t cam_index);
+  inline LPoint3 get_interpolated_point(CoordinateOrigin origin, float depth);
+  LVecBase3 get_snap_offset(const LMatrix4& mat, size_t resolution);
+
+  vector<NodePath> _cam_nodes;
+  vector<Camera*> _cameras;
+  vector<LVecBase2> _max_film_sizes;
+
+  // Current near and far points
+  // Order: UL, UR, LL, LR (See CoordinateOrigin)
+  LPoint3 _curr_near_points[4];
+  LPoint3 _curr_far_points[4];
+  float _pssm_distance;
+  float _sun_distance;
+  float _logarithmic_factor;
+  float _border_bias;
+  bool _use_fixed_film_size;
+  bool _use_stable_csm;
+  size_t _resolution;
+  size_t _num_splits;
+  NodePath _parent;
+
+  PTA_LMatrix4 _camera_mvps;
+  PTA_LVecBase2 _camera_nearfar;
+
+  static PStatCollector _update_collector;
+};
+
+#include "pssmCameraRig.I"
+
+#endif // PSSMCAMERARIG_H

+ 406 - 0
contrib/src/rplight/rpLight.I

@@ -0,0 +1,406 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+/**
+ * @brief Returns the amount of shadow sources
+ * @details This returns the amount of shadow sources attached to this light.
+ *   In case the light has no shadows enabled, or the light was not attached
+ *   yet, this returns 0.
+ *
+ * @return Amount of shadow sources
+ */
+inline int RPLight::get_num_shadow_sources() const {
+  return _shadow_sources.size();
+}
+
+/**
+ * @brief Returns the n-th shadow source
+ * @details This returns the n-th attached shadow source. This ranges from
+ *   0 .. RPLight::get_num_shadow_sources(). If an invalid index is passed,
+ *   an assertion is thrown.
+ *
+ * @param index Index of the source
+ * @return Handle to the shadow source
+ */
+inline ShadowSource* RPLight::get_shadow_source(size_t index) const {
+  nassertr(index < _shadow_sources.size(), nullptr); // Invalid shadow source index
+  return _shadow_sources[index];
+}
+
+/**
+ * @brief Clears all shadow source
+ * @details This removes and destructs all shadow sources attached to this light.
+ *   This usually gets called when the light gets detached or destructed.
+ *   All shadows sources are freed, and then removed from the shadow source list.
+ */
+inline void RPLight::clear_shadow_sources() {
+  for (size_t i = 0; i < _shadow_sources.size(); ++i) {
+    delete _shadow_sources[i];
+  }
+  _shadow_sources.clear();
+}
+
+/**
+ * @brief Sets whether the light needs an update
+ * @details This controls whether the light needs to get an update. This is the
+ *   case when a property of the light changed, e.g. position or color. It does
+ *   not affect the shadows (For that use RPLight::invalidate_shadows()).
+ *   When this flag is set to true, the light will get resubmitted to the GPU
+ *   in the next update cycle.
+ *
+ *   You should usually never set the flag to false manually. The
+ *   InternalLightManager will do this when the data got sucessfully updated.
+ *
+ * @param flag Update-Flag
+ */
+inline void RPLight::set_needs_update(bool flag) {
+  _needs_update = flag;
+}
+
+/**
+ * @brief Returns whether the light needs an update
+ * @details This returns whether the light needs an update. This might be the
+ *   case when a property of the light was changed, e.g. position or color.
+ *   It does not affect the shadows, you have to query the update flag of each
+ *   individual source for that.
+ *   The return value is the value previously set with RPLight::set_needs_update.
+ *
+ * @return Update-flag
+ */
+inline bool RPLight::get_needs_update() const {
+  return _needs_update;
+}
+
+/**
+ * @brief Returns whether the light has a slot
+ * @details This returns wheter the light currently is attached, and thus has
+ *   a slot in the InternalLightManagers light list. When the light is attached,
+ *   this returns true, otherwise it will return false.
+ *
+ * @return true if the light has a slot, false otherwise
+ */
+inline bool RPLight::has_slot() const {
+  return _slot >= 0;
+}
+
+/**
+ * @brief Returns the slot of the light
+ * @details This returns the slot of the light. This is the space on the GPU
+ *   where the light is stored. If the light is not attached yet, this will
+ *   return -1, otherwise the index of the light.
+ *
+ * @return Light-Slot
+ */
+inline int RPLight::get_slot() const {
+  return _slot;
+}
+
+/**
+ * @brief Removes the light slot
+ * @details This is an internal method to remove the slot of the light. It gets
+ *   called by the InternalLightManager when a light gets detached. It internally
+ *   sets the slot to -1 to indicate the light is no longer attached.
+ */
+inline void RPLight::remove_slot() {
+  _slot = -1;
+}
+
+/**
+ * @brief Assigns a slot to the light
+ * @details This assigns a slot to the light, marking it as attached. The slot
+ *   relates to the index in the GPU's storage of lights. This is an internal
+ *   method called by the InternalLightManager when the light got attached.
+ *
+ * @param slot Slot of the light
+ */
+inline void RPLight::assign_slot(int slot) {
+  _slot = slot;
+}
+
+/**
+ * @brief Invalidates the shadows
+ * @details This invalidates all shadows of the light, causing them to get
+ *   regenerated. This might be the case  when the lights position or similar
+ *   changed. This will cause all shadow sources to be updated, emitting a
+ *   shadow update. Be careful when calling this method if you don't want all
+ *   sources to get updated. If you only have to invalidate a single shadow source,
+ *   use get_shadow_source(n)->set_needs_update(true).
+ */
+inline void RPLight::invalidate_shadows() {
+  for (size_t i = 0; i < _shadow_sources.size(); ++i) {
+    _shadow_sources[i]->set_needs_update(true);
+  }
+}
+
+/**
+ * @brief Sets the position of the light
+ * @details This sets the position of the light in world space. It will cause
+ *   the light to get invalidated, and resubmitted to the GPU.
+ *
+ * @param pos Position in world space
+ */
+inline void RPLight::set_pos(const LVecBase3 &pos) {
+  set_pos(pos.get_x(), pos.get_y(), pos.get_z());
+}
+
+/**
+ * @brief Sets the position of the light
+ * @details @copydetails RPLight::set_pos(const LVecBase3 &pos)
+ *
+ * @param x X-component of the position
+ * @param y Y-component of the position
+ * @param z Z-component of the position
+ */
+inline void RPLight::set_pos(float x, float y, float z) {
+  _position.set(x, y, z);
+  set_needs_update(true);
+  invalidate_shadows();
+}
+
+/**
+ * @brief Returns the position of the light
+ * @details This returns the position of the light previously set with
+ *   RPLight::set_pos(). The returned position is in world space.
+ * @return Light-position
+ */
+inline const LVecBase3& RPLight::get_pos() const {
+  return _position;
+}
+
+/**
+ * @brief Sets the lights color
+ * @details This sets the lights color. The color should not include the brightness
+ *   of the light, you should control that with the energy. The color specifies
+ *   the lights "tint" and will get multiplied with its specular and diffuse
+ *   contribution.
+ *
+ *   The color will be normalized by dividing by the colors luminance. Setting
+ *   higher values than 1.0 will have no effect.
+ *
+ * @param color Light color
+ */
+inline void RPLight::set_color(const LVecBase3 &color) {
+  _color = color;
+  _color /= 0.2126 * color.get_x() + 0.7152 * color.get_y() + 0.0722 * color.get_z();
+  set_needs_update(true);
+}
+
+/**
+ * @brief Sets the lights color
+ * @details @copydetails RPLight::set_color(const LVecBase3 &color)
+ *
+ * @param r Red-component of the color
+ * @param g Green-component of the color
+ * @param b Blue-component of the color
+ */
+inline void RPLight::set_color(float r, float g, float b) {
+  set_color(LVecBase3(r, g, b));
+}
+
+/**
+ * @brief Returns the lights color
+ * @details This returns the light color, previously set with RPLight::set_color.
+ *   This does not include the energy of the light. It might differ from what
+ *   was set with set_color, because the color is normalized by dividing it
+ *   by its luminance.
+ * @return Light-color
+ */
+inline const LVecBase3& RPLight::get_color() const {
+  return _color;
+}
+
+/**
+ * @brief Sets the energy of the light
+ * @details This sets the energy of the light, which can be seen as the brightness
+ *   of the light. It will get multiplied with the normalized color.
+ *
+ * @param energy energy of the light
+ */
+inline void RPLight::set_energy(float energy) {
+  _energy = energy;
+  set_needs_update(true);
+}
+
+/**
+ * @brief Returns the energy of the light
+ * @details This returns the energy of the light, previously set with
+ *   RPLight::set_energy.
+ *
+ * @return energy of the light
+ */
+inline float RPLight::get_energy() const {
+  return _energy;
+}
+
+/**
+ * @brief Returns the type of the light
+ * @details This returns the internal type of the light, which was specified
+ *   in the lights constructor. This can be used to distinguish between light
+ *   types.
+ * @return Type of the light
+ */
+inline RPLight::LightType RPLight::get_light_type() const {
+  return _light_type;
+}
+
+/**
+ * @brief Controls whether the light casts shadows
+ * @details This sets whether the light casts shadows. You can not change this
+ *   while the light is attached. When flag is set to true, the light will be
+ *   setup to cast shadows, spawning shadow sources based on the lights type.
+ *   If the flag is set to false, the light will be inddicated to cast no shadows.
+ *
+ * @param flag Whether the light casts shadows
+ */
+inline void RPLight::set_casts_shadows(bool flag) {
+  if (has_slot()) {
+    cerr << "Light is already attached, can not call set_casts_shadows!" << endl;
+    return;
+  }
+  _casts_shadows = flag;
+}
+
+/**
+ * @brief Returns whether the light casts shadows
+ * @details This returns whether the light casts shadows, the returned value
+ *   is the one previously set with RPLight::set_casts_shadows.
+ *
+ * @return true if the light casts shadows, false otherwise
+ */
+inline bool RPLight::get_casts_shadows() const {
+  return _casts_shadows;
+}
+
+/**
+ * @brief Sets the lights shadow map resolution
+ * @details This sets the lights shadow map resolution. This has no effect
+ *   when the light is not told to cast shadows (Use RPLight::set_casts_shadows).
+ *
+ *   When calling this on a light with multiple shadow sources (e.g. PointLight),
+ *   this controls the resolution of each source. If the light has 6 shadow sources,
+ *   and you use a resolution of 512x512, the lights shadow map will occur a
+ *   space of 6 * 512x512 maps in the shadow atlas.
+ *
+ * @param resolution Resolution of the shadow map in pixels
+ */
+inline void RPLight::set_shadow_map_resolution(size_t resolution) {
+  nassertv(resolution >= 32 && resolution <= 16384);
+  _source_resolution = resolution;
+  invalidate_shadows();
+}
+
+/**
+ * @brief Returns the shadow map resolution
+ * @details This returns the shadow map resolution of each source of the light.
+ *   If the light is not setup to cast shadows, this value is meaningless.
+ *   The returned value is the one previously set with RPLight::set_shadow_map_resolution.
+ *
+ * @return Shadow map resolution in pixels
+ */
+inline size_t RPLight::get_shadow_map_resolution() const {
+  return _source_resolution;
+}
+
+/**
+ * @brief Sets the ies profile
+ * @details This sets the ies profile of the light. The parameter should be a
+ *   handle previously returned by RenderPipeline.load_ies_profile. Using a
+ *   value of -1 indicates no ies profile.
+ *
+ *   Notice that for ies profiles which cover a whole range, you should use
+ *   PointLights, whereas for ies profiles which only cover the lower hemisphere
+ *   you should use SpotLights for the best performance.
+ *
+ * @param profile IES Profile handle
+ */
+inline void RPLight::set_ies_profile(int profile) {
+  _ies_profile = profile;
+  set_needs_update(true);
+}
+
+/**
+ * @brief Returns the lights ies profile
+ * @details This returns the ies profile of a light, previously set with
+ *   RPLight::set_ies_profile. In case no ies profile was set, returns -1.
+ *
+ * @return IES Profile handle
+ */
+inline int RPLight::get_ies_profile() const {
+  return _ies_profile;
+}
+
+/**
+ * @brief Returns whether the light has an ies profile assigned
+ * @details This returns whether the light has an ies profile assigned,
+ *   previously done with RPLight::set_ies_profile.
+ *
+ * @return true if the light has an ies profile assigned, false otherwise
+ */
+inline bool RPLight::has_ies_profile() const {
+  return _ies_profile >= 0;
+}
+
+/**
+ * @brief Clears the ies profile
+ * @details This clears the ies profile of the light, telling it to no longer
+ *   use an ies profile, and instead use the default attenuation.
+ */
+inline void RPLight::clear_ies_profile() {
+  set_ies_profile(-1);
+}
+
+/**
+ * @brief Sets the near plane of the light
+ * @details This sets the near plane of all shadow sources of the light. It has
+ *   no effects if the light does not cast shadows. This prevents artifacts from
+ *   objects near to the light. It behaves like Lens::set_near_plane.
+ *
+ *   It can also help increasing shadow map precision, low near planes will
+ *   cause the precision to suffer. Try setting the near plane as big as possible.
+ *
+ *   If a negative or zero near plane is passed, an assertion is thrown.
+ *
+ * @param near_plane Near-plane
+ */
+inline void RPLight::set_near_plane(float near_plane) {
+  nassertv(near_plane > 0.00001);
+  _near_plane = near_plane;
+  invalidate_shadows();
+}
+
+/**
+ * @brief Returns the near plane of the light
+ * @details This returns the lights near plane, previously set with
+ *   RPLight::set_near_plane. If the light does not cast shadows, this value
+ *   is meaningless.
+ *
+ * @return Near-plane
+ */
+inline float RPLight::get_near_plane() const {
+  return _near_plane;
+}
+

+ 137 - 0
contrib/src/rplight/rpLight.cxx

@@ -0,0 +1,137 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "rpLight.h"
+
+
+/**
+ * @brief Constructs a new light with the given type
+ * @details This constructs a new base light with the given light type.
+ *   Sub-Classes should call this to initialize all properties.
+ *
+ * @param light_type Type of the light
+ */
+RPLight::RPLight(LightType light_type) {
+  _light_type = light_type;
+  _needs_update = false;
+  _casts_shadows = false;
+  _slot = -1;
+  _position.fill(0);
+  _color.fill(1);
+  _ies_profile = -1;
+  _source_resolution = 512;
+  _near_plane = 0.5;
+  _energy = 20.0;
+}
+
+/**
+ * @brief Writes the light to a GPUCommand
+ * @details This writes all of the lights data to the given GPUCommand handle.
+ *   Subclasses should first call this method, and then append their own
+ *   data. This makes sure that for unpacking a light, no information about
+ *   the type of the light is required.
+ *
+ * @param cmd The GPUCommand to write to
+ */
+void RPLight::write_to_command(GPUCommand &cmd) {
+  cmd.push_int(_light_type);
+  cmd.push_int(_ies_profile);
+
+  if (_casts_shadows) {
+    // If we casts shadows, write the index of the first source, we expect
+    // them to be consecutive
+    nassertv(_shadow_sources.size() >= 0);
+    nassertv(_shadow_sources[0]->has_slot());
+    cmd.push_int(_shadow_sources[0]->get_slot());
+  } else {
+    // If we cast no shadows, just push a negative number
+    cmd.push_int(-1);
+  }
+
+  cmd.push_vec3(_position);
+
+  // Get the lights color by multiplying color with energy. Divide by
+  // 100, since 16bit floating point buffers only go up to 65000.0, which
+  // prevents very bright lights 
+  cmd.push_vec3(_color * _energy / 100.0);
+}
+
+/**
+ * @brief Light destructor
+ * @details This destructs the light, cleaning up all resourced used. The light
+ *   should be detached at this point, because while the Light is attached,
+ *   the InternalLightManager holds a reference to prevent it from being
+ *   destructed.
+ */
+RPLight::~RPLight() {
+  nassertv(!has_slot()); // Light still attached - should never happen
+  clear_shadow_sources();
+}
+
+/**
+ * @brief Sets the lights color from a given color temperature
+ * @details This sets the lights color, given a temperature. This is more
+ *   physically based than setting a user defined color. The color will be
+ *   computed from the given temperature.
+ *
+ * @param temperature Light temperature
+ */
+void RPLight::set_color_from_temperature(float temperature) {
+
+  // Thanks to rdb for this conversion script
+  float mm = 1000.0 / temperature;
+  float mm2 = mm * mm;
+  float mm3 = mm2 * mm;
+  float x, y;
+
+  if (temperature < 4000) {
+    x = -0.2661239 * mm3 - 0.2343580 * mm2 + 0.8776956 * mm + 0.179910;
+  } else {
+    x = -3.0258469 * mm3 + 2.1070379 * mm2 + 0.2226347 * mm + 0.240390;
+  }
+
+  float x2 = x * x;
+  float x3 = x2 * x;
+  if (temperature < 2222) {
+    y = -1.1063814 * x3 - 1.34811020 * x2 + 2.18555832 * x - 0.20219683;
+  } else if (temperature < 4000) {
+    y = -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867;
+  } else {
+    y =  3.0817580 * x3 - 5.87338670 * x2 + 3.75112997 * x - 0.37001483;
+  }
+
+  // xyY to XYZ, assuming Y=1.
+  LVecBase3 xyz(x / y, 1, (1 - x - y) / y);
+
+  // Convert XYZ to linearized sRGB.
+  const static LMatrix3 xyz_to_rgb(
+   3.2406, -0.9689,  0.0557,
+  -1.5372,  1.8758, -0.2050,
+  -0.4986,  0.0415,  1.0570);
+
+  set_color(xyz_to_rgb.xform(xyz));
+}

+ 130 - 0
contrib/src/rplight/rpLight.h

@@ -0,0 +1,130 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RPLIGHT_H
+#define RPLIGHT_H
+
+#include "referenceCount.h"
+#include "luse.h"
+#include "gpuCommand.h"
+#include "shadowSource.h"
+
+/**
+ * @brief Base class for Lights
+ * @details This is the base class for all lights in the render pipeline. It
+ *   stores common properties, and provides methods to modify these.
+ *   It also defines some interface functions which subclasses have to implement.
+ */
+class RPLight : public ReferenceCount {
+PUBLISHED:
+  /**
+   * Different types of light.
+   */
+  enum LightType {
+    LT_empty = 0,
+    LT_point_light = 1,
+    LT_spot_light = 2,
+  };
+
+public:
+  RPLight(LightType light_type);
+  virtual ~RPLight();
+
+  virtual void init_shadow_sources() = 0;
+  virtual void update_shadow_sources() = 0;
+  virtual void write_to_command(GPUCommand &cmd);
+
+  inline int get_num_shadow_sources() const;
+  inline ShadowSource* get_shadow_source(size_t index) const;
+  inline void clear_shadow_sources();
+
+  inline void set_needs_update(bool flag);
+  inline bool get_needs_update() const;
+
+  inline bool has_slot() const;
+  inline int get_slot() const;
+  inline void remove_slot();
+  inline void assign_slot(int slot);
+
+PUBLISHED:
+  inline void invalidate_shadows();
+
+  inline void set_pos(const LVecBase3 &pos);
+  inline void set_pos(float x, float y, float z);
+  inline const LVecBase3& get_pos() const;
+  MAKE_PROPERTY(pos, get_pos, set_pos);
+
+  inline void set_color(const LVecBase3 &color);
+  inline void set_color(float r, float g, float b);
+  inline const LVecBase3& get_color() const;
+  MAKE_PROPERTY(color, get_color, set_color);
+
+  void set_color_from_temperature(float temperature);
+
+  inline void set_energy(float energy);
+  inline float get_energy() const;
+  MAKE_PROPERTY(energy, get_energy, set_energy);
+
+  inline LightType get_light_type() const;
+  MAKE_PROPERTY(light_type, get_light_type);
+
+  inline void set_casts_shadows(bool flag = true);
+  inline bool get_casts_shadows() const;
+  MAKE_PROPERTY(casts_shadows, get_casts_shadows, set_casts_shadows);
+
+  inline void set_shadow_map_resolution(size_t resolution);
+  inline size_t get_shadow_map_resolution() const;
+  MAKE_PROPERTY(shadow_map_resolution, get_shadow_map_resolution, set_shadow_map_resolution);
+
+  inline void set_ies_profile(int profile);
+  inline int get_ies_profile() const;
+  inline bool has_ies_profile() const;
+  inline void clear_ies_profile();
+  MAKE_PROPERTY2(ies_profile, has_ies_profile, get_ies_profile,
+                 set_ies_profile, clear_ies_profile);
+
+  inline void set_near_plane(float near_plane);
+  inline float get_near_plane() const;
+  MAKE_PROPERTY(near_plane, get_near_plane, set_near_plane);
+
+protected:
+  int _slot;
+  int _ies_profile;
+  size_t _source_resolution;
+  bool _needs_update;
+  bool _casts_shadows;
+  LVecBase3 _position;
+  LVecBase3 _color;
+  float _energy;
+  LightType _light_type;
+  float _near_plane;
+
+  vector<ShadowSource*> _shadow_sources;
+};
+
+#include "rpLight.I"
+
+#endif // RP_LIGHT_H

+ 82 - 0
contrib/src/rplight/rpPointLight.I

@@ -0,0 +1,82 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+/**
+ * @brief Sets the radius of the light
+ * @details This sets the radius of the light. It controls the lights
+ *   influence. After a distance greater than this radius, the light influence
+ *   is zero.
+ *
+ * @param radius Light radius in world space
+ */
+inline void RPPointLight::set_radius(float radius) {
+  nassertv(radius > 0); // Invalid light radius
+  _radius = radius;
+  set_needs_update(true);
+  invalidate_shadows();
+}
+
+/**
+ * @brief Returns the lights radius
+ * @details This returns the lights radius previously set with
+ *   RPPointLight::set_radius
+ * @return Light radius in world space
+ */
+inline float RPPointLight::get_radius() const {
+  return _radius;
+}
+
+/**
+ * @brief Sets the inner radius of the light
+ * @details This sets the inner radius of the light. Anything greater than
+ *   zero causes the light to get an area light. This has influence on the
+ *   specular highlights of the light aswell as the shadows.
+ *
+ *   The inner radius controls the size of the lights sphere size in world
+ *   space units. A radius of 0 means the light has no inner radius, and the
+ *   light will be have like an infinite small point light source.
+ *   A radius greater than zero will cause the light to behave like it would be
+ *   an emissive sphere with the given inner radius emitting light. This is
+ *   more physically correct.
+ *
+ * @param inner_radius Inner-radius in world space
+ */
+inline void RPPointLight::set_inner_radius(float inner_radius) {
+  nassertv(inner_radius >= 0.01); // Invalid inner radius
+  _inner_radius = inner_radius;
+  set_needs_update(true);
+}
+
+/**
+ * @brief Returns the inner radius of the light
+ * @details This returns the inner radius of the light, previously set with
+ *   RPPointLight::get_inner_radius.
+ * @return [description]
+ */
+inline float RPPointLight::get_inner_radius() const {
+  return _inner_radius;
+}

+ 90 - 0
contrib/src/rplight/rpPointLight.cxx

@@ -0,0 +1,90 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "rpPointLight.h"
+
+
+/**
+ * @brief Constructs a new point light
+ * @details This contructs a new point light with default settings. By default
+ *   the light is set to be an infinitely small point light source. You can
+ *   change this with RPPointLight::set_inner_radius.
+ */
+RPPointLight::RPPointLight() : RPLight(RPLight::LT_point_light) {
+  _radius = 10.0;
+  _inner_radius = 0.01;
+}
+
+/**
+ * @brief Writes the light to a GPUCommand
+ * @details This writes the point light data to a GPUCommand.
+ * @see RPLight::write_to_command
+ *
+ * @param cmd The target GPUCommand
+ */
+void RPPointLight::write_to_command(GPUCommand &cmd) {
+  RPLight::write_to_command(cmd);
+  cmd.push_float(_radius);
+  cmd.push_float(_inner_radius);
+}
+
+/**
+ * @brief Inits the shadow sources of the light
+ * @details This inits all required shadow sources for the point light.
+ * @see RPLight::init_shadow_sources
+ */
+void RPPointLight::init_shadow_sources() {
+  nassertv(_shadow_sources.size() == 0);
+  // Create 6 shadow sources, one for each direction
+  for(size_t i = 0; i < 6; ++i) {
+    _shadow_sources.push_back(new ShadowSource());
+  }
+}
+
+/**
+ * @brief Updates the shadow sources
+ * @details This updates all shadow sources of the light.
+ * @see RPLight::update_shadow_sources
+ */
+void RPPointLight::update_shadow_sources() {
+  LVecBase3 directions[6] = {
+    LVecBase3( 1,  0,  0),
+    LVecBase3(-1,  0,  0),
+    LVecBase3( 0,  1,  0),
+    LVecBase3( 0, -1,  0),
+    LVecBase3( 0,  0,  1),
+    LVecBase3( 0,  0, -1)
+  };
+
+  // Increase fov to prevent artifacts at the shadow map transitions
+  const float fov = 90.0f + 3.0f;
+  for (size_t i = 0; i < _shadow_sources.size(); ++i) {
+    _shadow_sources[i]->set_resolution(get_shadow_map_resolution());
+    _shadow_sources[i]->set_perspective_lens(fov, _near_plane, _radius,
+                        _position, directions[i]);
+  }
+}

+ 63 - 0
contrib/src/rplight/rpPointLight.h

@@ -0,0 +1,63 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RPPOINTLIGHT_H
+#define RPPOINTLIGHT_H
+
+#include "pandabase.h"
+#include "rpLight.h"
+
+/**
+ * @brief PointLight class
+ * @details This represents a point light, a light which has a position and
+ *   radius. Checkout the RenderPipeline documentation for more information
+ *   about this type of light.
+ */
+class RPPointLight : public RPLight {
+PUBLISHED:
+  RPPointLight();
+
+  inline void set_radius(float radius);
+  inline float get_radius() const;
+  MAKE_PROPERTY(radius, get_radius, set_radius);
+
+  inline void set_inner_radius(float inner_radius);
+  inline float get_inner_radius() const;
+  MAKE_PROPERTY(inner_radius, get_inner_radius, set_inner_radius);
+
+public:
+  virtual void write_to_command(GPUCommand &cmd);
+  virtual void update_shadow_sources();
+  virtual void init_shadow_sources();
+
+protected:
+  float _radius;
+  float _inner_radius;
+};
+
+#include "rpPointLight.I"
+
+#endif // RPPOINTLIGHT_H

+ 74 - 0
contrib/src/rplight/rpSpotLight.I

@@ -0,0 +1,74 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+
+inline void RPSpotLight::set_radius(float radius) {
+  _radius = radius;
+  set_needs_update(true);
+  invalidate_shadows();
+}
+
+inline float RPSpotLight::get_radius() const {
+  return _radius;
+}
+
+
+inline void RPSpotLight::set_fov(float fov) {
+  _fov = fov;
+  set_needs_update(true);
+  invalidate_shadows();
+}
+
+inline float RPSpotLight::get_fov() const {
+  return _fov;
+}
+
+inline void RPSpotLight::set_direction(LVecBase3 direction) {
+  _direction = direction;
+  _direction.normalize();
+  set_needs_update(true);
+  invalidate_shadows();
+}
+
+inline void RPSpotLight::set_direction(float dx, float dy, float dz) {
+  _direction.set(dx, dy, dz);
+  _direction.normalize();
+  set_needs_update(true);
+  invalidate_shadows();
+}
+
+inline const LVecBase3& RPSpotLight::get_direction() const {
+  return _direction;
+}
+
+inline void RPSpotLight::look_at(LVecBase3 point) {
+  set_direction(point - _position);
+}
+
+inline void RPSpotLight::look_at(float x, float y, float z) {
+  set_direction(LVecBase3(x, y, z) - _position);
+}

+ 80 - 0
contrib/src/rplight/rpSpotLight.cxx

@@ -0,0 +1,80 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "rpSpotLight.h"
+
+#define _USE_MATH_DEFINES
+#include <math.h>
+
+
+/**
+ * @brief Creates a new spot light
+ * @details This creates a new spot light with default properties set. You should
+ *   set at least a direction, fov, radius and position to make the light useful.
+ */
+RPSpotLight::RPSpotLight() : RPLight(RPLight::LT_spot_light) {
+  _radius = 10.0;
+  _fov = 45.0;
+  _direction.set(0, 0, -1);
+}
+
+/**
+ * @brief Writes the light to a GPUCommand
+ * @details This writes the spot light data to a GPUCommand.
+ * @see RPLight::write_to_command
+ *
+ * @param cmd The target GPUCommand
+ */
+void RPSpotLight::write_to_command(GPUCommand &cmd) {
+  RPLight::write_to_command(cmd);
+  cmd.push_float(_radius);
+
+  // Encode FOV as cos(fov)
+  cmd.push_float(cos(_fov / 360.0 * M_PI));
+  cmd.push_vec3(_direction);
+}
+
+/**
+ * @brief Inits the shadow sources of the light
+ * @details This inits all required shadow sources for the spot light.
+ * @see RPLight::init_shadow_sources
+ */
+void RPSpotLight::init_shadow_sources() {
+  nassertv(_shadow_sources.size() == 0);
+  _shadow_sources.push_back(new ShadowSource());
+}
+
+/**
+ * @brief Updates the shadow sources
+ * @details This updates all shadow sources of the light.
+ * @see RPLight::update_shadow_sources
+ */
+void RPSpotLight::update_shadow_sources() {
+  _shadow_sources[0]->set_resolution(get_shadow_map_resolution());
+  _shadow_sources[0]->set_perspective_lens(_fov, _near_plane, _radius, _position, _direction);
+}
+

+ 71 - 0
contrib/src/rplight/rpSpotLight.h

@@ -0,0 +1,71 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RPSPOTLIGHT_H
+#define RPSPOTLIGHT_H
+
+#include "pandabase.h"
+#include "rpLight.h"
+
+/**
+ * @brief SpotLight class
+ * @details This represents a spot light, a light which has a position, radius,
+ *   direction and FoV. Checkout the RenderPipeline documentation for more
+ *   information about this type of light.
+ */
+class RPSpotLight : public RPLight {
+PUBLISHED:
+  RPSpotLight();
+
+  inline void set_radius(float radius);
+  inline float get_radius() const;
+  MAKE_PROPERTY(radius, get_radius, set_radius);
+
+  inline void set_fov(float fov);
+  inline float get_fov() const;
+  MAKE_PROPERTY(fov, get_fov, set_fov);
+
+  inline void set_direction(LVecBase3 direction);
+  inline void set_direction(float dx, float dy, float dz);
+  inline const LVecBase3& get_direction() const;
+  inline void look_at(LVecBase3 point);
+  inline void look_at(float x, float y, float z);
+  MAKE_PROPERTY(direction, get_direction, set_direction);
+
+public:
+  virtual void write_to_command(GPUCommand &cmd);
+  virtual void init_shadow_sources();
+  virtual void update_shadow_sources();
+
+protected:
+  float _radius;
+  float _fov;
+  LVecBase3 _direction;
+};
+
+#include "rpSpotLight.I"
+
+#endif // RPSPOTLIGHT_H

+ 159 - 0
contrib/src/rplight/shadowAtlas.I

@@ -0,0 +1,159 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+/**
+ * @brief Returns the tile size of the atlas.
+ * @details This returns the tile size of the atlas, which was set in the
+ *   constructor. This is the smalles unit of the atlas, and every resolution
+ *   has to be a multiple of the tile size.
+ * @return Tile size in pixels
+ */
+inline int ShadowAtlas::get_tile_size() const {
+  return _tile_size;
+}
+
+/**
+ * @brief Sets a specific tile status.
+ * @details This marks a tile to either reserved or free, depending on the flag.
+ *   If flag is true, the tile gets marked as reserved. If flag is false, the
+ *   tile gets marked as free.
+ *
+ *   No bounds checking is done for performance reasons. Passing an invalid tile
+ *   index causes a crash. The coordinates are expected to be in tile space.
+ *
+ * @param x x-position of the tile
+ * @param y y-position of the tile
+ * @param flag Flag to set the tile to
+ */
+inline void ShadowAtlas::set_tile(size_t x, size_t y, bool flag) {
+  _flags[x + y * _num_tiles] = flag;
+}
+
+/**
+ * @brief Returns the status of a given tile.
+ * @details This returns the value of a tile. If the tile is true, this means
+ *   the tile is already reserved. If the tile is false, the tile can be
+ *   used, and is not reserved.
+ *
+ *   No bounds checking is done for performance reasons. Passing an invalid tile
+ *   index causes a crash. The coordinates are expected to be in tile space.
+ *
+ * @param x x-position of the tile
+ * @param y y-position of the tile
+ *
+ * @return Tile-Status
+ */
+inline bool ShadowAtlas::get_tile(size_t x, size_t y) const {
+  return _flags[x + y * _num_tiles];
+}
+
+/**
+ * @brief Checks wheter a given region is free.
+ * @details This checks whether a given region in the atlas is still free. This
+ *   is true if *all* tiles in that region are false, and thus are not taken yet.
+ *   The coordinates are expected to be in tile space.
+ *
+ *   Passing an invalid region, causes an assertion, in case those are enabled.
+ *   If assertions are optimized out, this method will crash when passing invalid
+ *   bounds.
+ *
+ * @param x x- start position of the region
+ * @param y y- start position of the region
+ * @param w width of the region
+ * @param h height of the region
+ * @return true if the region is completely free, else false
+ */
+inline bool ShadowAtlas::region_is_free(size_t x, size_t y, size_t w, size_t h) const {
+  // Check if we are out of bounds, this should be disabled for performance
+  // reasons at some point.
+  nassertr(x >= 0 && y >= 0 && x + w <= _num_tiles && y + h <= _num_tiles, false);
+
+  // Iterate over every tile in that region and check if it is still free.
+  for (size_t cx = 0; cx < w; ++cx) {
+    for (size_t cy = 0; cy < h; ++cy) {
+      if (get_tile(cx + x, cy + y)) return false;
+    }
+  }
+  return true;
+}
+
+/**
+ * @brief Returns the amount of tiles required to store a resolution.
+ * @details Returns the amount of tiles which would be required to store a
+ *   given resolution. This basically just returns resolution / tile_size.
+ *
+ *   When an invalid resolution is passed (not a multiple of the tile size),
+ *   an error is printed and 1 is returned.
+ *   When a negative or zero resolution is passed, undefined behaviour occurs.
+ *
+ * @param resolution The resolution to compute the amount of tiles for
+ * @return Amount of tiles to store the resolution
+ */
+inline int ShadowAtlas::get_required_tiles(size_t resolution) const {
+  nassertr(resolution > 0, -1);
+
+  if (resolution % _tile_size != 0) {
+    shadowatlas_cat.error() << "Resolution " << resolution << " is not a multiple "
+                << "of the shadow atlas tile size (" << _tile_size << ")!" << endl;
+    return 1;
+  }
+  return resolution / _tile_size;
+}
+
+/**
+ * @brief Converts a tile-space region to uv space.
+ * @details This converts a region (presumably from ShadowAtlas::find_and_reserve_region)
+ *   to uv space (0 .. 1 range). This can be used in shaders, since they expect
+ *   floating point coordinates instead of integer coordinates.
+ *
+ * @param region tile-space region
+ * @return uv-space region
+ */
+inline LVecBase4 ShadowAtlas::region_to_uv(const LVecBase4i& region) {
+  LVecBase4 flt = LVecBase4(region.get_x(), region.get_y(), region.get_z(), region.get_w());
+  return flt * ((float)_tile_size / (float)_size);
+}
+
+/**
+ * @brief Returns the amount of used tiles
+ * @details Returns the amount of used tiles in the atlas
+ * @return Amount of used tiles
+ */
+inline int ShadowAtlas::get_num_used_tiles() const {
+  return _num_used_tiles;
+}
+
+/**
+ * @brief Returns the amount of used tiles in percentage
+ * @details This returns in percentage from 0 to 1 how much space of the atlas
+ *   is used right now. A value of 1 means the atlas is completely full, whereas
+ *   a value of 0 means the atlas is completely free.
+ * @return Atlas usage in percentage
+ */
+inline float ShadowAtlas::get_coverage() const {
+  return float(_num_used_tiles) / float(_num_tiles * _num_tiles);
+}

+ 186 - 0
contrib/src/rplight/shadowAtlas.cxx

@@ -0,0 +1,186 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "shadowAtlas.h"
+#include <string.h>
+
+NotifyCategoryDef(shadowatlas, "");
+
+/**
+ * @brief Constructs a new shadow atlas.
+ * @details This constructs a new shadow atlas with the given size and tile size.
+ *
+ *   The size determines the total size of the atlas in pixels. It should be a
+ *   power-of-two to favour the GPU.
+ *
+ *   The tile_size determines the smallest unit of tiles the atlas can store.
+ *   If, for example, a tile_size of 32 is used, then every entry stored must
+ *   have a resolution of 32 or greater, and the resolution must be a multiple
+ *   of 32. This is to optimize the search in the atlas, so the atlas does not
+ *   have to check every pixel, and instead can just check whole tiles.
+ *
+ *   If you want to disable the use of tiles, set the tile_size to 1, which
+ *   will make the shadow atlas use pixels instead of tiles.
+ *
+ * @param size Atlas-size in pixels
+ * @param tile_size tile-size in pixels, or 1 to use no tiles.
+ */
+ShadowAtlas::ShadowAtlas(size_t size, size_t tile_size) {
+  nassertv(size > 1 && tile_size >= 1);
+  nassertv(tile_size < size && size % tile_size == 0);
+  _size = size;
+  _tile_size = tile_size;
+  _num_used_tiles = 0;
+  init_tiles();
+}
+
+/**
+ * @brief Destructs the shadow atlas.
+ * @details This destructs the shadow atlas, freeing all used resources.
+ */
+ShadowAtlas::~ShadowAtlas() {
+  delete [] _flags;
+}
+
+/**
+ * @brief Internal method to init the storage.
+ * @details This method setups the storage used for storing the tile flags.
+ */
+void ShadowAtlas::init_tiles() {
+  _num_tiles = _size / _tile_size;
+  _flags = new bool[_num_tiles * _num_tiles];
+  memset(_flags, 0x0, sizeof(bool) * _num_tiles * _num_tiles);
+}
+
+/**
+ * @brief Internal method to reserve a region in the atlas.
+ * @details This reserves a given region in the shadow atlas. The region should
+ *   be in tile space.This is called by the ShadowAtlas::find_and_reserve_region.
+ *   It sets all flags in that region to true, indicating that those are used.
+ *   When an invalid region is passed, an assertion is triggered. If assertions
+ *   are optimized out, undefined behaviour occurs.
+ *
+ * @param x x- start positition of the region
+ * @param y y- start position of the region
+ * @param w width of the region
+ * @param h height of the region
+ */
+void ShadowAtlas::reserve_region(size_t x, size_t y, size_t w, size_t h) {
+  // Check if we are out of bounds, this should be disabled for performance
+  // reasons at some point.
+  nassertv(x >= 0 && y >= 0 && x + w <= _num_tiles && y + h <= _num_tiles);
+
+  _num_used_tiles += w * h;
+
+  // Iterate over every tile in the region and mark it as used
+  for (size_t cx = 0; cx < w; ++cx) {
+    for (size_t cy = 0; cy < h; ++cy) {
+      set_tile(cx + x, cy + y, true);
+    }
+  }
+}
+
+/**
+ * @brief Finds space for a map of the given size in the atlas.
+ * @details This methods searches for a space to store a region of the given
+ *   size in the atlas. tile_width and tile_height should be already in tile
+ *   space. They can be converted using ShadowAtlas::get_required_tiles.
+ *
+ *   If no region is found, or an invalid size is passed, an integer vector with
+ *   all components set to -1 is returned.
+ *
+ *  If a region is found, an integer vector with the given layout is returned:
+ *   x: x- Start of the region
+ *   y: y- Start of the region
+ *   z: width of the region
+ *   w: height of the region
+ *
+ *   The layout is in tile space, and can get converted to uv space using
+ *   ShadowAtlas::region_to_uv.
+ *
+ * @param tile_width Width of the region in tile space
+ * @param tile_height Height of the region in tile space
+ *
+ * @return Region, see description, or -1 when no region is found.
+ */
+LVecBase4i ShadowAtlas::find_and_reserve_region(size_t tile_width, size_t tile_height) {
+
+  // Check for empty region
+  if (tile_width < 1 || tile_height < 1) {
+    shadowatlas_cat.error() << "Called find_and_reserve_region with null-region!" << endl;
+    return LVecBase4i(-1);
+  }
+
+  // Check for region bigger than the shadow atlas
+  if (tile_width > _num_tiles || tile_height > _num_tiles) {
+    shadowatlas_cat.error() << "Requested region exceeds shadow atlas size!" << endl;
+    return LVecBase4i(-1);
+  }
+
+  // Iterate over every possible region and check if its still free
+  for (size_t x = 0; x <= _num_tiles - tile_width; ++x) {
+    for (size_t y = 0; y <= _num_tiles - tile_height; ++y) {
+      if (region_is_free(x, y, tile_width, tile_height)) {
+        // Found free region, now reserve it
+        reserve_region(x, y, tile_width, tile_height);
+        return LVecBase4i(x, y, tile_width, tile_height);
+      }
+    }
+  }
+
+  // When we reached this part, we couldn't find a free region, so the atlas
+  // seems to be full.
+  shadowatlas_cat.error() << "Failed to find a free region of size " << tile_width
+              << " x " << tile_height << "!"  << endl;
+  return LVecBase4i(-1);
+}
+
+/**
+ * @brief Frees a given region
+ * @details This frees a given region, marking it as free so that other shadow
+ *   maps can use the space again. The region should be the same as returned
+ *   by ShadowAtlas::find_and_reserve_region.
+ *
+ *   If an invalid region is passed, an assertion is triggered. If assertions
+ *   are compiled out, undefined behaviour will occur.
+ *
+ * @param region Region to free
+ */
+void ShadowAtlas::free_region(const LVecBase4i& region) {
+  // Out of bounds check, can't hurt
+  nassertv(region.get_x() >= 0 && region.get_y() >= 0);
+  nassertv(region.get_x() + region.get_z() <= _num_tiles && region.get_y() + region.get_w() <= _num_tiles);
+
+  _num_used_tiles -= region.get_z() * region.get_w();
+
+  for (size_t x = 0; x < region.get_z(); ++x) {
+    for (size_t y = 0; y < region.get_w(); ++y) {
+      // Could do an assert here, that the tile should have been used (=true) before
+      set_tile(region.get_x() + x, region.get_y() + y, false);
+    }
+  }
+}

+ 80 - 0
contrib/src/rplight/shadowAtlas.h

@@ -0,0 +1,80 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef SHADOWATLAS_H
+#define SHADOWATLAS_H
+
+#include "pandabase.h"
+#include "lvecBase4.h"
+
+NotifyCategoryDecl(shadowatlas, EXPORT_CLASS, EXPORT_TEMPL);
+
+
+/**
+ * @brief Class which manages distributing shadow maps in an atlas.
+ * @details This class manages the shadow atlas. It handles finding and reserving
+ *   space for new shadow maps.
+ */
+class ShadowAtlas {
+PUBLISHED:
+  ShadowAtlas(size_t size, size_t tile_size = 32);
+  ~ShadowAtlas();
+
+  inline int get_num_used_tiles() const;
+  inline float get_coverage() const;
+
+  MAKE_PROPERTY(num_used_tiles, get_num_used_tiles);
+  MAKE_PROPERTY(coverage, get_coverage);
+
+public:
+
+  LVecBase4i find_and_reserve_region(size_t tile_width, size_t tile_height);
+  void free_region(const LVecBase4i& region);
+  inline LVecBase4 region_to_uv(const LVecBase4i& region);
+
+  inline int get_tile_size() const;
+  inline int get_required_tiles(size_t resolution) const;
+
+protected:
+
+  void init_tiles();
+
+  inline void set_tile(size_t x, size_t y, bool flag);
+  inline bool get_tile(size_t x, size_t y) const;
+
+  inline bool region_is_free(size_t x, size_t y, size_t w, size_t h) const;
+  void reserve_region(size_t x, size_t y, size_t w, size_t h);
+
+  size_t _size;
+  size_t _num_tiles;
+  size_t _tile_size;
+  size_t _num_used_tiles;
+  bool* _flags;
+};
+
+#include "shadowAtlas.I"
+
+#endif // SHADOWATLAS_H

+ 192 - 0
contrib/src/rplight/shadowManager.I

@@ -0,0 +1,192 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+/**
+ * @brief Sets the maximum amount of updates per frame.
+ * @details This controls the maximum amount of updated ShadowSources per frame.
+ *   The ShadowManager will take the first <max_updates> ShadowSources, and
+ *   generate shadow maps for them every frame. If there are more ShadowSources
+ *   waiting to get updated than available updates, the sources are sorted by
+ *   priority, and the update of the less important sources is delayed to the
+ *   next frame.
+ *
+ *   If the update count is set too low, and there are a lot of ShadowSources
+ *   waiting to get updated, artifacts will occur, and there might be ShadowSources
+ *   which never get updated, due to low priority.
+ *
+ *   If an update count of 0 is passed, no updates will happen. This also means
+ *   that there are no shadows. This is not recommended.
+ *
+ *   If an update count < 0 is passed, undefined behaviour occurs.
+ *
+ *   This method has to get called before ShadowManager::init, otherwise an
+ *   assertion will get triggered.
+ *
+ * @param max_updates Maximum amoumt of updates
+ */
+inline void ShadowManager::set_max_updates(size_t max_updates) {
+  nassertv(max_updates >= 0);
+  nassertv(_atlas == nullptr);  // ShadowManager was already initialized
+  if (max_updates == 0) {
+    shadowmanager_cat.warning() << "max_updates set to 0, no shadows will be updated." << endl;
+  }
+  _max_updates = max_updates;
+}
+
+/**
+ * @brief Sets the shadow atlas size
+ * @details This sets the desired shadow atlas size. It should be big enough
+ *   to store all important shadow sources, with some buffer, because the shadow
+ *   maps usually won't be fitted perfectly, so gaps can occur.
+ *
+ *   This has to get called before calling ShadowManager::init. When calling this
+ *   method after initialization, an assertion will get triggered.
+ *
+ * @param atlas_size Size of the shadow atlas in pixels
+ */
+inline void ShadowManager::set_atlas_size(size_t atlas_size) {
+  nassertv(atlas_size >= 16 && atlas_size <= 16384);
+  nassertv(_atlas == nullptr);  // ShadowManager was already initialized
+  _atlas_size = atlas_size;
+}
+
+/**
+ * @brief Returns the shadow atlas size.
+ * @details This returns the shadow atlas size previously set with
+ *   ShadowManager::set_atlas_size.
+ * @return Shadow atlas size in pixels
+ */
+inline size_t ShadowManager::get_atlas_size() const {
+  return _atlas_size;
+}
+
+
+/**
+ * @brief Returns a handle to the shadow atlas.
+ * @details This returns a handle to the internal shadow atlas instance. This
+ *   is only valid after calling ShadowManager::init. Calling this earlier will
+ *   trigger an assertion and undefined behaviour.
+ * @return The internal ShadowAtlas instance
+ */
+inline ShadowAtlas* ShadowManager::get_atlas() const {
+  nassertr(_atlas != nullptr, nullptr); // Can't hurt to check
+  return _atlas;
+}
+
+/**
+ * @brief Sets the target scene
+ * @details This sets the target scene for rendering shadows. All shadow cameras
+ *   will be parented to this scene to render shadows.
+ *
+ *   Usually the scene will be ShowBase.render. If the scene is an empty or
+ *   invalid NodePath, an assertion will be triggered.
+ *
+ *   This method has to get called before calling ShadowManager::init, or an
+ *   assertion will get triggered.
+ *
+ * @param scene_parent The target scene
+ */
+inline void ShadowManager::set_scene(NodePath scene_parent) {
+  nassertv(!scene_parent.is_empty());
+  nassertv(_atlas == nullptr);  // ShadowManager was already initialized
+  _scene_parent = scene_parent;
+}
+
+/**
+ * @brief Sets the handle to the TagStageManager.
+ * @details This sets the handle to the TagStateManager used by the pipeline.
+ *   Usually this is RenderPipeline.get_tag_mgr().
+ *
+ *   This has to get called before ShadowManager::init, otherwise an assertion
+ *   will get triggered.
+ *
+ * @param tag_mgr [description]
+ */
+inline void ShadowManager::set_tag_state_manager(TagStateManager* tag_mgr) {
+  nassertv(tag_mgr != nullptr);
+  nassertv(_atlas == nullptr);  // ShadowManager was already initialized
+  _tag_state_mgr = tag_mgr;
+}
+
+/**
+ * @brief Sets the handle to the Shadow targets output
+ * @details This sets the handle to the GraphicsOutput of the shadow atlas.
+ *   Usually this is RenderTarget.get_internal_buffer(), whereas the RenderTarget
+ *   is the target of the ShadowStage.
+ *
+ *   This is used for creating display regions and attaching cameras to them,
+ *   for performing shadow updates.
+ *
+ *   This has to get called before ShadowManager::init, otherwise an assertion
+ *   will be triggered.
+ *
+ * @param graphics_output [description]
+ */
+inline void ShadowManager::set_atlas_graphics_output(GraphicsOutput* graphics_output) {
+  nassertv(graphics_output != nullptr);
+  nassertv(_atlas == nullptr);  // ShadowManager was already initialized
+  _atlas_graphics_output = graphics_output;
+}
+
+
+/**
+ * @brief Adds a new shadow update
+ * @details This adds a new update to the update queue. When the queue is already
+ *   full, this method returns false, otherwise it returns true. The next time
+ *   the manager is updated, the shadow source will recieve an update of its
+ *   shadow map.
+ *
+ * @param source The shadow source to update
+ *
+ * @return Whether the shadow source udpate was sucessfully queued.
+ */
+inline bool ShadowManager::add_update(const ShadowSource* source) {
+  nassertr(_atlas != nullptr, false); // ShadowManager::init not called yet.
+  nassertr(source != nullptr, false); // nullptr-Pointer passed
+
+  if (_queued_updates.size() >= _max_updates) {
+    if (shadowmanager_cat.is_debug()) {
+      shadowmanager_cat.debug() << "cannot update source, out of update slots" << endl;
+    }
+    return false;
+  }
+
+  // Add the update to the queue
+  _queued_updates.push_back(source);
+  return true;
+}
+
+/**
+ * @brief Returns how many update slots are left.
+ * @details This returns how many update slots are left. You can assume the
+ *   next n calls to add_update will succeed, whereas n is the value returned
+ *   by this function.
+ * @return Number of update slots left.
+ */
+inline size_t ShadowManager::get_num_update_slots_left() const {
+  return _max_updates - _queued_updates.size();
+}

+ 157 - 0
contrib/src/rplight/shadowManager.cxx

@@ -0,0 +1,157 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "shadowManager.h"
+
+NotifyCategoryDef(shadowmanager, "");
+
+/**
+ * @brief Constructs a new shadow atlas
+ * @details This constructs a new shadow atlas. There are a set of properties
+ *   which should be set before calling ShadowManager::init, see the set-Methods.
+ *   After all properties are set, ShadowManager::init should get called.
+ *   ShadowManager::update should get called on a per frame basis.
+ */
+ShadowManager::ShadowManager() {
+  _max_updates = 10;
+  _atlas = nullptr;
+  _atlas_size = 4096;
+  _tag_state_mgr = nullptr;
+  _atlas_graphics_output = nullptr;
+}
+
+/**
+ * @brief Destructs the ShadowManager
+ * @details This destructs the shadow manager, clearing all resources used
+ */
+ShadowManager::~ShadowManager() {
+  delete _atlas;
+
+  // Todo: Could eventually unregister all shadow cameras. Since the tag state
+  // manager does this on cleanup already, and we get destructed at the same
+  // time (if at all), this is not really necessary
+}
+
+
+/**
+ * @brief Initializes the ShadowManager.
+ * @details This initializes the ShadowManager. All properties should have
+ *   been set before calling this, otherwise assertions will get triggered.
+ *
+ *   This setups everything required for rendering shadows, including the
+ *   shadow atlas and the various shadow cameras. After calling this method,
+ *   no properties can be changed anymore.
+ */
+void ShadowManager::init() {
+  nassertv(!_scene_parent.is_empty());    // Scene parent not set, call set_scene_parent before init!
+  nassertv(_tag_state_mgr != nullptr);     // TagStateManager not set, call set_tag_state_mgr before init!
+  nassertv(_atlas_graphics_output != nullptr); // AtlasGraphicsOutput not set, call set_atlas_graphics_output before init!
+
+  _cameras.resize(_max_updates);
+  _display_regions.resize(_max_updates);
+  _camera_nps.reserve(_max_updates);
+
+  // Create the cameras and regions
+  for(size_t i = 0; i < _max_updates; ++i) {
+
+    // Create the camera
+    PT(Camera) camera = new Camera("ShadowCam-" + format_string(i));
+    camera->set_lens(new MatrixLens());
+    camera->set_active(false);
+    camera->set_scene(_scene_parent);
+    _tag_state_mgr->register_camera("shadow", camera);
+    _camera_nps.push_back(_scene_parent.attach_new_node(camera));
+    _cameras[i] = camera;
+
+    // Create the display region
+    PT(DisplayRegion) region = _atlas_graphics_output->make_display_region();
+    region->set_sort(1000);
+    region->set_clear_depth_active(true);
+    region->set_clear_depth(1.0);
+    region->set_clear_color_active(false);
+    region->set_camera(_camera_nps[i]);
+    region->set_active(false);
+    _display_regions[i] = region;
+  }
+
+  // Create the atlas
+  _atlas = new ShadowAtlas(_atlas_size);
+
+  // Reserve enough space for the updates
+  _queued_updates.reserve(_max_updates);
+}
+
+
+/**
+ * @brief Updates the ShadowManager
+ * @details This updates the ShadowManager, processing all shadow sources which
+ *   need to get updated.
+ *
+ *   This first collects all sources which require an update, sorts them by priority,
+ *   and then processes the first <max_updates> ShadowSources.
+ *
+ *   This may not get called before ShadowManager::init, or an assertion will be
+ *   thrown.
+ */
+void ShadowManager::update() {
+  nassertv(_atlas != nullptr);             // ShadowManager::init not called yet
+  nassertv(_queued_updates.size() <= _max_updates); // Internal error, should not happen
+
+  // Disable all cameras and regions which will not be used
+  for (size_t i = _queued_updates.size(); i < _max_updates; ++i) {
+    _cameras[i]->set_active(false);
+    _display_regions[i]->set_active(false);
+  }
+
+  // Iterate over all queued updates
+  for (size_t i = 0; i < _queued_updates.size(); ++i) {
+    const ShadowSource* source = _queued_updates[i];
+
+    // Enable the camera and display region, so they perform a render
+    _cameras[i]->set_active(true);
+    _display_regions[i]->set_active(true);
+
+    // Set the view projection matrix
+    DCAST(MatrixLens, _cameras[i]->get_lens())->set_user_mat(source->get_mvp());
+
+    // Optional: Show the camera frustum for debugging
+    // _cameras[i]->show_frustum();
+
+    // Set the correct dimensions on the display region
+    const LVecBase4& uv = source->get_uv_region();
+    _display_regions[i]->set_dimensions(
+      uv.get_x(),        // left
+      uv.get_x() + uv.get_z(), // right
+      uv.get_y(),        // bottom
+      uv.get_y() + uv.get_w()  // top
+    );
+  }
+
+  // Clear the update list
+  _queued_updates.clear();
+  _queued_updates.reserve(_max_updates);
+}

+ 91 - 0
contrib/src/rplight/shadowManager.h

@@ -0,0 +1,91 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef SHADOWMANAGER_H
+#define SHADOWMANAGER_H
+
+#include "pandabase.h"
+#include "camera.h"
+#include "luse.h"
+#include "matrixLens.h"
+#include "referenceCount.h"
+#include "nodePath.h"
+#include "displayRegion.h"
+#include "graphicsOutput.h"
+
+#include "tagStateManager.h"
+#include "shadowSource.h"
+#include "shadowAtlas.h"
+
+NotifyCategoryDecl(shadowmanager, EXPORT_CLASS, EXPORT_TEMPL);
+
+
+class ShadowManager : public ReferenceCount {
+PUBLISHED:
+  ShadowManager();
+  ~ShadowManager();
+
+  inline void set_max_updates(size_t max_updates);
+  inline void set_scene(NodePath scene_parent);
+  inline void set_tag_state_manager(TagStateManager* tag_mgr);
+  inline void set_atlas_graphics_output(GraphicsOutput* graphics_output);
+
+  inline void set_atlas_size(size_t atlas_size);
+  inline size_t get_atlas_size() const;
+  MAKE_PROPERTY(atlas_size, get_atlas_size, set_atlas_size);
+
+  inline size_t get_num_update_slots_left() const;
+  MAKE_PROPERTY(num_update_slots_left, get_num_update_slots_left);
+
+  inline ShadowAtlas* get_atlas() const;
+  MAKE_PROPERTY(atlas, get_atlas);
+
+  void init();
+  void update();
+
+public:
+  inline bool add_update(const ShadowSource* source);
+
+private:
+  size_t _max_updates;
+  size_t _atlas_size;
+  NodePath _scene_parent;
+
+  pvector<PT(Camera)> _cameras;
+  pvector<NodePath> _camera_nps;
+  pvector<PT(DisplayRegion)> _display_regions;
+
+  ShadowAtlas* _atlas;
+  TagStateManager* _tag_state_mgr;
+  GraphicsOutput* _atlas_graphics_output;
+
+  typedef pvector<const ShadowSource*> UpdateQueue;
+  UpdateQueue _queued_updates;
+};
+
+#include "shadowManager.I"
+
+#endif // SHADOWMANAGER_H

+ 262 - 0
contrib/src/rplight/shadowSource.I

@@ -0,0 +1,262 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+
+/**
+ * @brief Returns whether the shadow source needs an update.
+ * @details This returns the update flag, which was previously set with
+ *   ShadowSource::set_needs_update. If the value is true, it means that the
+ *   ShadowSource is invalid and should be regenerated. This can either be the
+ *   case because the scene changed and affected the shadow source, or the light
+ *   moved.
+ * @return Update-Flag
+ */
+inline bool ShadowSource::get_needs_update() const {
+  return !has_region() || _needs_update;
+}
+
+/**
+ * @brief Returns the slot of the shadow source.
+ * @details This returns the assigned slot of the ShadowSource, or -1 if no slot
+ *   was assigned yet. You can check if a slot exists with ShadowSource::has_slot.
+ *   The slot is the index of the ShadowSource in the global source buffer.
+ * @return Slot, or -1 to indicate no slot.
+ */
+inline int ShadowSource::get_slot() const {
+  return _slot;
+}
+
+/**
+ * @brief Returns whether the source has a slot.
+ * @details This returns whether the ShadowSource currently has an assigned slot.
+ *   If the source has a slot assigned, this returns true, otherwise false. Cases
+ *   where the source has no slot might be when the source just got attached, but
+ *   never got rendered yet.
+ * @return [description]
+ */
+inline bool ShadowSource::has_slot() const {
+  return _slot >= 0;
+}
+
+/**
+ * @brief Assigns the source a slot
+ * @details This assigns a slot to the ShadowSource. This is called from the
+ *   ShadowManager, when the source gets attached first time. This should not
+ *   get called by the user.
+ *
+ * @param slot Slot of the source, or -1 to indicate no slot.
+ */
+inline void ShadowSource::set_slot(int slot) {
+  _slot = slot;
+}
+
+/**
+ * @brief Setups a perspective lens for the source.
+ * @details This makes the shadow source behave like a perspective lens. The
+ *   parameters are similar to the ones of a PerspectiveLens.
+ *
+ * @param fov FoV of the lens
+ * @param near_plane The near plane of the lens, to avoid artifacts at low distance
+ * @param far_plane The far plane of the lens
+ * @param pos Position of the lens, in world space
+ * @param direction Direction (Orientation) of the lens
+ */
+inline void ShadowSource::
+set_perspective_lens(PN_stdfloat fov, PN_stdfloat near_plane,
+                     PN_stdfloat far_plane, LVecBase3 pos,
+                     LVecBase3 direction) {
+  // Construct the transfo*rmation matrix
+  LMatrix4 transform_mat = LMatrix4::translate_mat(-pos);
+
+  // Construct a temporary lens to generate the lens matrix
+  PerspectiveLens temp_lens = PerspectiveLens(fov, fov);
+  temp_lens.set_film_offset(0, 0);
+  temp_lens.set_near_far(near_plane, far_plane);
+  temp_lens.set_view_vector(direction, LVector3::up());
+  set_matrix_lens(transform_mat * temp_lens.get_projection_mat());
+
+  // Set new bounds, approximate with sphere
+  CPT(BoundingHexahedron) hexahedron = DCAST(BoundingHexahedron, temp_lens.make_bounds());
+  LPoint3 center = (hexahedron->get_min() + hexahedron->get_max()) * 0.5f;
+  _bounds = BoundingSphere(pos + center, (hexahedron->get_max() - center).length());
+}
+
+/**
+ * @brief Sets a custom matrix for the source.
+ * @details This tells the source to use a custom matrix for rendering, just like
+ *   the matrix lens. The matrix should include all transformations, rotations and
+ *   scales. No other matrices will be used for rendering this shadow source (not
+ *   even a coordinate system conversion).
+ *
+ * @param mvp Custom View-Projection matrix
+ */
+inline void ShadowSource::set_matrix_lens(const LMatrix4& mvp) {
+  _mvp = mvp;
+  set_needs_update(true);
+}
+
+/**
+ * @brief Sets the update flag of the source.
+ * @details Sets whether the source is still valid, or needs to get regenerated.
+ *   Usually you only want to flag the shadow source as invalid, by passing
+ *   true as the flag. However, the ShadowManager will set the flag to false
+ *   after updating the source.
+ *
+ * @param flag The update flag
+ */
+inline void ShadowSource::set_needs_update(bool flag) {
+  _needs_update = flag;
+}
+
+/**
+ * @brief Returns whether the source has a valid region.
+ * @details This returns whether the ShadowSource has a valid shadow atlas region
+ *   assigned. This might be not the case when the source never was rendered yet,
+ *   or is about to get updated.
+ * @return true if the source has a valid region, else false.
+ */
+inline bool ShadowSource::has_region() const {
+  return _region.get_x() >= 0 && _region.get_y() >= 0 && _region.get_z() >= 0 && _region.get_w() >= 0;
+}
+
+/**
+ * @brief Returns the resolution of the source.
+ * @details Returns the shadow map resolution of source, in pixels. This is the
+ *   space the source takes in the shadow atlas, in pixels.
+ * @return Resolution in pixels
+ */
+inline size_t ShadowSource::get_resolution() const {
+  return _resolution;
+}
+
+/**
+ * @brief Returns the assigned region of the source in atlas space.
+ * @details This returns the region of the source, in atlas space. This is the
+ *  region set by ShadowSource::set_region. If no region was set yet, returns
+ *  a 4-component integer vector with all components set to -1. To check this,
+ *  you should call ShadowSource::has_region() first.
+ *
+ * @return [description]
+ */
+inline const LVecBase4i& ShadowSource::get_region() const {
+  return _region;
+}
+
+/**
+ * @brief Returns the assigned region of the source in UV space.
+ * @details This returns the region of the source, in UV space. This is the
+ *  region set by ShadowSource::set_region. If no region was set yet, returns
+ *  a 4-component integer vector with all components set to -1. To check this,
+ *  you should call ShadowSource::has_region() first.
+ *
+ * @return [description]
+ */
+inline const LVecBase4& ShadowSource::get_uv_region() const {
+  return _region_uv;
+}
+
+/**
+ * @brief Sets the assigned region of the source in atlas and uv space.
+ * @details This sets the assigned region of the ShadowSource. The region in
+ *   atlas space should be the region returned from the
+ *   ShadowAtlas::find_and_reserve_region. The uv-region should be the same region,
+ *   but in the 0 .. 1 range (can be converted with ShadowAtlas::region_to_uv).
+ *   This is required for the shaders, because they expect coordinates in the
+ *   0 .. 1 range for sampling.
+ *
+ * @param region Atlas-Space region
+ * @param region_uv UV-Space region
+ */
+inline void ShadowSource::set_region(const LVecBase4i& region, const LVecBase4& region_uv) {
+  _region = region;
+  _region_uv = region_uv;
+}
+
+/**
+ * @brief Returns the View-Projection matrix of the source.
+ * @details This returns the current view-projection matrix of the ShadowSource.
+ *   If no matrix was set yet, returns a matrix with all components zero.
+ *   If a matrix was set with ShadowSource::set_matrix_lens, returns the matrix
+ *   set by that function call.
+ *
+ *   If a matrix was set with ShadowSource::set_perspective_lens, returns a
+ *   perspective view-projection matrix setup by those parameters.
+ *
+ *   The matrix returned is the matrix used for rendering the shadow map, and
+ *   includes the camera transform as well as the projection matrix.
+ *
+ * @return View-Projection matrix.
+ */
+inline const LMatrix4& ShadowSource::get_mvp() const {
+  return _mvp;
+}
+
+/**
+ * @brief Writes the source to a GPUCommand.
+ * @details This writes the ShadowSource to a GPUCommand. This stores the
+ *   mvp and the uv-region in the command.
+ *
+ * @param cmd GPUCommand to write to.
+ */
+inline void ShadowSource::write_to_command(GPUCommand &cmd) const {
+  // When storing on the gpu, we should already have a valid slot
+  nassertv(_slot >= 0);
+  cmd.push_mat4(_mvp);
+  cmd.push_vec4(_region_uv);
+}
+
+/**
+ * @brief Sets the resolution of the source.
+ * @details This sets the resolution of the ShadowSource, in pixels. It should be
+ *   a multiple of the tile size of the ShadowAtlas, and greater than zero.
+ *
+ * @param resolution [description]
+ */
+inline void ShadowSource::set_resolution(size_t resolution) {
+  nassertv(resolution > 0);
+  _resolution = resolution;
+  set_needs_update(true);
+}
+
+/**
+ * @brief Returns the shadow sources bounds
+ * @details This returns the bounds of the shadow source, approximated as a sphere
+ * @return Bounds as a BoundingSphere
+ */
+inline const BoundingSphere& ShadowSource::get_bounds() const {
+  return _bounds;
+}
+
+/**
+ * @brief Clears the assigned region of the source
+ * @details This unassigns any shadow atlas region from the source, previously
+ *   set with set_region
+ */
+inline void ShadowSource::clear_region() {
+  _region.fill(-1);
+  _region_uv.fill(0);
+}

+ 41 - 0
contrib/src/rplight/shadowSource.cxx

@@ -0,0 +1,41 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "shadowSource.h"
+
+/**
+ * @brief Constructs a new shadow source
+ * @details This constructs a new shadow source, with no projection setup,
+ *   and no slot assigned.
+ */
+ShadowSource::ShadowSource() {
+  _slot = -1;
+  _needs_update = true;
+  _resolution = 512;
+  _mvp.fill(0.0);
+  _region.fill(-1);
+  _region_uv.fill(0);
+}

+ 93 - 0
contrib/src/rplight/shadowSource.h

@@ -0,0 +1,93 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#ifndef SHADOWSOURCE_H
+#define SHADOWSOURCE_H
+
+#include "pandabase.h"
+#include "luse.h"
+#include "transformState.h"
+#include "look_at.h"
+#include "compose_matrix.h"
+#include "perspectiveLens.h"
+#include "boundingVolume.h"
+#include "boundingSphere.h"
+#include "boundingHexahedron.h"
+#include "geometricBoundingVolume.h"
+
+#include "gpuCommand.h"
+
+/**
+ * @brief This class represents a single shadow source.
+ * @details The ShadowSource can be seen as a Camera. It is used by the Lights
+ *   to render their shadows. Each ShadowSource has a position in the atlas,
+ *   and a view-projection matrix. The shadow manager regenerates the shadow maps
+ *   using the data from the shadow sources.
+ */
+class ShadowSource {
+public:
+  ShadowSource();
+
+  inline void write_to_command(GPUCommand &cmd) const;
+
+  inline void set_needs_update(bool flag);
+  inline void set_slot(int slot);
+  inline void set_region(const LVecBase4i& region, const LVecBase4& region_uv);
+  inline void set_resolution(size_t resolution);
+  inline void set_perspective_lens(PN_stdfloat fov, PN_stdfloat near_plane,
+                                   PN_stdfloat far_plane, LVecBase3 pos,
+                                   LVecBase3 direction);
+  inline void set_matrix_lens(const LMatrix4& mvp);
+
+  inline bool has_region() const;
+  inline bool has_slot() const;
+
+  inline void clear_region();
+
+  inline int get_slot() const;
+  inline bool get_needs_update() const;
+  inline size_t get_resolution() const;
+  inline const LMatrix4& get_mvp() const;
+  inline const LVecBase4i& get_region() const;
+  inline const LVecBase4& get_uv_region() const;
+
+  inline const BoundingSphere& get_bounds() const;
+
+private:
+  int _slot;
+  bool _needs_update;
+  size_t _resolution;
+  LMatrix4 _mvp;
+  LVecBase4i _region;
+  LVecBase4 _region_uv;
+
+  BoundingSphere _bounds;
+};
+
+#include "shadowSource.I"
+
+#endif // SHADOWSOURCE_H

+ 93 - 0
contrib/src/rplight/tagStateManager.I

@@ -0,0 +1,93 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+/**
+ * @brief Registers a new camera which renders a certain pass
+ * @details This registers a new camera which will be used to render the given
+ *   pass. The TagStateManager will keep track of the camera and
+ *   applies all registered states onto the camera with Camera::set_tag_state.
+ *   It also applies the appropriate camera mask to the camera,
+ *   and sets an initial state to disable color write depending on the pass.
+ *
+ * @param source Camera which will be used to render shadows
+ */
+inline void TagStateManager::
+register_camera(const string& name, Camera* source) {
+  ContainerList::iterator entry = _containers.find(name);
+  nassertv(entry != _containers.end());
+  register_camera(entry->second, source);
+}
+
+/**
+ * @brief Unregisters a camera from the list of shadow cameras
+ * @details This unregisters a camera from the list of shadows cameras. It also
+ *   resets all tag states of the camera, and also its initial state.
+ *
+ * @param source Camera to unregister
+ */
+inline void TagStateManager::
+unregister_camera(const string& name, Camera* source) {
+  ContainerList::iterator entry = _containers.find(name);
+  nassertv(entry != _containers.end());
+  unregister_camera(entry->second, source);
+}
+
+/**
+ * @brief Applies a given state for a pass to a NodePath
+ * @details This applies a shader to the given NodePath which is used when the
+ *   NodePath is rendered by any registered camera for that pass.
+ *   It also disables color write depending on the pass.
+ *
+ * @param np The nodepath to apply the shader to
+ * @param shader A handle to the shader to apply
+ * @param name Name of the state, should be a unique identifier
+ * @param sort Determines the sort with which the shader will be applied.
+ */
+inline void TagStateManager::
+apply_state(const string& state, NodePath np, Shader* shader,
+            const string &name, int sort) {
+  ContainerList::iterator entry = _containers.find(state);
+  nassertv(entry != _containers.end());
+  apply_state(entry->second, np, shader, name, sort);
+}
+
+/**
+ * @brief Returns the render mask for the given state
+ * @details This returns the mask of a given render pass, which can be used
+ *   to either show or hide objects from this pass.
+ *
+ * @param container_name Name of the render-pass
+ * @return Bit mask of the render pass
+ */
+inline BitMask32 TagStateManager::
+get_mask(const string &container_name) {
+  if (container_name == "gbuffer") {
+    return BitMask32::bit(1);
+  }
+  ContainerList::iterator entry = _containers.find(container_name);
+  nassertr(entry != _containers.end(), BitMask32());
+  return entry->second.mask;
+}

+ 202 - 0
contrib/src/rplight/tagStateManager.cxx

@@ -0,0 +1,202 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+
+#include "tagStateManager.h"
+
+
+NotifyCategoryDef(tagstatemgr, "");
+
+/**
+ * @brief Constructs a new TagStateManager
+ * @details This constructs a new TagStateManager. The #main_cam_node should
+ *   refer to the main scene camera, and will most likely be base.cam.
+ *   It is necessary to pass the camera because the C++ code does not have
+ *   access to the showbase.
+ *
+ * @param main_cam_node The main scene camera
+ */
+TagStateManager::
+TagStateManager(NodePath main_cam_node) {
+  nassertv(!main_cam_node.is_empty());
+  nassertv(DCAST(Camera, main_cam_node.node()) != nullptr);
+  _main_cam_node = main_cam_node;
+
+  // Set default camera mask
+  DCAST(Camera, _main_cam_node.node())->set_camera_mask(BitMask32::bit(1));
+
+  // Init containers
+  _containers["shadow"]   = StateContainer("Shadows",  2, false);
+  _containers["voxelize"] = StateContainer("Voxelize", 3, false);
+  _containers["envmap"]   = StateContainer("Envmap",   4, true);
+  _containers["forward"]  = StateContainer("Forward",  5, true);
+}
+
+/**
+ * @brief Destructs the TagStateManager
+ * @details This destructs the TagStateManager, and cleans up all resources used.
+ */
+TagStateManager::
+~TagStateManager() {
+  cleanup_states();
+}
+
+/**
+ * @brief Applies a given state to a NodePath
+ * @details This applies a shader to the given NodePath which is used when the
+ *   NodePath is rendered by any registered camera of the container.
+ *
+ * @param container The container which is used to store the state
+ * @param np The nodepath to apply the shader to
+ * @param shader A handle to the shader to apply
+ * @param name Name of the state, should be a unique identifier
+ * @param sort Changes the sort with which the shader will be applied.
+ */
+void TagStateManager::
+apply_state(StateContainer& container, NodePath np, Shader* shader,
+            const string &name, int sort) {
+  if (tagstatemgr_cat.is_spam()) {
+    tagstatemgr_cat.spam() << "Constructing new state " << name
+                 << " with shader " << shader << endl;
+  }
+
+  // Construct the render state
+  CPT(RenderState) state = RenderState::make_empty();
+
+  // Disable color write for all stages except the environment container
+  if (!container.write_color) {
+    state = state->set_attrib(ColorWriteAttrib::make(ColorWriteAttrib::C_off), 10000);
+  }
+  state = state->set_attrib(ShaderAttrib::make(shader, sort), sort);
+
+  // Emit a warning if we override an existing state
+  if (container.tag_states.count(name) != 0) {
+    tagstatemgr_cat.warning() << "Overriding existing state " << name << endl;
+  }
+
+  // Store the state, this is required whenever we attach a new camera, so
+  // it can also track the existing states
+  container.tag_states[name] = state;
+
+  // Save the tag on the node path
+  np.set_tag(container.tag_name, name);
+
+  // Apply the state on all cameras which are attached so far
+  for (size_t i = 0; i < container.cameras.size(); ++i) {
+    container.cameras[i]->set_tag_state(name, state);
+  }
+}
+
+/**
+ * @brief Cleans up all registered states.
+ * @details This cleans up all states which were registered to the TagStateManager.
+ *   It also calls Camera::clear_tag_states() on the main_cam_node and all attached
+ *   cameras.
+ */
+void TagStateManager::
+cleanup_states() {
+  if (tagstatemgr_cat.is_info()) {
+    tagstatemgr_cat.info() << "cleaning up states" << endl;
+  }
+
+  // Clear all tag states of the main camera
+  DCAST(Camera, _main_cam_node.node())->clear_tag_states();
+
+  // Clear the containers
+  // XXX: Just iterate over the _container map
+  cleanup_container_states(_containers["shadow"]);
+  cleanup_container_states(_containers["voxelize"]);
+  cleanup_container_states(_containers["envmap"]);
+  cleanup_container_states(_containers["forward"]);
+}
+
+/**
+ * @brief Cleans up the states of a given container
+ * @details This cleans all tag states of the given container,
+ *   and also calls Camera::clear_tag_states on every assigned camera.
+ *
+ * @param container Container to clear
+ */
+void TagStateManager::
+cleanup_container_states(StateContainer& container) {
+  for (size_t i = 0; i < container.cameras.size(); ++i) {
+    container.cameras[i]->clear_tag_states();
+  }
+  container.tag_states.clear();
+}
+
+/**
+ * @brief Registers a new camera to a given container
+ * @details This registers a new camera to a container, and sets its initial
+ *   state as well as the camera mask.
+ *
+ * @param container The container to add the camera to
+ * @param source The camera to add
+ */
+void TagStateManager::
+register_camera(StateContainer& container, Camera* source) {
+  source->set_tag_state_key(container.tag_name);
+  source->set_camera_mask(container.mask);
+
+  // Construct an initial state which also disables color write, additionally
+  // to the ColorWriteAttrib on each unique state.
+  CPT(RenderState) state = RenderState::make_empty();
+
+  if (!container.write_color) {
+    state = state->set_attrib(ColorWriteAttrib::make(ColorWriteAttrib::C_off), 10000);
+  }
+  source->set_initial_state(state);
+
+  // Store the camera so we can keep track of it
+  container.cameras.push_back(source);
+}
+
+/**
+ * @brief Unregisters a camera from a container
+ * @details This unregisters a camera from the list of cameras of a given
+ *   container. It also resets all tag states of the camera, and also its initial
+ *   state.
+ *
+ * @param source Camera to unregister
+ */
+void TagStateManager::
+unregister_camera(StateContainer& container, Camera* source) {
+  CameraList& cameras = container.cameras;
+
+  // Make sure the camera was attached so far
+  if (std::find(cameras.begin(), cameras.end(), source) == cameras.end()) {
+    tagstatemgr_cat.error()
+      << "Called unregister_camera but camera was never registered!" << endl;
+    return;
+  }
+
+  // Remove the camera from the list of attached cameras
+  cameras.erase(std::remove(cameras.begin(), cameras.end(), source), cameras.end());
+
+  // Reset the camera
+  source->clear_tag_states();
+  source->set_initial_state(RenderState::make_empty());
+}

+ 93 - 0
contrib/src/rplight/tagStateManager.h

@@ -0,0 +1,93 @@
+/**
+ *
+ * RenderPipeline
+ *
+ * Copyright (c) 2014-2016 tobspr <[email protected]>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef TAGSTATEMANAGER_H
+#define TAGSTATEMANAGER_H
+
+#include "pandabase.h"
+#include "bitMask.h"
+#include "camera.h"
+#include "nodePath.h"
+#include "shader.h"
+#include "renderState.h"
+#include "shaderAttrib.h"
+#include "colorWriteAttrib.h"
+
+NotifyCategoryDecl(tagstatemgr, EXPORT_CLASS, EXPORT_TEMPL);
+
+/**
+ * @brief This class handles all different tag states
+ * @details The TagStateManager stores a list of RenderStates assigned to different
+ *   steps in the pipeline. For example, there are a list of shadow states, which
+ *   are applied whenever objects are rendered from a shadow camera.
+ *
+ *   The Manager also stores a list of all cameras used in the different stages,
+ *   to keep track of the states used and to be able to attach new states.
+ */
+class TagStateManager {
+PUBLISHED:
+  TagStateManager(NodePath main_cam_node);
+  ~TagStateManager();
+
+  inline void apply_state(const string& state, NodePath np, Shader* shader, const string &name, int sort);
+  void cleanup_states();
+
+  inline void register_camera(const string& state, Camera* source);
+  inline void unregister_camera(const string& state, Camera* source);
+  inline BitMask32 get_mask(const string &container_name);
+
+private:
+  typedef vector<Camera*> CameraList;
+  typedef pmap<string, CPT(RenderState)> TagStateList;
+
+  struct StateContainer {
+    CameraList cameras;
+    TagStateList tag_states;
+    string tag_name;
+    BitMask32 mask;
+    bool write_color;
+
+    StateContainer() {};
+    StateContainer(const string &tag_name, size_t mask, bool write_color)
+      : tag_name(tag_name), mask(BitMask32::bit(mask)), write_color(write_color) {};
+  };
+
+  void apply_state(StateContainer& container, NodePath np, Shader* shader,
+                   const string& name, int sort);
+  void cleanup_container_states(StateContainer& container);
+  void register_camera(StateContainer &container, Camera* source);
+  void unregister_camera(StateContainer &container, Camera* source);
+
+  typedef pmap<string, StateContainer> ContainerList;
+  ContainerList _containers;
+
+  NodePath _main_cam_node;
+};
+
+
+#include "tagStateManager.I"
+
+#endif // TAGSTATEMANAGER_H

+ 5 - 0
direct/src/gui/DirectEntry.py

@@ -59,6 +59,8 @@ class DirectEntry(DirectFrame):
             # Text used for the PGEntry text node
             # Text used for the PGEntry text node
             # NOTE: This overrides the DirectFrame text option
             # NOTE: This overrides the DirectFrame text option
             ('initialText',     '',               DGG.INITOPT),
             ('initialText',     '',               DGG.INITOPT),
+            # Enable or disable text overflow scrolling
+            ('overflow',        0,                self.setOverflowMode),
             # Command to be called on hitting Enter
             # Command to be called on hitting Enter
             ('command',        None,              None),
             ('command',        None,              None),
             ('extraArgs',      [],                None),
             ('extraArgs',      [],                None),
@@ -159,6 +161,9 @@ class DirectEntry(DirectFrame):
     def setCursorKeysActive(self):
     def setCursorKeysActive(self):
         PGEntry.setCursorKeysActive(self.guiItem, self['cursorKeys'])
         PGEntry.setCursorKeysActive(self.guiItem, self['cursorKeys'])
 
 
+    def setOverflowMode(self):
+        PGEntry.set_overflow_mode(self.guiItem, self['overflow'])
+
     def setObscureMode(self):
     def setObscureMode(self):
         PGEntry.setObscureMode(self.guiItem, self['obscured'])
         PGEntry.setObscureMode(self.guiItem, self['obscured'])
 
 

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

@@ -51,6 +51,8 @@
 #elif defined(__ANDROID__)
 #elif defined(__ANDROID__)
 #if defined(__ARM_ARCH_7A__)
 #if defined(__ARM_ARCH_7A__)
 #define DTOOL_PLATFORM "android_armv7a"
 #define DTOOL_PLATFORM "android_armv7a"
+#elif defined(__aarch64__)
+#define DTOOL_PLATFORM "android_aarch64"
 #elif defined(__arm__)
 #elif defined(__arm__)
 #define DTOOL_PLATFORM "android_arm"
 #define DTOOL_PLATFORM "android_arm"
 #elif defined(__mips__)
 #elif defined(__mips__)

+ 43 - 11
makepanda/makepanda.py

@@ -865,7 +865,7 @@ if (COMPILER=="GCC"):
 
 
         if not PkgSkip("PYTHON"):
         if not PkgSkip("PYTHON"):
             python_lib = SDK["PYTHONVERSION"]
             python_lib = SDK["PYTHONVERSION"]
-            if not RTDIST:
+            if not RTDIST and GetTarget() != 'android':
                 # We don't link anything in the SDK with libpython.
                 # We don't link anything in the SDK with libpython.
                 python_lib = ""
                 python_lib = ""
             SmartPkgEnable("PYTHON", "", python_lib, (SDK["PYTHONVERSION"], SDK["PYTHONVERSION"] + "/Python.h"))
             SmartPkgEnable("PYTHON", "", python_lib, (SDK["PYTHONVERSION"], SDK["PYTHONVERSION"] + "/Python.h"))
@@ -1260,8 +1260,11 @@ def CompileCxx(obj,src,opts):
         if GetTarget() == "android":
         if GetTarget() == "android":
             # Most of the specific optimization flags here were
             # Most of the specific optimization flags here were
             # just copied from the default Android Makefiles.
             # just copied from the default Android Makefiles.
-            cmd += ' -I%s/include' % (SDK["ANDROID_STL"])
-            cmd += ' -I%s/libs/%s/include' % (SDK["ANDROID_STL"], SDK["ANDROID_ABI"])
+            if "ANDROID_API" in SDK:
+                cmd += ' -D__ANDROID_API__=' + str(SDK["ANDROID_API"])
+            if "ANDROID_STL" in SDK:
+                cmd += ' -I%s/include' % (SDK["ANDROID_STL"])
+                cmd += ' -I%s/libs/%s/include' % (SDK["ANDROID_STL"], SDK["ANDROID_ABI"])
             cmd += ' -ffunction-sections -funwind-tables'
             cmd += ' -ffunction-sections -funwind-tables'
             if arch == 'armv7a':
             if arch == 'armv7a':
                 cmd += ' -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__'
                 cmd += ' -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__'
@@ -1311,7 +1314,7 @@ def CompileCxx(obj,src,opts):
                 if optlevel >= 4 or GetTarget() == "android":
                 if optlevel >= 4 or GetTarget() == "android":
                     cmd += " -fno-rtti"
                     cmd += " -fno-rtti"
 
 
-        if ('SSE2' in opts or not PkgSkip("SSE2")) and not arch.startswith("arm"):
+        if ('SSE2' in opts or not PkgSkip("SSE2")) and not arch.startswith("arm") and arch != 'aarch64':
             cmd += " -msse2"
             cmd += " -msse2"
 
 
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
         # Needed by both Python, Panda, Eigen, all of which break aliasing rules.
@@ -1438,12 +1441,19 @@ def CompileIgate(woutd,wsrc,opts):
         cmd += ' -D_MSC_VER=1600 -D"__declspec(param)=" -D__cdecl -D_near -D_far -D__near -D__far -D__stdcall'
         cmd += ' -D_MSC_VER=1600 -D"__declspec(param)=" -D__cdecl -D_near -D_far -D__near -D__far -D__stdcall'
     if (COMPILER=="GCC"):
     if (COMPILER=="GCC"):
         cmd += ' -D__attribute__\(x\)='
         cmd += ' -D__attribute__\(x\)='
-        if GetTargetArch() in ("x86_64", "amd64"):
+        target_arch = GetTargetArch()
+        if target_arch in ("x86_64", "amd64"):
             cmd += ' -D_LP64'
             cmd += ' -D_LP64'
+        elif target_arch == 'aarch64':
+            cmd += ' -D_LP64 -D__LP64__ -D__aarch64__'
         else:
         else:
             cmd += ' -D__i386__'
             cmd += ' -D__i386__'
-        if GetTarget() == 'darwin':
+
+        target = GetTarget()
+        if target == 'darwin':
             cmd += ' -D__APPLE__'
             cmd += ' -D__APPLE__'
+        elif target == 'android':
+            cmd += ' -D__ANDROID__'
 
 
     optlevel = GetOptimizeOption(opts)
     optlevel = GetOptimizeOption(opts)
     if (optlevel==1): cmd += ' -D_DEBUG'
     if (optlevel==1): cmd += ' -D_DEBUG'
@@ -1745,8 +1755,8 @@ def CompileLink(dll, obj, opts):
         if LDFLAGS != "":
         if LDFLAGS != "":
             cmd += " " + LDFLAGS
             cmd += " " + LDFLAGS
 
 
-        # Don't link libraries with Python.
-        if "PYTHON" in opts and GetOrigExt(dll) != ".exe" and not RTDIST:
+        # Don't link libraries with Python, except on Android.
+        if "PYTHON" in opts and GetOrigExt(dll) != ".exe" and not RTDIST and GetTarget() != 'android':
             opts = opts[:]
             opts = opts[:]
             opts.remove("PYTHON")
             opts.remove("PYTHON")
 
 
@@ -5051,8 +5061,7 @@ if (not RTDIST and not RUNTIME and PkgSkip("PVIEW")==0 and GetTarget() != 'andro
 #
 #
 
 
 if (not RUNTIME and GetTarget() == 'android'):
 if (not RUNTIME and GetTarget() == 'android'):
-  native_app_glue = os.path.join(SDK['ANDROID_NDK'], 'sources', 'android', 'native_app_glue')
-  OPTS=['DIR:panda/src/android', 'DIR:' + native_app_glue]
+  OPTS=['DIR:panda/src/android']
 
 
   TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
   TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
   TargetAdd('libp3android.dll', input='p3android_composite1.obj')
   TargetAdd('libp3android.dll', input='p3android_composite1.obj')
@@ -5080,7 +5089,7 @@ if (not RUNTIME and GetTarget() == 'android'):
 
 
 if (GetTarget() == 'android' and PkgSkip("EGL")==0 and PkgSkip("GLES")==0 and not RUNTIME):
 if (GetTarget() == 'android' and PkgSkip("EGL")==0 and PkgSkip("GLES")==0 and not RUNTIME):
   DefSymbol('GLES', 'OPENGLES_1', '')
   DefSymbol('GLES', 'OPENGLES_1', '')
-  OPTS=['DIR:panda/src/androiddisplay', 'DIR:panda/src/glstuff', 'DIR:' + native_app_glue, 'BUILDING:PANDAGLES',  'GLES', 'EGL']
+  OPTS=['DIR:panda/src/androiddisplay', 'DIR:panda/src/glstuff', 'BUILDING:PANDAGLES',  'GLES', 'EGL']
   TargetAdd('pandagles_androiddisplay_composite1.obj', opts=OPTS, input='p3androiddisplay_composite1.cxx')
   TargetAdd('pandagles_androiddisplay_composite1.obj', opts=OPTS, input='p3androiddisplay_composite1.cxx')
   OPTS=['DIR:panda/metalibs/pandagles', 'BUILDING:PANDAGLES', 'GLES', 'EGL']
   OPTS=['DIR:panda/metalibs/pandagles', 'BUILDING:PANDAGLES', 'GLES', 'EGL']
   TargetAdd('pandagles_pandagles.obj', opts=OPTS, input='pandagles.cxx')
   TargetAdd('pandagles_pandagles.obj', opts=OPTS, input='pandagles.cxx')
@@ -6500,6 +6509,29 @@ if (PkgSkip("CONTRIB")==0 and not RUNTIME):
   TargetAdd('ai.pyd', input=COMMON_PANDA_LIBS)
   TargetAdd('ai.pyd', input=COMMON_PANDA_LIBS)
   TargetAdd('ai.pyd', opts=['PYTHON'])
   TargetAdd('ai.pyd', opts=['PYTHON'])
 
 
+#
+# DIRECTORY: contrib/src/rplight/
+#
+if not PkgSkip("CONTRIB") and not PkgSkip("PYTHON") and not RUNTIME:
+  OPTS=['DIR:contrib/src/rplight', 'BUILDING:RPLIGHT', 'PYTHON']
+  TargetAdd('p3rplight_composite1.obj', opts=OPTS, input='p3rplight_composite1.cxx')
+
+  IGATEFILES=GetDirectoryContents('contrib/src/rplight', ["*.h", "*_composite*.cxx"])
+  TargetAdd('libp3rplight.in', opts=OPTS, input=IGATEFILES)
+  TargetAdd('libp3rplight.in', opts=['IMOD:panda3d._rplight', 'ILIB:libp3rplight', 'SRCDIR:contrib/src/rplight'])
+  TargetAdd('libp3rplight_igate.obj', input='libp3rplight.in', opts=["DEPENDENCYONLY"])
+
+  TargetAdd('rplight_module.obj', input='libp3rplight.in')
+  TargetAdd('rplight_module.obj', opts=OPTS)
+  TargetAdd('rplight_module.obj', opts=['IMOD:panda3d._rplight', 'ILIB:_rplight', 'IMPORT:panda3d.core'])
+
+  TargetAdd('_rplight.pyd', input='rplight_module.obj')
+  TargetAdd('_rplight.pyd', input='libp3rplight_igate.obj')
+  TargetAdd('_rplight.pyd', input='p3rplight_composite1.obj')
+  TargetAdd('_rplight.pyd', input='libp3interrogatedb.dll')
+  TargetAdd('_rplight.pyd', input=COMMON_PANDA_LIBS)
+  TargetAdd('_rplight.pyd', opts=['PYTHON'])
+
 #
 #
 # Generate the models directory and samples directory
 # Generate the models directory and samples directory
 #
 #

+ 60 - 17
makepanda/makepandacore.py

@@ -37,6 +37,7 @@ TARGET_ARCH = None
 HAS_TARGET_ARCH = False
 HAS_TARGET_ARCH = False
 TOOLCHAIN_PREFIX = ""
 TOOLCHAIN_PREFIX = ""
 ANDROID_ABI = None
 ANDROID_ABI = None
+ANDROID_API = 14
 SYS_LIB_DIRS = []
 SYS_LIB_DIRS = []
 SYS_INC_DIRS = []
 SYS_INC_DIRS = []
 DEBUG_DEPENDENCIES = False
 DEBUG_DEPENDENCIES = False
@@ -290,7 +291,13 @@ def GetHost():
     elif sys.platform == 'darwin':
     elif sys.platform == 'darwin':
         return 'darwin'
         return 'darwin'
     elif sys.platform.startswith('linux'):
     elif sys.platform.startswith('linux'):
-        return 'linux'
+        try:
+            # Python seems to offer no built-in way to check this.
+            osname = subprocess.check_output(["uname", "-o"])
+            if osname.strip().lower() == b'android':
+                return 'android'
+        except:
+            return 'linux'
     elif sys.platform.startswith('freebsd'):
     elif sys.platform.startswith('freebsd'):
         return 'freebsd'
         return 'freebsd'
     else:
     else:
@@ -344,9 +351,19 @@ def SetTarget(target, arch=None):
             if arch not in choices:
             if arch not in choices:
                 exit('Mac OS X architecture must be one of %s' % (', '.join(choices)))
                 exit('Mac OS X architecture must be one of %s' % (', '.join(choices)))
 
 
-    elif target == 'android':
+    elif target == 'android' or target.startswith('android-'):
         if arch is None:
         if arch is None:
-            arch = 'arm'
+            # If compiling on Android, default to same architecture.  Otherwise, arm.
+            if host == 'android':
+                arch = host_arch
+            else:
+                arch = 'arm'
+
+        # Did we specify an API level?
+        target, _, api = target.partition('-')
+        if api:
+            global ANDROID_API
+            ANDROID_API = int(api)
 
 
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         # Determine the prefix for our gcc tools, eg. arm-linux-androideabi-gcc
         global ANDROID_ABI
         global ANDROID_ABI
@@ -356,14 +373,23 @@ def SetTarget(target, arch=None):
         elif arch == 'arm':
         elif arch == 'arm':
             ANDROID_ABI = 'armeabi'
             ANDROID_ABI = 'armeabi'
             TOOLCHAIN_PREFIX = 'arm-linux-androideabi-'
             TOOLCHAIN_PREFIX = 'arm-linux-androideabi-'
-        elif arch == 'x86':
-            ANDROID_ABI = 'x86'
-            TOOLCHAIN_PREFIX = 'i686-linux-android-'
+        elif arch == 'aarch64':
+            ANDROID_ABI = 'arm64-v8a'
+            TOOLCHAIN_PREFIX = 'aarch64-linux-android-'
         elif arch == 'mips':
         elif arch == 'mips':
             ANDROID_ABI = 'mips'
             ANDROID_ABI = 'mips'
             TOOLCHAIN_PREFIX = 'mipsel-linux-android-'
             TOOLCHAIN_PREFIX = 'mipsel-linux-android-'
+        elif arch == 'mips64':
+            ANDROID_ABI = 'mips64'
+            TOOLCHAIN_PREFIX = 'mips64el-linux-android-'
+        elif arch == 'x86':
+            ANDROID_ABI = 'x86'
+            TOOLCHAIN_PREFIX = 'i686-linux-android-'
+        elif arch == 'x86_64':
+            ANDROID_ABI = 'x86_64'
+            TOOLCHAIN_PREFIX = 'x86_64-linux-android-'
         else:
         else:
-            exit('Android architecture must be arm, armv7a, x86 or mips')
+            exit('Android architecture must be arm, armv7a, aarch64, mips, mips64, x86 or x86_64')
 
 
     elif target == 'linux':
     elif target == 'linux':
         if arch is not None:
         if arch is not None:
@@ -413,13 +439,13 @@ def CrossCompiling():
     return GetTarget() != GetHost()
     return GetTarget() != GetHost()
 
 
 def GetCC():
 def GetCC():
-    if TARGET == 'darwin' or TARGET == 'freebsd':
+    if TARGET in ('darwin', 'freebsd', 'android'):
         return os.environ.get('CC', TOOLCHAIN_PREFIX + 'clang')
         return os.environ.get('CC', TOOLCHAIN_PREFIX + 'clang')
     else:
     else:
         return os.environ.get('CC', TOOLCHAIN_PREFIX + 'gcc')
         return os.environ.get('CC', TOOLCHAIN_PREFIX + 'gcc')
 
 
 def GetCXX():
 def GetCXX():
-    if TARGET == 'darwin' or TARGET == 'freebsd':
+    if TARGET in ('darwin', 'freebsd', 'android'):
         return os.environ.get('CXX', TOOLCHAIN_PREFIX + 'clang++')
         return os.environ.get('CXX', TOOLCHAIN_PREFIX + 'clang++')
     else:
     else:
         return os.environ.get('CXX', TOOLCHAIN_PREFIX + 'g++')
         return os.environ.get('CXX', TOOLCHAIN_PREFIX + 'g++')
@@ -2339,6 +2365,16 @@ def SdkLocateAndroid():
     if GetTarget() != 'android':
     if GetTarget() != 'android':
         return
         return
 
 
+    # Allow ANDROID_API/ANDROID_ABI to be used in makepanda.py.
+    api = ANDROID_API
+    SDK["ANDROID_API"] = api
+
+    abi = ANDROID_ABI
+    SDK["ANDROID_ABI"] = abi
+
+    if GetHost() == 'android':
+        return
+
     # Determine the NDK installation directory.
     # Determine the NDK installation directory.
     if 'NDK_ROOT' not in os.environ:
     if 'NDK_ROOT' not in os.environ:
         exit('NDK_ROOT must be set when compiling for Android!')
         exit('NDK_ROOT must be set when compiling for Android!')
@@ -2355,18 +2391,20 @@ def SdkLocateAndroid():
     if arch == 'armv7a' or arch == 'arm':
     if arch == 'armv7a' or arch == 'arm':
         arch = 'arm'
         arch = 'arm'
         toolchain = 'arm-linux-androideabi-' + gcc_ver
         toolchain = 'arm-linux-androideabi-' + gcc_ver
-    elif arch == 'x86':
-        toolchain = 'x86-' + gcc_ver
+    elif arch == 'aarch64':
+        toolchain = 'aarch64-linux-android-' + gcc_ver
     elif arch == 'mips':
     elif arch == 'mips':
         toolchain = 'mipsel-linux-android-' + gcc_ver
         toolchain = 'mipsel-linux-android-' + gcc_ver
+    elif arch == 'mips64':
+        toolchain = 'mips64el-linux-android-' + gcc_ver
+    elif arch == 'x86':
+        toolchain = 'x86-' + gcc_ver
+    elif arch == 'x86_64':
+        toolchain = 'x86_64-' + gcc_ver
     SDK["ANDROID_TOOLCHAIN"] = os.path.join(ndk_root, 'toolchains', toolchain)
     SDK["ANDROID_TOOLCHAIN"] = os.path.join(ndk_root, 'toolchains', toolchain)
 
 
-    # Allow ANDROID_ABI to be used in makepanda.py.
-    abi = ANDROID_ABI
-    SDK["ANDROID_ABI"] = abi
-
     # Determine the sysroot directory.
     # Determine the sysroot directory.
-    SDK["SYSROOT"] = os.path.join(ndk_root, 'platforms', 'android-9', 'arch-%s' % (arch))
+    SDK["SYSROOT"] = os.path.join(ndk_root, 'platforms', 'android-%s' % (api), 'arch-%s' % (arch))
     #IncDirectory("ALWAYS", os.path.join(SDK["SYSROOT"], 'usr', 'include'))
     #IncDirectory("ALWAYS", os.path.join(SDK["SYSROOT"], 'usr', 'include'))
 
 
     stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'gnu-libstdc++', gcc_ver)
     stdlibc = os.path.join(ndk_root, 'sources', 'cxx-stl', 'gnu-libstdc++', gcc_ver)
@@ -2626,7 +2664,12 @@ def SetupBuildEnvironment(compiler):
         print("Using compiler: %s" % compiler)
         print("Using compiler: %s" % compiler)
         print("Host OS: %s" % GetHost())
         print("Host OS: %s" % GetHost())
         print("Host arch: %s" % GetHostArch())
         print("Host arch: %s" % GetHostArch())
+
+    target = GetTarget()
+    if target != 'android':
         print("Target OS: %s" % GetTarget())
         print("Target OS: %s" % GetTarget())
+    else:
+        print("Target OS: %s (API level %d)" % (GetTarget(), ANDROID_API))
     print("Target arch: %s" % GetTargetArch())
     print("Target arch: %s" % GetTargetArch())
 
 
     # Set to English so we can safely parse the result of gcc commands.
     # Set to English so we can safely parse the result of gcc commands.
@@ -2732,7 +2775,7 @@ def SetupBuildEnvironment(compiler):
                 print("  " + dir)
                 print("  " + dir)
 
 
     # In the case of Android, we have to put the toolchain on the PATH in order to use it.
     # In the case of Android, we have to put the toolchain on the PATH in order to use it.
-    if GetTarget() == 'android':
+    if GetTarget() == 'android' and GetHost() != 'android':
         # Locate the directory where the toolchain binaries reside.
         # Locate the directory where the toolchain binaries reside.
         prebuilt_dir = os.path.join(SDK['ANDROID_TOOLCHAIN'], 'prebuilt')
         prebuilt_dir = os.path.join(SDK['ANDROID_TOOLCHAIN'], 'prebuilt')
         if not os.path.isdir(prebuilt_dir):
         if not os.path.isdir(prebuilt_dir):

+ 442 - 0
panda/src/android/android_native_app_glue.c

@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include <jni.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/resource.h>
+
+#include "android_native_app_glue.h"
+#include <android/log.h>
+
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__))
+
+/* For debug builds, always enable the debug traces in this library */
+#ifndef NDEBUG
+#  define LOGV(...)  ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__))
+#else
+#  define LOGV(...)  ((void)0)
+#endif
+
+static void free_saved_state(struct android_app* android_app) {
+    pthread_mutex_lock(&android_app->mutex);
+    if (android_app->savedState != NULL) {
+        free(android_app->savedState);
+        android_app->savedState = NULL;
+        android_app->savedStateSize = 0;
+    }
+    pthread_mutex_unlock(&android_app->mutex);
+}
+
+int8_t android_app_read_cmd(struct android_app* android_app) {
+    int8_t cmd;
+    if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) {
+        switch (cmd) {
+            case APP_CMD_SAVE_STATE:
+                free_saved_state(android_app);
+                break;
+        }
+        return cmd;
+    } else {
+        LOGE("No data on command pipe!");
+    }
+    return -1;
+}
+
+static void print_cur_config(struct android_app* android_app) {
+    char lang[2], country[2];
+    AConfiguration_getLanguage(android_app->config, lang);
+    AConfiguration_getCountry(android_app->config, country);
+
+    LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
+            "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
+            "modetype=%d modenight=%d",
+            AConfiguration_getMcc(android_app->config),
+            AConfiguration_getMnc(android_app->config),
+            lang[0], lang[1], country[0], country[1],
+            AConfiguration_getOrientation(android_app->config),
+            AConfiguration_getTouchscreen(android_app->config),
+            AConfiguration_getDensity(android_app->config),
+            AConfiguration_getKeyboard(android_app->config),
+            AConfiguration_getNavigation(android_app->config),
+            AConfiguration_getKeysHidden(android_app->config),
+            AConfiguration_getNavHidden(android_app->config),
+            AConfiguration_getSdkVersion(android_app->config),
+            AConfiguration_getScreenSize(android_app->config),
+            AConfiguration_getScreenLong(android_app->config),
+            AConfiguration_getUiModeType(android_app->config),
+            AConfiguration_getUiModeNight(android_app->config));
+}
+
+void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
+    switch (cmd) {
+        case APP_CMD_INPUT_CHANGED:
+            LOGV("APP_CMD_INPUT_CHANGED\n");
+            pthread_mutex_lock(&android_app->mutex);
+            if (android_app->inputQueue != NULL) {
+                AInputQueue_detachLooper(android_app->inputQueue);
+            }
+            android_app->inputQueue = android_app->pendingInputQueue;
+            if (android_app->inputQueue != NULL) {
+                LOGV("Attaching input queue to looper");
+                AInputQueue_attachLooper(android_app->inputQueue,
+                        android_app->looper, LOOPER_ID_INPUT, NULL,
+                        &android_app->inputPollSource);
+            }
+            pthread_cond_broadcast(&android_app->cond);
+            pthread_mutex_unlock(&android_app->mutex);
+            break;
+
+        case APP_CMD_INIT_WINDOW:
+            LOGV("APP_CMD_INIT_WINDOW\n");
+            pthread_mutex_lock(&android_app->mutex);
+            android_app->window = android_app->pendingWindow;
+            pthread_cond_broadcast(&android_app->cond);
+            pthread_mutex_unlock(&android_app->mutex);
+            break;
+
+        case APP_CMD_TERM_WINDOW:
+            LOGV("APP_CMD_TERM_WINDOW\n");
+            pthread_cond_broadcast(&android_app->cond);
+            break;
+
+        case APP_CMD_RESUME:
+        case APP_CMD_START:
+        case APP_CMD_PAUSE:
+        case APP_CMD_STOP:
+            LOGV("activityState=%d\n", cmd);
+            pthread_mutex_lock(&android_app->mutex);
+            android_app->activityState = cmd;
+            pthread_cond_broadcast(&android_app->cond);
+            pthread_mutex_unlock(&android_app->mutex);
+            break;
+
+        case APP_CMD_CONFIG_CHANGED:
+            LOGV("APP_CMD_CONFIG_CHANGED\n");
+            AConfiguration_fromAssetManager(android_app->config,
+                    android_app->activity->assetManager);
+            print_cur_config(android_app);
+            break;
+
+        case APP_CMD_DESTROY:
+            LOGV("APP_CMD_DESTROY\n");
+            android_app->destroyRequested = 1;
+            break;
+    }
+}
+
+void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
+    switch (cmd) {
+        case APP_CMD_TERM_WINDOW:
+            LOGV("APP_CMD_TERM_WINDOW\n");
+            pthread_mutex_lock(&android_app->mutex);
+            android_app->window = NULL;
+            pthread_cond_broadcast(&android_app->cond);
+            pthread_mutex_unlock(&android_app->mutex);
+            break;
+
+        case APP_CMD_SAVE_STATE:
+            LOGV("APP_CMD_SAVE_STATE\n");
+            pthread_mutex_lock(&android_app->mutex);
+            android_app->stateSaved = 1;
+            pthread_cond_broadcast(&android_app->cond);
+            pthread_mutex_unlock(&android_app->mutex);
+            break;
+
+        case APP_CMD_RESUME:
+            free_saved_state(android_app);
+            break;
+    }
+}
+
+void app_dummy() {
+
+}
+
+static void android_app_destroy(struct android_app* android_app) {
+    LOGV("android_app_destroy!");
+    free_saved_state(android_app);
+    pthread_mutex_lock(&android_app->mutex);
+    if (android_app->inputQueue != NULL) {
+        AInputQueue_detachLooper(android_app->inputQueue);
+    }
+    AConfiguration_delete(android_app->config);
+    android_app->destroyed = 1;
+    pthread_cond_broadcast(&android_app->cond);
+    pthread_mutex_unlock(&android_app->mutex);
+    // Can't touch android_app object after this.
+}
+
+static void process_input(struct android_app* app, struct android_poll_source* source) {
+    AInputEvent* event = NULL;
+    while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
+        LOGV("New input event: type=%d\n", AInputEvent_getType(event));
+        if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
+            continue;
+        }
+        int32_t handled = 0;
+        if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
+        AInputQueue_finishEvent(app->inputQueue, event, handled);
+    }
+}
+
+static void process_cmd(struct android_app* app, struct android_poll_source* source) {
+    int8_t cmd = android_app_read_cmd(app);
+    android_app_pre_exec_cmd(app, cmd);
+    if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
+    android_app_post_exec_cmd(app, cmd);
+}
+
+static void* android_app_entry(void* param) {
+    struct android_app* android_app = (struct android_app*)param;
+
+    android_app->config = AConfiguration_new();
+    AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager);
+
+    print_cur_config(android_app);
+
+    android_app->cmdPollSource.id = LOOPER_ID_MAIN;
+    android_app->cmdPollSource.app = android_app;
+    android_app->cmdPollSource.process = process_cmd;
+    android_app->inputPollSource.id = LOOPER_ID_INPUT;
+    android_app->inputPollSource.app = android_app;
+    android_app->inputPollSource.process = process_input;
+
+    ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
+    ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
+            &android_app->cmdPollSource);
+    android_app->looper = looper;
+
+    pthread_mutex_lock(&android_app->mutex);
+    android_app->running = 1;
+    pthread_cond_broadcast(&android_app->cond);
+    pthread_mutex_unlock(&android_app->mutex);
+
+    android_main(android_app);
+
+    android_app_destroy(android_app);
+    return NULL;
+}
+
+// --------------------------------------------------------------------
+// Native activity interaction (called from main thread)
+// --------------------------------------------------------------------
+
+static struct android_app* android_app_create(ANativeActivity* activity,
+        void* savedState, size_t savedStateSize) {
+    struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
+    memset(android_app, 0, sizeof(struct android_app));
+    android_app->activity = activity;
+
+    pthread_mutex_init(&android_app->mutex, NULL);
+    pthread_cond_init(&android_app->cond, NULL);
+
+    if (savedState != NULL) {
+        android_app->savedState = malloc(savedStateSize);
+        android_app->savedStateSize = savedStateSize;
+        memcpy(android_app->savedState, savedState, savedStateSize);
+    }
+
+    int msgpipe[2];
+    if (pipe(msgpipe)) {
+        LOGE("could not create pipe: %s", strerror(errno));
+        return NULL;
+    }
+    android_app->msgread = msgpipe[0];
+    android_app->msgwrite = msgpipe[1];
+
+    pthread_attr_t attr; 
+    pthread_attr_init(&attr);
+    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+    pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
+
+    // Wait for thread to start.
+    pthread_mutex_lock(&android_app->mutex);
+    while (!android_app->running) {
+        pthread_cond_wait(&android_app->cond, &android_app->mutex);
+    }
+    pthread_mutex_unlock(&android_app->mutex);
+
+    return android_app;
+}
+
+static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
+    if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
+        LOGE("Failure writing android_app cmd: %s\n", strerror(errno));
+    }
+}
+
+static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) {
+    pthread_mutex_lock(&android_app->mutex);
+    android_app->pendingInputQueue = inputQueue;
+    android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED);
+    while (android_app->inputQueue != android_app->pendingInputQueue) {
+        pthread_cond_wait(&android_app->cond, &android_app->mutex);
+    }
+    pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) {
+    pthread_mutex_lock(&android_app->mutex);
+    if (android_app->pendingWindow != NULL) {
+        android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
+    }
+    android_app->pendingWindow = window;
+    if (window != NULL) {
+        android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
+    }
+    while (android_app->window != android_app->pendingWindow) {
+        pthread_cond_wait(&android_app->cond, &android_app->mutex);
+    }
+    pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) {
+    pthread_mutex_lock(&android_app->mutex);
+    android_app_write_cmd(android_app, cmd);
+    while (android_app->activityState != cmd) {
+        pthread_cond_wait(&android_app->cond, &android_app->mutex);
+    }
+    pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_free(struct android_app* android_app) {
+    pthread_mutex_lock(&android_app->mutex);
+    android_app_write_cmd(android_app, APP_CMD_DESTROY);
+    while (!android_app->destroyed) {
+        pthread_cond_wait(&android_app->cond, &android_app->mutex);
+    }
+    pthread_mutex_unlock(&android_app->mutex);
+
+    close(android_app->msgread);
+    close(android_app->msgwrite);
+    pthread_cond_destroy(&android_app->cond);
+    pthread_mutex_destroy(&android_app->mutex);
+    free(android_app);
+}
+
+static void onDestroy(ANativeActivity* activity) {
+    LOGV("Destroy: %p\n", activity);
+    android_app_free((struct android_app*)activity->instance);
+}
+
+static void onStart(ANativeActivity* activity) {
+    LOGV("Start: %p\n", activity);
+    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START);
+}
+
+static void onResume(ANativeActivity* activity) {
+    LOGV("Resume: %p\n", activity);
+    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME);
+}
+
+static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
+    struct android_app* android_app = (struct android_app*)activity->instance;
+    void* savedState = NULL;
+
+    LOGV("SaveInstanceState: %p\n", activity);
+    pthread_mutex_lock(&android_app->mutex);
+    android_app->stateSaved = 0;
+    android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
+    while (!android_app->stateSaved) {
+        pthread_cond_wait(&android_app->cond, &android_app->mutex);
+    }
+
+    if (android_app->savedState != NULL) {
+        savedState = android_app->savedState;
+        *outLen = android_app->savedStateSize;
+        android_app->savedState = NULL;
+        android_app->savedStateSize = 0;
+    }
+
+    pthread_mutex_unlock(&android_app->mutex);
+
+    return savedState;
+}
+
+static void onPause(ANativeActivity* activity) {
+    LOGV("Pause: %p\n", activity);
+    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE);
+}
+
+static void onStop(ANativeActivity* activity) {
+    LOGV("Stop: %p\n", activity);
+    android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP);
+}
+
+static void onConfigurationChanged(ANativeActivity* activity) {
+    struct android_app* android_app = (struct android_app*)activity->instance;
+    LOGV("ConfigurationChanged: %p\n", activity);
+    android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED);
+}
+
+static void onLowMemory(ANativeActivity* activity) {
+    struct android_app* android_app = (struct android_app*)activity->instance;
+    LOGV("LowMemory: %p\n", activity);
+    android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY);
+}
+
+static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
+    LOGV("WindowFocusChanged: %p -- %d\n", activity, focused);
+    android_app_write_cmd((struct android_app*)activity->instance,
+            focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
+}
+
+static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
+    LOGV("NativeWindowCreated: %p -- %p\n", activity, window);
+    android_app_set_window((struct android_app*)activity->instance, window);
+}
+
+static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
+    LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window);
+    android_app_set_window((struct android_app*)activity->instance, NULL);
+}
+
+static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
+    LOGV("InputQueueCreated: %p -- %p\n", activity, queue);
+    android_app_set_input((struct android_app*)activity->instance, queue);
+}
+
+static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
+    LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue);
+    android_app_set_input((struct android_app*)activity->instance, NULL);
+}
+
+JNIEXPORT
+void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState,
+                              size_t savedStateSize) {
+    LOGV("Creating: %p\n", activity);
+    activity->callbacks->onDestroy = onDestroy;
+    activity->callbacks->onStart = onStart;
+    activity->callbacks->onResume = onResume;
+    activity->callbacks->onSaveInstanceState = onSaveInstanceState;
+    activity->callbacks->onPause = onPause;
+    activity->callbacks->onStop = onStop;
+    activity->callbacks->onConfigurationChanged = onConfigurationChanged;
+    activity->callbacks->onLowMemory = onLowMemory;
+    activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
+    activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
+    activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
+    activity->callbacks->onInputQueueCreated = onInputQueueCreated;
+    activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
+
+    activity->instance = android_app_create(activity, savedState, savedStateSize);
+}

+ 354 - 0
panda/src/android/android_native_app_glue.h

@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#ifndef _ANDROID_NATIVE_APP_GLUE_H
+#define _ANDROID_NATIVE_APP_GLUE_H
+
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+
+#include <android/configuration.h>
+#include <android/looper.h>
+#include <android/native_activity.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The native activity interface provided by <android/native_activity.h>
+ * is based on a set of application-provided callbacks that will be called
+ * by the Activity's main thread when certain events occur.
+ *
+ * This means that each one of this callbacks _should_ _not_ block, or they
+ * risk having the system force-close the application. This programming
+ * model is direct, lightweight, but constraining.
+ *
+ * The 'android_native_app_glue' static library is used to provide a different
+ * execution model where the application can implement its own main event
+ * loop in a different thread instead. Here's how it works:
+ *
+ * 1/ The application must provide a function named "android_main()" that
+ *    will be called when the activity is created, in a new thread that is
+ *    distinct from the activity's main thread.
+ *
+ * 2/ android_main() receives a pointer to a valid "android_app" structure
+ *    that contains references to other important objects, e.g. the
+ *    ANativeActivity obejct instance the application is running in.
+ *
+ * 3/ the "android_app" object holds an ALooper instance that already
+ *    listens to two important things:
+ *
+ *      - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX
+ *        declarations below.
+ *
+ *      - input events coming from the AInputQueue attached to the activity.
+ *
+ *    Each of these correspond to an ALooper identifier returned by
+ *    ALooper_pollOnce with values of LOOPER_ID_MAIN and LOOPER_ID_INPUT,
+ *    respectively.
+ *
+ *    Your application can use the same ALooper to listen to additional
+ *    file-descriptors.  They can either be callback based, or with return
+ *    identifiers starting with LOOPER_ID_USER.
+ *
+ * 4/ Whenever you receive a LOOPER_ID_MAIN or LOOPER_ID_INPUT event,
+ *    the returned data will point to an android_poll_source structure.  You
+ *    can call the process() function on it, and fill in android_app->onAppCmd
+ *    and android_app->onInputEvent to be called for your own processing
+ *    of the event.
+ *
+ *    Alternatively, you can call the low-level functions to read and process
+ *    the data directly...  look at the process_cmd() and process_input()
+ *    implementations in the glue to see how to do this.
+ *
+ * See the sample named "native-activity" that comes with the NDK with a
+ * full usage example.  Also look at the JavaDoc of NativeActivity.
+ */
+
+struct android_app;
+
+/**
+ * Data associated with an ALooper fd that will be returned as the "outData"
+ * when that source has data ready.
+ */
+struct android_poll_source {
+    // The identifier of this source.  May be LOOPER_ID_MAIN or
+    // LOOPER_ID_INPUT.
+    int32_t id;
+
+    // The android_app this ident is associated with.
+    struct android_app* app;
+
+    // Function to call to perform the standard processing of data from
+    // this source.
+    void (*process)(struct android_app* app, struct android_poll_source* source);
+};
+
+/**
+ * This is the interface for the standard glue code of a threaded
+ * application.  In this model, the application's code is running
+ * in its own thread separate from the main thread of the process.
+ * It is not required that this thread be associated with the Java
+ * VM, although it will need to be in order to make JNI calls any
+ * Java objects.
+ */
+struct android_app {
+    // The application can place a pointer to its own state object
+    // here if it likes.
+    void* userData;
+
+    // Fill this in with the function to process main app commands (APP_CMD_*)
+    void (*onAppCmd)(struct android_app* app, int32_t cmd);
+
+    // Fill this in with the function to process input events.  At this point
+    // the event has already been pre-dispatched, and it will be finished upon
+    // return.  Return 1 if you have handled the event, 0 for any default
+    // dispatching.
+    int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
+
+    // The ANativeActivity object instance that this app is running in.
+    ANativeActivity* activity;
+
+    // The current configuration the app is running in.
+    AConfiguration* config;
+
+    // This is the last instance's saved state, as provided at creation time.
+    // It is NULL if there was no state.  You can use this as you need; the
+    // memory will remain around until you call android_app_exec_cmd() for
+    // APP_CMD_RESUME, at which point it will be freed and savedState set to NULL.
+    // These variables should only be changed when processing a APP_CMD_SAVE_STATE,
+    // at which point they will be initialized to NULL and you can malloc your
+    // state and place the information here.  In that case the memory will be
+    // freed for you later.
+    void* savedState;
+    size_t savedStateSize;
+
+    // The ALooper associated with the app's thread.
+    ALooper* looper;
+
+    // When non-NULL, this is the input queue from which the app will
+    // receive user input events.
+    AInputQueue* inputQueue;
+
+    // When non-NULL, this is the window surface that the app can draw in.
+    ANativeWindow* window;
+
+    // Current content rectangle of the window; this is the area where the
+    // window's content should be placed to be seen by the user.
+    ARect contentRect;
+
+    // Current state of the app's activity.  May be either APP_CMD_START,
+    // APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below.
+    int activityState;
+
+    // This is non-zero when the application's NativeActivity is being
+    // destroyed and waiting for the app thread to complete.
+    int destroyRequested;
+
+    // -------------------------------------------------
+    // Below are "private" implementation of the glue code.
+
+    pthread_mutex_t mutex;
+    pthread_cond_t cond;
+
+    int msgread;
+    int msgwrite;
+
+    pthread_t thread;
+
+    struct android_poll_source cmdPollSource;
+    struct android_poll_source inputPollSource;
+
+    int running;
+    int stateSaved;
+    int destroyed;
+    int redrawNeeded;
+    AInputQueue* pendingInputQueue;
+    ANativeWindow* pendingWindow;
+    ARect pendingContentRect;
+};
+
+enum {
+    /**
+     * Looper data ID of commands coming from the app's main thread, which
+     * is returned as an identifier from ALooper_pollOnce().  The data for this
+     * identifier is a pointer to an android_poll_source structure.
+     * These can be retrieved and processed with android_app_read_cmd()
+     * and android_app_exec_cmd().
+     */
+    LOOPER_ID_MAIN = 1,
+
+    /**
+     * Looper data ID of events coming from the AInputQueue of the
+     * application's window, which is returned as an identifier from
+     * ALooper_pollOnce().  The data for this identifier is a pointer to an
+     * android_poll_source structure.  These can be read via the inputQueue
+     * object of android_app.
+     */
+    LOOPER_ID_INPUT = 2,
+
+    /**
+     * Start of user-defined ALooper identifiers.
+     */
+    LOOPER_ID_USER = 3,
+};
+
+enum {
+    /**
+     * Command from main thread: the AInputQueue has changed.  Upon processing
+     * this command, android_app->inputQueue will be updated to the new queue
+     * (or NULL).
+     */
+    APP_CMD_INPUT_CHANGED,
+
+    /**
+     * Command from main thread: a new ANativeWindow is ready for use.  Upon
+     * receiving this command, android_app->window will contain the new window
+     * surface.
+     */
+    APP_CMD_INIT_WINDOW,
+
+    /**
+     * Command from main thread: the existing ANativeWindow needs to be
+     * terminated.  Upon receiving this command, android_app->window still
+     * contains the existing window; after calling android_app_exec_cmd
+     * it will be set to NULL.
+     */
+    APP_CMD_TERM_WINDOW,
+
+    /**
+     * Command from main thread: the current ANativeWindow has been resized.
+     * Please redraw with its new size.
+     */
+    APP_CMD_WINDOW_RESIZED,
+
+    /**
+     * Command from main thread: the system needs that the current ANativeWindow
+     * be redrawn.  You should redraw the window before handing this to
+     * android_app_exec_cmd() in order to avoid transient drawing glitches.
+     */
+    APP_CMD_WINDOW_REDRAW_NEEDED,
+
+    /**
+     * Command from main thread: the content area of the window has changed,
+     * such as from the soft input window being shown or hidden.  You can
+     * find the new content rect in android_app::contentRect.
+     */
+    APP_CMD_CONTENT_RECT_CHANGED,
+
+    /**
+     * Command from main thread: the app's activity window has gained
+     * input focus.
+     */
+    APP_CMD_GAINED_FOCUS,
+
+    /**
+     * Command from main thread: the app's activity window has lost
+     * input focus.
+     */
+    APP_CMD_LOST_FOCUS,
+
+    /**
+     * Command from main thread: the current device configuration has changed.
+     */
+    APP_CMD_CONFIG_CHANGED,
+
+    /**
+     * Command from main thread: the system is running low on memory.
+     * Try to reduce your memory use.
+     */
+    APP_CMD_LOW_MEMORY,
+
+    /**
+     * Command from main thread: the app's activity has been started.
+     */
+    APP_CMD_START,
+
+    /**
+     * Command from main thread: the app's activity has been resumed.
+     */
+    APP_CMD_RESUME,
+
+    /**
+     * Command from main thread: the app should generate a new saved state
+     * for itself, to restore from later if needed.  If you have saved state,
+     * allocate it with malloc and place it in android_app.savedState with
+     * the size in android_app.savedStateSize.  The will be freed for you
+     * later.
+     */
+    APP_CMD_SAVE_STATE,
+
+    /**
+     * Command from main thread: the app's activity has been paused.
+     */
+    APP_CMD_PAUSE,
+
+    /**
+     * Command from main thread: the app's activity has been stopped.
+     */
+    APP_CMD_STOP,
+
+    /**
+     * Command from main thread: the app's activity is being destroyed,
+     * and waiting for the app thread to clean up and exit before proceeding.
+     */
+    APP_CMD_DESTROY,
+};
+
+/**
+ * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
+ * app command message.
+ */
+int8_t android_app_read_cmd(struct android_app* android_app);
+
+/**
+ * Call with the command returned by android_app_read_cmd() to do the
+ * initial pre-processing of the given command.  You can perform your own
+ * actions for the command after calling this function.
+ */
+void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
+
+/**
+ * Call with the command returned by android_app_read_cmd() to do the
+ * final post-processing of the given command.  You must have done your own
+ * actions for the command before calling this function.
+ */
+void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
+
+/**
+ * Dummy function that used to be used to prevent the linker from stripping app
+ * glue code. No longer necessary, since __attribute__((visibility("default")))
+ * does this for us.
+ */
+__attribute__((
+    deprecated("Calls to app_dummy are no longer necessary. See "
+               "https://github.com/android-ndk/ndk/issues/381."))) void
+app_dummy();
+
+/**
+ * This is the function that application code must implement, representing
+ * the main entry to the app.
+ */
+extern void android_main(struct android_app* app);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _ANDROID_NATIVE_APP_GLUE_H */

+ 0 - 1
panda/src/cocoadisplay/cocoaGraphicsPipe.mm

@@ -15,7 +15,6 @@
 #include "cocoaGraphicsBuffer.h"
 #include "cocoaGraphicsBuffer.h"
 #include "cocoaGraphicsWindow.h"
 #include "cocoaGraphicsWindow.h"
 #include "cocoaGraphicsStateGuardian.h"
 #include "cocoaGraphicsStateGuardian.h"
-#include "cocoaPandaApp.h"
 #include "config_cocoadisplay.h"
 #include "config_cocoadisplay.h"
 #include "frameBufferProperties.h"
 #include "frameBufferProperties.h"
 #include "displayInformation.h"
 #include "displayInformation.h"

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

@@ -15,6 +15,7 @@
 #include "cocoaGraphicsStateGuardian.h"
 #include "cocoaGraphicsStateGuardian.h"
 #include "config_cocoadisplay.h"
 #include "config_cocoadisplay.h"
 #include "cocoaGraphicsPipe.h"
 #include "cocoaGraphicsPipe.h"
+#include "cocoaPandaApp.h"
 
 
 #include "graphicsPipe.h"
 #include "graphicsPipe.h"
 #include "keyboardButton.h"
 #include "keyboardButton.h"
@@ -68,6 +69,8 @@ CocoaGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
   // Now that we know for sure we want a window, we can create the Cocoa app.
   // Now that we know for sure we want a window, we can create the Cocoa app.
   // This will cause the application icon to appear and start bouncing.
   // This will cause the application icon to appear and start bouncing.
   if (NSApp == nil) {
   if (NSApp == nil) {
+    [CocoaPandaApp sharedApplication];
+
 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1060
 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 1060
     [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
     [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
 #endif
 #endif

+ 4 - 0
panda/src/collide/collisionVisualizer.I

@@ -31,6 +31,7 @@ SolidInfo() {
  */
  */
 INLINE void CollisionVisualizer::
 INLINE void CollisionVisualizer::
 set_point_scale(PN_stdfloat point_scale) {
 set_point_scale(PN_stdfloat point_scale) {
+  LightMutexHolder holder(_lock);
   _point_scale = point_scale;
   _point_scale = point_scale;
 }
 }
 
 
@@ -39,6 +40,7 @@ set_point_scale(PN_stdfloat point_scale) {
  */
  */
 INLINE PN_stdfloat CollisionVisualizer::
 INLINE PN_stdfloat CollisionVisualizer::
 get_point_scale() const {
 get_point_scale() const {
+  LightMutexHolder holder(_lock);
   return _point_scale;
   return _point_scale;
 }
 }
 
 
@@ -51,6 +53,7 @@ get_point_scale() const {
  */
  */
 INLINE void CollisionVisualizer::
 INLINE void CollisionVisualizer::
 set_normal_scale(PN_stdfloat normal_scale) {
 set_normal_scale(PN_stdfloat normal_scale) {
+  LightMutexHolder holder(_lock);
   _normal_scale = normal_scale;
   _normal_scale = normal_scale;
 }
 }
 
 
@@ -59,6 +62,7 @@ set_normal_scale(PN_stdfloat normal_scale) {
  */
  */
 INLINE PN_stdfloat CollisionVisualizer::
 INLINE PN_stdfloat CollisionVisualizer::
 get_normal_scale() const {
 get_normal_scale() const {
+  LightMutexHolder holder(_lock);
   return _normal_scale;
   return _normal_scale;
 }
 }
 
 

+ 28 - 6
panda/src/collide/collisionVisualizer.cxx

@@ -41,7 +41,7 @@ TypeHandle CollisionVisualizer::_type_handle;
  *
  *
  */
  */
 CollisionVisualizer::
 CollisionVisualizer::
-CollisionVisualizer(const string &name) : PandaNode(name) {
+CollisionVisualizer(const string &name) : PandaNode(name), _lock("CollisionVisualizer") {
   set_cull_callback();
   set_cull_callback();
 
 
   // We always want to render the CollisionVisualizer node itself (even if it
   // We always want to render the CollisionVisualizer node itself (even if it
@@ -51,6 +51,23 @@ CollisionVisualizer(const string &name) : PandaNode(name) {
   _normal_scale = 1.0f;
   _normal_scale = 1.0f;
 }
 }
 
 
+/**
+ * Copy constructor.
+ */
+CollisionVisualizer::
+CollisionVisualizer(const CollisionVisualizer &copy) :
+  PandaNode(copy),
+  _lock("CollisionVisualizer"),
+  _point_scale(copy._point_scale),
+  _normal_scale(copy._normal_scale) {
+
+  set_cull_callback();
+
+  // We always want to render the CollisionVisualizer node itself (even if it
+  // doesn't appear to have any geometry within it).
+  set_internal_bounds(new OmniBoundingVolume());
+}
+
 /**
 /**
  *
  *
  */
  */
@@ -64,6 +81,7 @@ CollisionVisualizer::
  */
  */
 void CollisionVisualizer::
 void CollisionVisualizer::
 clear() {
 clear() {
+  LightMutexHolder holder(_lock);
   _data.clear();
   _data.clear();
 }
 }
 
 
@@ -99,6 +117,8 @@ bool CollisionVisualizer::
 cull_callback(CullTraverser *trav, CullTraverserData &data) {
 cull_callback(CullTraverser *trav, CullTraverserData &data) {
   // Now we go through and actually draw our visualized collision solids.
   // Now we go through and actually draw our visualized collision solids.
 
 
+  LightMutexHolder holder(_lock);
+
   Data::const_iterator di;
   Data::const_iterator di;
   for (di = _data.begin(); di != _data.end(); ++di) {
   for (di = _data.begin(); di != _data.end(); ++di) {
     const TransformState *net_transform = (*di).first;
     const TransformState *net_transform = (*di).first;
@@ -257,6 +277,7 @@ output(ostream &out) const {
 void CollisionVisualizer::
 void CollisionVisualizer::
 begin_traversal() {
 begin_traversal() {
   CollisionRecorder::begin_traversal();
   CollisionRecorder::begin_traversal();
+  LightMutexHolder holder(_lock);
   _data.clear();
   _data.clear();
 }
 }
 
 
@@ -271,12 +292,13 @@ collision_tested(const CollisionEntry &entry, bool detected) {
 
 
   NodePath node_path = entry.get_into_node_path();
   NodePath node_path = entry.get_into_node_path();
   CPT(TransformState) net_transform = node_path.get_net_transform();
   CPT(TransformState) net_transform = node_path.get_net_transform();
-  const CollisionSolid *solid = entry.get_into();
-  nassertv(solid != (CollisionSolid *)NULL);
+  CPT(CollisionSolid) solid = entry.get_into();
+  nassertv(!solid.is_null());
 
 
-  VizInfo &viz_info = _data[net_transform];
+  LightMutexHolder holder(_lock);
+  VizInfo &viz_info = _data[move(net_transform)];
   if (detected) {
   if (detected) {
-    viz_info._solids[solid]._detected_count++;
+    viz_info._solids[move(solid)]._detected_count++;
 
 
     if (entry.has_surface_point()) {
     if (entry.has_surface_point()) {
       CollisionPoint p;
       CollisionPoint p;
@@ -286,7 +308,7 @@ collision_tested(const CollisionEntry &entry, bool detected) {
     }
     }
 
 
   } else {
   } else {
-    viz_info._solids[solid]._missed_count++;
+    viz_info._solids[move(solid)]._missed_count++;
   }
   }
 }
 }
 
 

+ 3 - 0
panda/src/collide/collisionVisualizer.h

@@ -20,6 +20,7 @@
 #include "collisionSolid.h"
 #include "collisionSolid.h"
 #include "nodePath.h"
 #include "nodePath.h"
 #include "pmap.h"
 #include "pmap.h"
+#include "lightMutex.h"
 
 
 #ifdef DO_COLLISION_RECORDING
 #ifdef DO_COLLISION_RECORDING
 
 
@@ -34,6 +35,7 @@
 class EXPCL_PANDA_COLLIDE CollisionVisualizer : public PandaNode, public CollisionRecorder {
 class EXPCL_PANDA_COLLIDE CollisionVisualizer : public PandaNode, public CollisionRecorder {
 PUBLISHED:
 PUBLISHED:
   explicit CollisionVisualizer(const string &name);
   explicit CollisionVisualizer(const string &name);
+  CollisionVisualizer(const CollisionVisualizer &copy);
   virtual ~CollisionVisualizer();
   virtual ~CollisionVisualizer();
 
 
   INLINE void set_point_scale(PN_stdfloat point_scale);
   INLINE void set_point_scale(PN_stdfloat point_scale);
@@ -89,6 +91,7 @@ private:
     Points _points;
     Points _points;
   };
   };
 
 
+  LightMutex _lock;
   typedef pmap<CPT(TransformState), VizInfo> Data;
   typedef pmap<CPT(TransformState), VizInfo> Data;
   Data _data;
   Data _data;
 
 

+ 2 - 0
panda/src/display/graphicsEngine.cxx

@@ -583,6 +583,8 @@ void GraphicsEngine::
 remove_all_windows() {
 remove_all_windows() {
   Thread *current_thread = Thread::get_current_thread();
   Thread *current_thread = Thread::get_current_thread();
 
 
+  ReMutexHolder holder(_lock, current_thread);
+
   // Let's move the _windows vector into a local copy first, and walk through
   // Let's move the _windows vector into a local copy first, and walk through
   // that local copy, just in case someone we call during the loop attempts to
   // that local copy, just in case someone we call during the loop attempts to
   // modify _windows.  I don't know what code would be doing this, but it
   // modify _windows.  I don't know what code would be doing this, but it

+ 30 - 7
panda/src/dxgsg9/dxTextureContext9.cxx

@@ -365,6 +365,12 @@ create_texture(DXScreenData &scrn) {
     shrink_original = true;
     shrink_original = true;
   }
   }
 
 
+  if (target_width == 0 || target_height == 0) {
+    // We can't create a zero-sized texture, so change it to 1x1.
+    target_width = 1;
+    target_height = 1;
+  }
+
   const char *error_message;
   const char *error_message;
 
 
   error_message = "create_texture failed: couldn't find compatible device Texture Pixel Format for input texture";
   error_message = "create_texture failed: couldn't find compatible device Texture Pixel Format for input texture";
@@ -1721,16 +1727,32 @@ HRESULT DXTextureContext9::fill_d3d_texture_mipmap_pixels(int mip_level, int dep
   bool using_temp_buffer = false;
   bool using_temp_buffer = false;
   HRESULT hr = E_FAIL;
   HRESULT hr = E_FAIL;
   CPTA_uchar image = get_texture()->get_ram_mipmap_image(mip_level);
   CPTA_uchar image = get_texture()->get_ram_mipmap_image(mip_level);
-  nassertr(!image.is_null(), E_FAIL);
   BYTE *pixels = (BYTE*) image.p();
   BYTE *pixels = (BYTE*) image.p();
   DWORD width  = (DWORD) get_texture()->get_expected_mipmap_x_size(mip_level);
   DWORD width  = (DWORD) get_texture()->get_expected_mipmap_x_size(mip_level);
   DWORD height = (DWORD) get_texture()->get_expected_mipmap_y_size(mip_level);
   DWORD height = (DWORD) get_texture()->get_expected_mipmap_y_size(mip_level);
   int component_width = get_texture()->get_component_width();
   int component_width = get_texture()->get_component_width();
 
 
-  size_t view_size = get_texture()->get_ram_mipmap_view_size(mip_level);
-  pixels += view_size * get_view();
   size_t page_size = get_texture()->get_expected_ram_mipmap_page_size(mip_level);
   size_t page_size = get_texture()->get_expected_ram_mipmap_page_size(mip_level);
-  pixels += page_size * depth_index;
+  size_t view_size;
+  vector_uchar clear_data;
+  if (page_size > 0) {
+    if (image.is_null()) {
+      // Make an image, filled with the texture's clear color.
+      image = get_texture()->make_ram_mipmap_image(mip_level);
+      nassertr(!image.is_null(), E_FAIL);
+      pixels = (BYTE *)image.p();
+    }
+    view_size = image.size();
+    pixels += view_size * get_view();
+    pixels += page_size * depth_index;
+  } else {
+    // This is a 0x0 texture, which gets loaded as though it were 1x1.
+    width = 1;
+    height = 1;
+    clear_data = get_texture()->get_clear_data();
+    pixels = clear_data.data();
+    view_size = clear_data.size();
+  }
 
 
   if (get_texture()->get_texture_type() == Texture::TT_cube_map) {
   if (get_texture()->get_texture_type() == Texture::TT_cube_map) {
     nassertr(IS_VALID_PTR(_d3d_cube_texture), E_FAIL);
     nassertr(IS_VALID_PTR(_d3d_cube_texture), E_FAIL);
@@ -1909,7 +1931,9 @@ fill_d3d_texture_pixels(DXScreenData &scrn, bool compress_texture) {
                 DWORD flags;
                 DWORD flags;
                 D3DCOLOR color;
                 D3DCOLOR color;
 
 
-                color = 0xFF000000;
+                LColor scaled = tex->get_clear_color().fmin(LColor(1)).fmax(LColor::zero());
+                scaled *= 255;
+                color = D3DCOLOR_RGBA((int)scaled[0], (int)scaled[1], (int)scaled[2], (int)scaled[3]);
                 flags = D3DCLEAR_TARGET;
                 flags = D3DCLEAR_TARGET;
                 if (device -> Clear (NULL, NULL, flags, color, 0.0f, 0) == D3D_OK) {
                 if (device -> Clear (NULL, NULL, flags, color, 0.0f, 0) == D3D_OK) {
                 }
                 }
@@ -1933,9 +1957,8 @@ fill_d3d_texture_pixels(DXScreenData &scrn, bool compress_texture) {
 
 
       return S_OK;
       return S_OK;
     }
     }
-    return E_FAIL;
   }
   }
-  nassertr(IS_VALID_PTR((BYTE*)image.p()), E_FAIL);
+  //nassertr(IS_VALID_PTR((BYTE*)image.p()), E_FAIL);
   nassertr(IS_VALID_PTR(_d3d_texture), E_FAIL);
   nassertr(IS_VALID_PTR(_d3d_texture), E_FAIL);
 
 
   PStatTimer timer(GraphicsStateGuardian::_load_texture_pcollector);
   PStatTimer timer(GraphicsStateGuardian::_load_texture_pcollector);

+ 44 - 5
panda/src/egg2pg/eggSaver.cxx

@@ -52,6 +52,7 @@
 #include "modelNode.h"
 #include "modelNode.h"
 #include "animBundleNode.h"
 #include "animBundleNode.h"
 #include "animChannelMatrixXfmTable.h"
 #include "animChannelMatrixXfmTable.h"
+#include "characterJointEffect.h"
 #include "characterJoint.h"
 #include "characterJoint.h"
 #include "character.h"
 #include "character.h"
 #include "string_utils.h"
 #include "string_utils.h"
@@ -155,6 +156,16 @@ convert_node(const WorkingNodePath &node_path, EggGroupNode *egg_parent,
     convert_character_node(DCAST(Character, node), node_path, egg_parent, has_decal);
     convert_character_node(DCAST(Character, node), node_path, egg_parent, has_decal);
 
 
   } else {
   } else {
+    // Is this a ModelNode that represents an exposed joint?  If so, skip it,
+    // as we'll take care of it when building the joint hierarchy.
+    if (node->get_type() == ModelNode::get_class_type()) {
+      ModelNode *model_node = (ModelNode *)node;
+      if (model_node->get_preserve_transform() == ModelNode::PT_net &&
+          model_node->has_effect(CharacterJointEffect::get_class_type())) {
+        return;
+      }
+    }
+
     // Just a generic node.
     // Just a generic node.
     EggGroup *egg_group = new EggGroup(node->get_name());
     EggGroup *egg_group = new EggGroup(node->get_name());
     egg_parent->add_child(egg_group);
     egg_parent->add_child(egg_group);
@@ -354,6 +365,17 @@ convert_character_bundle(PartGroup *bundleNode, EggGroupNode *egg_parent, Charac
     EggGroup *joint = new EggGroup(bundleNode->get_name());
     EggGroup *joint = new EggGroup(bundleNode->get_name());
     joint->add_matrix4(transformd);
     joint->add_matrix4(transformd);
     joint->set_group_type(EggGroup::GT_joint);
     joint->set_group_type(EggGroup::GT_joint);
+
+    // Is this joint exposed?
+    NodePathCollection coll = character_joint->get_net_transforms();
+    for (size_t i = 0; i < coll.size(); ++i) {
+      const NodePath &np = coll[i];
+      if (np.get_name() == bundleNode->get_name() && np.node()->is_of_type(ModelNode::get_class_type())) {
+        joint->set_dcs_type(EggGroup::DC_net);
+        break;
+      }
+    }
+
     joint_group = joint;
     joint_group = joint;
     egg_parent->add_child(joint_group);
     egg_parent->add_child(joint_group);
     if (joint_map != NULL) {
     if (joint_map != NULL) {
@@ -384,16 +406,33 @@ convert_character_node(Character *node, const WorkingNodePath &node_path,
 
 
   // A sequence node gets converted to an ordinary EggGroup, we only apply the
   // A sequence node gets converted to an ordinary EggGroup, we only apply the
   // appropriate switch attributes to turn it into a sequence.
   // appropriate switch attributes to turn it into a sequence.
-  // We have to use DT_structured since it is the only mode that preserves the
-  // node hierarchy, including LODNodes and CollisionNodes that may be under
-  // this Character node.
   EggGroup *egg_group = new EggGroup(node->get_name());
   EggGroup *egg_group = new EggGroup(node->get_name());
-  egg_group->set_dart_type(EggGroup::DT_structured);
   egg_parent->add_child(egg_group);
   egg_parent->add_child(egg_group);
   apply_node_properties(egg_group, node);
   apply_node_properties(egg_group, node);
 
 
   CharacterJointMap joint_map;
   CharacterJointMap joint_map;
-  recurse_nodes(node_path, egg_group, has_decal, &joint_map);
+  bool is_structured = false;
+
+  int num_children = node->get_num_children();
+  for (int i = 0; i < num_children; i++) {
+    PandaNode *child = node->get_child(i);
+    convert_node(WorkingNodePath(node_path, child), egg_parent, has_decal, &joint_map);
+
+    TypeHandle type = child->get_type();
+    if (child->get_num_children() > 0 ||
+        (type != GeomNode::get_class_type() && type != ModelNode::get_class_type())) {
+      is_structured = true;
+    }
+  }
+
+  // We have to use DT_structured if it is necessary to preserve any node
+  // hierarchy, such as LODNodes and CollisionNodes that may be under this
+  // Character node.
+  if (is_structured) {
+    egg_group->set_dart_type(EggGroup::DT_structured);
+  } else {
+    egg_group->set_dart_type(EggGroup::DT_default);
+  }
 
 
   // turn it into a switch.. egg_group->set_switch_flag(true);
   // turn it into a switch.. egg_group->set_switch_flag(true);
 
 

+ 2 - 4
panda/src/event/asyncTask.cxx

@@ -402,8 +402,7 @@ unlock_and_do_task() {
   nassertr(current_thread->_current_task == nullptr, DS_interrupt);
   nassertr(current_thread->_current_task == nullptr, DS_interrupt);
 
 
   void *ptr = AtomicAdjust::compare_and_exchange_ptr
   void *ptr = AtomicAdjust::compare_and_exchange_ptr
-    ((void * TVOLATILE &)current_thread->_current_task,
-     (void *)nullptr, (void *)this);
+    (current_thread->_current_task, nullptr, (TypedReferenceCount *)this);
 
 
   // If the return value is other than nullptr, someone else must have
   // If the return value is other than nullptr, someone else must have
   // assigned the task first, in another thread.  That shouldn't be possible.
   // assigned the task first, in another thread.  That shouldn't be possible.
@@ -437,8 +436,7 @@ unlock_and_do_task() {
   nassertr(current_thread->_current_task == this, status);
   nassertr(current_thread->_current_task == this, status);
 
 
   ptr = AtomicAdjust::compare_and_exchange_ptr
   ptr = AtomicAdjust::compare_and_exchange_ptr
-    ((void * TVOLATILE &)current_thread->_current_task,
-     (void *)this, (void *)nullptr);
+    (current_thread->_current_task, (TypedReferenceCount *)this, nullptr);
 
 
   // If the return value is other than this, someone else must have assigned
   // If the return value is other than this, someone else must have assigned
   // the task first, in another thread.  That shouldn't be possible.
   // the task first, in another thread.  That shouldn't be possible.

+ 12 - 8
panda/src/gobj/geomVertexData.cxx

@@ -1836,17 +1836,21 @@ do_transform_vector_column(const GeomVertexFormat *format, GeomVertexRewriter &d
   bool normalize = false;
   bool normalize = false;
   if (data_column->get_contents() == C_normal) {
   if (data_column->get_contents() == C_normal) {
     // This is to preserve perpendicularity to the surface.
     // This is to preserve perpendicularity to the surface.
-    LVecBase3 scale, shear, hpr;
-    if (decompose_matrix(mat.get_upper_3(), scale, shear, hpr) &&
-        IS_NEARLY_EQUAL(scale[0], scale[1]) &&
-        IS_NEARLY_EQUAL(scale[0], scale[2])) {
-      if (scale[0] == 1) {
+    LVecBase3 scale_sq(mat.get_row3(0).length_squared(),
+                       mat.get_row3(1).length_squared(),
+                       mat.get_row3(2).length_squared());
+    if (IS_THRESHOLD_EQUAL(scale_sq[0], scale_sq[1], 2.0e-3f) &&
+        IS_THRESHOLD_EQUAL(scale_sq[0], scale_sq[2], 2.0e-3f)) {
+      // There is a uniform scale.
+      LVecBase3 scale, shear, hpr;
+      if (IS_THRESHOLD_EQUAL(scale_sq[0], 1, 2.0e-3f)) {
         // No scale to worry about.
         // No scale to worry about.
         xform = mat;
         xform = mat;
-      } else {
-        // Simply take the uniform scale out of the transformation.  Not sure
-        // if it might be better to just normalize?
+      } else if (decompose_matrix(mat.get_upper_3(), scale, shear, hpr)) {
+        // Make a new matrix with scale/translate taken out of the equation.
         compose_matrix(xform, LVecBase3(1, 1, 1), shear, hpr, LVecBase3::zero());
         compose_matrix(xform, LVecBase3(1, 1, 1), shear, hpr, LVecBase3::zero());
+      } else {
+        normalize = true;
       }
       }
     } else {
     } else {
       // There is a non-uniform scale, so we need to do all this to preserve
       // There is a non-uniform scale, so we need to do all this to preserve

+ 9 - 12
panda/src/gobj/shader.I

@@ -694,9 +694,9 @@ read_datagram(DatagramIterator &scan) {
  *
  *
  */
  */
 INLINE Shader::ShaderFile::
 INLINE Shader::ShaderFile::
-ShaderFile(const string &shared) :
+ShaderFile(string shared) :
   _separate(false),
   _separate(false),
-  _shared(shared)
+  _shared(move(shared))
 {
 {
 }
 }
 
 
@@ -704,17 +704,14 @@ ShaderFile(const string &shared) :
  *
  *
  */
  */
 INLINE Shader::ShaderFile::
 INLINE Shader::ShaderFile::
-ShaderFile(const string &vertex,
-           const string &fragment,
-           const string &geometry,
-           const string &tess_control,
-           const string &tess_evaluation) :
+ShaderFile(string vertex, string fragment, string geometry,
+           string tess_control, string tess_evaluation) :
   _separate(true),
   _separate(true),
-  _vertex(vertex),
-  _fragment(fragment),
-  _geometry(geometry),
-  _tess_control(tess_control),
-  _tess_evaluation(tess_evaluation)
+  _vertex(move(vertex)),
+  _fragment(move(fragment)),
+  _geometry(move(geometry)),
+  _tess_control(move(tess_control)),
+  _tess_evaluation(move(tess_evaluation))
 {
 {
 }
 }
 
 

+ 15 - 15
panda/src/gobj/shader.cxx

@@ -3179,7 +3179,7 @@ load_compute(ShaderLanguage lang, const Filename &fn) {
  * Loads the shader, using the string as shader body.
  * Loads the shader, using the string as shader body.
  */
  */
 PT(Shader) Shader::
 PT(Shader) Shader::
-make(const string &body, ShaderLanguage lang) {
+make(string body, ShaderLanguage lang) {
   if (lang == SL_GLSL) {
   if (lang == SL_GLSL) {
     shader_cat.error()
     shader_cat.error()
       << "GLSL shaders must have separate shader bodies!\n";
       << "GLSL shaders must have separate shader bodies!\n";
@@ -3197,7 +3197,7 @@ make(const string &body, ShaderLanguage lang) {
   }
   }
 #endif
 #endif
 
 
-  ShaderFile sbody(body);
+  ShaderFile sbody(move(body));
 
 
   if (cache_generated_shaders) {
   if (cache_generated_shaders) {
     ShaderTable::const_iterator i = _make_table.find(sbody);
     ShaderTable::const_iterator i = _make_table.find(sbody);
@@ -3208,7 +3208,7 @@ make(const string &body, ShaderLanguage lang) {
 
 
   PT(Shader) shader = new Shader(lang);
   PT(Shader) shader = new Shader(lang);
   shader->_filename = ShaderFile("created-shader");
   shader->_filename = ShaderFile("created-shader");
-  shader->_text = sbody;
+  shader->_text = move(sbody);
 
 
 #ifdef HAVE_CG
 #ifdef HAVE_CG
   if (lang == SL_Cg) {
   if (lang == SL_Cg) {
@@ -3223,7 +3223,7 @@ make(const string &body, ShaderLanguage lang) {
 #endif
 #endif
 
 
   if (cache_generated_shaders) {
   if (cache_generated_shaders) {
-    _make_table[sbody] = shader;
+    _make_table[shader->_text] = shader;
   }
   }
 
 
   if (dump_generated_shaders) {
   if (dump_generated_shaders) {
@@ -3235,7 +3235,7 @@ make(const string &body, ShaderLanguage lang) {
 
 
     pofstream s;
     pofstream s;
     s.open(fn.c_str(), ios::out | ios::trunc);
     s.open(fn.c_str(), ios::out | ios::trunc);
-    s << body;
+    s << shader->get_text();
     s.close();
     s.close();
   }
   }
   return shader;
   return shader;
@@ -3245,9 +3245,8 @@ make(const string &body, ShaderLanguage lang) {
  * Loads the shader, using the strings as shader bodies.
  * Loads the shader, using the strings as shader bodies.
  */
  */
 PT(Shader) Shader::
 PT(Shader) Shader::
-make(ShaderLanguage lang, const string &vertex, const string &fragment,
-     const string &geometry, const string &tess_control,
-     const string &tess_evaluation) {
+make(ShaderLanguage lang, string vertex, string fragment, string geometry,
+     string tess_control, string tess_evaluation) {
 #ifndef HAVE_CG
 #ifndef HAVE_CG
   if (lang == SL_Cg) {
   if (lang == SL_Cg) {
     shader_cat.error() << "Support for Cg shaders is not enabled.\n";
     shader_cat.error() << "Support for Cg shaders is not enabled.\n";
@@ -3260,7 +3259,8 @@ make(ShaderLanguage lang, const string &vertex, const string &fragment,
     return NULL;
     return NULL;
   }
   }
 
 
-  ShaderFile sbody(vertex, fragment, geometry, tess_control, tess_evaluation);
+  ShaderFile sbody(move(vertex), move(fragment), move(geometry),
+                   move(tess_control), move(tess_evaluation));
 
 
   if (cache_generated_shaders) {
   if (cache_generated_shaders) {
     ShaderTable::const_iterator i = _make_table.find(sbody);
     ShaderTable::const_iterator i = _make_table.find(sbody);
@@ -3271,7 +3271,7 @@ make(ShaderLanguage lang, const string &vertex, const string &fragment,
 
 
   PT(Shader) shader = new Shader(lang);
   PT(Shader) shader = new Shader(lang);
   shader->_filename = ShaderFile("created-shader");
   shader->_filename = ShaderFile("created-shader");
-  shader->_text = sbody;
+  shader->_text = move(sbody);
 
 
 #ifdef HAVE_CG
 #ifdef HAVE_CG
   if (lang == SL_Cg) {
   if (lang == SL_Cg) {
@@ -3284,7 +3284,7 @@ make(ShaderLanguage lang, const string &vertex, const string &fragment,
 #endif
 #endif
 
 
   if (cache_generated_shaders) {
   if (cache_generated_shaders) {
-    _make_table[sbody] = shader;
+    _make_table[shader->_text] = shader;
   }
   }
 
 
   return shader;
   return shader;
@@ -3294,7 +3294,7 @@ make(ShaderLanguage lang, const string &vertex, const string &fragment,
  * Loads the compute shader from the given string.
  * Loads the compute shader from the given string.
  */
  */
 PT(Shader) Shader::
 PT(Shader) Shader::
-make_compute(ShaderLanguage lang, const string &body) {
+make_compute(ShaderLanguage lang, string body) {
   if (lang != SL_GLSL) {
   if (lang != SL_GLSL) {
     shader_cat.error()
     shader_cat.error()
       << "Only GLSL compute shaders are currently supported.\n";
       << "Only GLSL compute shaders are currently supported.\n";
@@ -3303,7 +3303,7 @@ make_compute(ShaderLanguage lang, const string &body) {
 
 
   ShaderFile sbody;
   ShaderFile sbody;
   sbody._separate = true;
   sbody._separate = true;
-  sbody._compute = body;
+  sbody._compute = move(body);
 
 
   if (cache_generated_shaders) {
   if (cache_generated_shaders) {
     ShaderTable::const_iterator i = _make_table.find(sbody);
     ShaderTable::const_iterator i = _make_table.find(sbody);
@@ -3314,10 +3314,10 @@ make_compute(ShaderLanguage lang, const string &body) {
 
 
   PT(Shader) shader = new Shader(lang);
   PT(Shader) shader = new Shader(lang);
   shader->_filename = ShaderFile("created-shader");
   shader->_filename = ShaderFile("created-shader");
-  shader->_text = sbody;
+  shader->_text = move(sbody);
 
 
   if (cache_generated_shaders) {
   if (cache_generated_shaders) {
-    _make_table[sbody] = shader;
+    _make_table[shader->_text] = shader;
   }
   }
 
 
   return shader;
   return shader;

+ 9 - 12
panda/src/gobj/shader.h

@@ -84,7 +84,7 @@ PUBLISHED:
   };
   };
 
 
   static PT(Shader) load(const Filename &file, ShaderLanguage lang = SL_none);
   static PT(Shader) load(const Filename &file, ShaderLanguage lang = SL_none);
-  static PT(Shader) make(const string &body, ShaderLanguage lang = SL_none);
+  static PT(Shader) make(string body, ShaderLanguage lang = SL_none);
   static PT(Shader) load(ShaderLanguage lang,
   static PT(Shader) load(ShaderLanguage lang,
                          const Filename &vertex, const Filename &fragment,
                          const Filename &vertex, const Filename &fragment,
                          const Filename &geometry = "",
                          const Filename &geometry = "",
@@ -92,11 +92,11 @@ PUBLISHED:
                          const Filename &tess_evaluation = "");
                          const Filename &tess_evaluation = "");
   static PT(Shader) load_compute(ShaderLanguage lang, const Filename &fn);
   static PT(Shader) load_compute(ShaderLanguage lang, const Filename &fn);
   static PT(Shader) make(ShaderLanguage lang,
   static PT(Shader) make(ShaderLanguage lang,
-                         const string &vertex, const string &fragment,
-                         const string &geometry = "",
-                         const string &tess_control = "",
-                         const string &tess_evaluation = "");
-  static PT(Shader) make_compute(ShaderLanguage lang, const string &body);
+                         string vertex, string fragment,
+                         string geometry = "",
+                         string tess_control = "",
+                         string tess_evaluation = "");
+  static PT(Shader) make_compute(ShaderLanguage lang, string body);
 
 
   INLINE Filename get_filename(ShaderType type = ST_none) const;
   INLINE Filename get_filename(ShaderType type = ST_none) const;
   INLINE void set_filename(ShaderType type, const Filename &filename);
   INLINE void set_filename(ShaderType type, const Filename &filename);
@@ -464,12 +464,9 @@ public:
   class ShaderFile : public ReferenceCount {
   class ShaderFile : public ReferenceCount {
   public:
   public:
     INLINE ShaderFile() {};
     INLINE ShaderFile() {};
-    INLINE ShaderFile(const string &shared);
-    INLINE ShaderFile(const string &vertex,
-                      const string &fragment,
-                      const string &geometry,
-                      const string &tess_control,
-                      const string &tess_evaluation);
+    INLINE ShaderFile(string shared);
+    INLINE ShaderFile(string vertex, string fragment, string geometry,
+                      string tess_control, string tess_evaluation);
 
 
     INLINE void write_datagram(Datagram &dg) const;
     INLINE void write_datagram(Datagram &dg) const;
     INLINE void read_datagram(DatagramIterator &source);
     INLINE void read_datagram(DatagramIterator &source);

+ 3 - 0
panda/src/pgraph/lightAttrib.cxx

@@ -841,6 +841,9 @@ compose_impl(const RenderAttrib *other) const {
     lobj->attrib_ref();
     lobj->attrib_ref();
   }
   }
 
 
+  // This is needed since _sorted_on_lights is not yet populated.
+  new_attrib->_sort_seq = UpdateSeq::old();
+
   return return_new(new_attrib);
   return return_new(new_attrib);
 }
 }
 
 

+ 30 - 7
panda/src/pgraphnodes/shaderGenerator.cxx

@@ -409,11 +409,12 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
     }
     }
 
 
     // In fact, perhaps this stage should be disabled altogether?
     // In fact, perhaps this stage should be disabled altogether?
+    bool skip = false;
     switch (info._mode) {
     switch (info._mode) {
     case TextureStage::M_normal:
     case TextureStage::M_normal:
       if (!shader_attrib->auto_normal_on() ||
       if (!shader_attrib->auto_normal_on() ||
           (!key._lighting && (key._outputs & AuxBitplaneAttrib::ABO_aux_normal) == 0)) {
           (!key._lighting && (key._outputs & AuxBitplaneAttrib::ABO_aux_normal) == 0)) {
-        continue;
+        skip = true;
       } else {
       } else {
         info._flags = ShaderKey::TF_map_normal;
         info._flags = ShaderKey::TF_map_normal;
       }
       }
@@ -422,24 +423,34 @@ analyze_renderstate(ShaderKey &key, const RenderState *rs) {
       if (shader_attrib->auto_glow_on()) {
       if (shader_attrib->auto_glow_on()) {
         info._flags = ShaderKey::TF_map_glow;
         info._flags = ShaderKey::TF_map_glow;
       } else {
       } else {
-        continue;
+        skip = true;
       }
       }
       break;
       break;
     case TextureStage::M_gloss:
     case TextureStage::M_gloss:
       if (key._lighting && shader_attrib->auto_gloss_on()) {
       if (key._lighting && shader_attrib->auto_gloss_on()) {
         info._flags = ShaderKey::TF_map_gloss;
         info._flags = ShaderKey::TF_map_gloss;
       } else {
       } else {
-        continue;
+        skip = true;
       }
       }
       break;
       break;
     case TextureStage::M_height:
     case TextureStage::M_height:
       if (parallax_mapping_samples > 0) {
       if (parallax_mapping_samples > 0) {
         info._flags = ShaderKey::TF_map_height;
         info._flags = ShaderKey::TF_map_height;
       } else {
       } else {
-        continue;
+        skip = true;
       }
       }
       break;
       break;
     }
     }
+    // We can't just drop a disabled slot from the list, since then the
+    // indices for the texture stages will no longer match up.  So we keep it,
+    // but set it to a noop state to indicate that it should be skipped.
+    if (skip) {
+      info._type = Texture::TT_1d_texture;
+      info._mode = TextureStage::M_modulate;
+      info._flags = 0;
+      key._textures.push_back(info);
+      continue;
+    }
 
 
     // Check if this state has a texture matrix to transform the texture
     // Check if this state has a texture matrix to transform the texture
     // coordinates.
     // coordinates.
@@ -947,6 +958,11 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
   }
   }
   for (size_t i = 0; i < key._textures.size(); ++i) {
   for (size_t i = 0; i < key._textures.size(); ++i) {
     const ShaderKey::TextureInfo &tex = key._textures[i];
     const ShaderKey::TextureInfo &tex = key._textures[i];
+    if (tex._mode == TextureStage::M_modulate && tex._flags == 0) {
+      // Skip this stage.
+      continue;
+    }
+
     text << "\t uniform sampler" << texture_type_as_string(tex._type) << " tex_" << i << ",\n";
     text << "\t uniform sampler" << texture_type_as_string(tex._type) << " tex_" << i << ",\n";
 
 
     if (tex._flags & ShaderKey::TF_has_texscale) {
     if (tex._flags & ShaderKey::TF_has_texscale) {
@@ -1023,6 +1039,10 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
   // has a TexMatrixAttrib, also transform them.
   // has a TexMatrixAttrib, also transform them.
   for (size_t i = 0; i < key._textures.size(); ++i) {
   for (size_t i = 0; i < key._textures.size(); ++i) {
     const ShaderKey::TextureInfo &tex = key._textures[i];
     const ShaderKey::TextureInfo &tex = key._textures[i];
+    if (tex._mode == TextureStage::M_modulate && tex._flags == 0) {
+      // Skip this stage.
+      continue;
+    }
     switch (tex._gen_mode) {
     switch (tex._gen_mode) {
     case TexGenAttrib::M_off:
     case TexGenAttrib::M_off:
       // Cg seems to be able to optimize this temporary away when appropriate.
       // Cg seems to be able to optimize this temporary away when appropriate.
@@ -1099,6 +1119,10 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
   }
   }
   for (size_t i = 0; i < key._textures.size(); ++i) {
   for (size_t i = 0; i < key._textures.size(); ++i) {
     ShaderKey::TextureInfo &tex = key._textures[i];
     ShaderKey::TextureInfo &tex = key._textures[i];
+    if (tex._mode == TextureStage::M_modulate && tex._flags == 0) {
+      // Skip this stage.
+      continue;
+    }
     if ((tex._flags & ShaderKey::TF_map_height) == 0) {
     if ((tex._flags & ShaderKey::TF_map_height) == 0) {
       // Parallax mapping pushes the texture coordinates of the other textures
       // Parallax mapping pushes the texture coordinates of the other textures
       // away from the camera.
       // away from the camera.
@@ -1444,7 +1468,7 @@ synthesize_shader(const RenderState *rs, const GeomVertexAnimationSpec &anim) {
         text << "\t result.rgb = ";
         text << "\t result.rgb = ";
         text << combine_mode_as_string(tex, combine_rgb, false, i);
         text << combine_mode_as_string(tex, combine_rgb, false, i);
         text << ";\n\t result.a = ";
         text << ";\n\t result.a = ";
-        text << combine_mode_as_string(tex, combine_alpha, false, i);
+        text << combine_mode_as_string(tex, combine_alpha, true, i);
         text << ";\n";
         text << ";\n";
       }
       }
       if (tex._flags & ShaderKey::TF_rgb_scale_2) {
       if (tex._flags & ShaderKey::TF_rgb_scale_2) {
@@ -1667,6 +1691,7 @@ combine_source_as_string(const ShaderKey::TextureInfo &info, short num, bool alp
     csource << "saturate(1.0f - ";
     csource << "saturate(1.0f - ";
   }
   }
   switch (c_src) {
   switch (c_src) {
+    case TextureStage::CS_undefined:
     case TextureStage::CS_texture:
     case TextureStage::CS_texture:
       csource << "tex" << texindex;
       csource << "tex" << texindex;
       break;
       break;
@@ -1685,8 +1710,6 @@ combine_source_as_string(const ShaderKey::TextureInfo &info, short num, bool alp
     case TextureStage::CS_last_saved_result:
     case TextureStage::CS_last_saved_result:
       csource << "last_saved_result";
       csource << "last_saved_result";
       break;
       break;
-    case TextureStage::CS_undefined:
-      break;
   }
   }
   if (c_op == TextureStage::CO_one_minus_src_color ||
   if (c_op == TextureStage::CO_one_minus_src_color ||
       c_op == TextureStage::CO_one_minus_src_alpha) {
       c_op == TextureStage::CO_one_minus_src_alpha) {

+ 2 - 2
panda/src/pipeline/thread.I

@@ -277,9 +277,9 @@ preempt() {
  * AsyncTaskManager), if any, or NULL if the thread is not currently servicing
  * AsyncTaskManager), if any, or NULL if the thread is not currently servicing
  * a task.
  * a task.
  */
  */
-INLINE AsyncTask *Thread::
+INLINE TypedReferenceCount *Thread::
 get_current_task() const {
 get_current_task() const {
-  return _current_task;
+  return (TypedReferenceCount *)_current_task;
 }
 }
 
 
 /**
 /**

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

@@ -89,7 +89,7 @@ PUBLISHED:
   BLOCKING INLINE void join();
   BLOCKING INLINE void join();
   INLINE void preempt();
   INLINE void preempt();
 
 
-  INLINE AsyncTask *get_current_task() const;
+  INLINE TypedReferenceCount *get_current_task() const;
 
 
   INLINE void set_python_index(int index);
   INLINE void set_python_index(int index);
 
 
@@ -142,7 +142,7 @@ private:
   int _pipeline_stage;
   int _pipeline_stage;
   PStatsCallback *_pstats_callback;
   PStatsCallback *_pstats_callback;
   bool _joinable;
   bool _joinable;
-  AsyncTask *_current_task;
+  AtomicAdjust::Pointer _current_task;
 
 
   int _python_index;
   int _python_index;
 
 

+ 63 - 45
panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx

@@ -2397,9 +2397,6 @@ upload_texture(TinyTextureContext *gtc, bool force, bool uses_mipmaps) {
 
 
   PStatTimer timer(_load_texture_pcollector);
   PStatTimer timer(_load_texture_pcollector);
   CPTA_uchar src_image = tex->get_uncompressed_ram_image();
   CPTA_uchar src_image = tex->get_uncompressed_ram_image();
-  if (src_image.is_null()) {
-    return false;
-  }
 
 
 #ifdef DO_PSTATS
 #ifdef DO_PSTATS
   _data_transferred_pcollector.add_level(tex->get_ram_image_size());
   _data_transferred_pcollector.add_level(tex->get_ram_image_size());
@@ -2438,56 +2435,70 @@ upload_texture(TinyTextureContext *gtc, bool force, bool uses_mipmaps) {
   for (int level = 0; level < gltex->num_levels; ++level) {
   for (int level = 0; level < gltex->num_levels; ++level) {
     ZTextureLevel *dest = &gltex->levels[level];
     ZTextureLevel *dest = &gltex->levels[level];
 
 
-    switch (tex->get_format()) {
-    case Texture::F_rgb:
-    case Texture::F_rgb5:
-    case Texture::F_rgb8:
-    case Texture::F_rgb12:
-    case Texture::F_rgb332:
-      copy_rgb_image(dest, xsize, ysize, gtc, level);
-      break;
+    if (tex->has_ram_mipmap_image(level)) {
+      switch (tex->get_format()) {
+      case Texture::F_rgb:
+      case Texture::F_rgb5:
+      case Texture::F_rgb8:
+      case Texture::F_rgb12:
+      case Texture::F_rgb332:
+        copy_rgb_image(dest, xsize, ysize, gtc, level);
+        break;
 
 
-    case Texture::F_rgba:
-    case Texture::F_rgbm:
-    case Texture::F_rgba4:
-    case Texture::F_rgba5:
-    case Texture::F_rgba8:
-    case Texture::F_rgba12:
-    case Texture::F_rgba16:
-    case Texture::F_rgba32:
-      copy_rgba_image(dest, xsize, ysize, gtc, level);
-      break;
+      case Texture::F_rgba:
+      case Texture::F_rgbm:
+      case Texture::F_rgba4:
+      case Texture::F_rgba5:
+      case Texture::F_rgba8:
+      case Texture::F_rgba12:
+      case Texture::F_rgba16:
+      case Texture::F_rgba32:
+        copy_rgba_image(dest, xsize, ysize, gtc, level);
+        break;
 
 
-    case Texture::F_luminance:
-      copy_lum_image(dest, xsize, ysize, gtc, level);
-      break;
+      case Texture::F_luminance:
+        copy_lum_image(dest, xsize, ysize, gtc, level);
+        break;
 
 
-    case Texture::F_red:
-      copy_one_channel_image(dest, xsize, ysize, gtc, level, 0);
-      break;
+      case Texture::F_red:
+        copy_one_channel_image(dest, xsize, ysize, gtc, level, 0);
+        break;
 
 
-    case Texture::F_green:
-      copy_one_channel_image(dest, xsize, ysize, gtc, level, 1);
-      break;
+      case Texture::F_green:
+        copy_one_channel_image(dest, xsize, ysize, gtc, level, 1);
+        break;
 
 
-    case Texture::F_blue:
-      copy_one_channel_image(dest, xsize, ysize, gtc, level, 2);
-      break;
+      case Texture::F_blue:
+        copy_one_channel_image(dest, xsize, ysize, gtc, level, 2);
+        break;
 
 
-    case Texture::F_alpha:
-      copy_alpha_image(dest, xsize, ysize, gtc, level);
-      break;
+      case Texture::F_alpha:
+        copy_alpha_image(dest, xsize, ysize, gtc, level);
+        break;
 
 
-    case Texture::F_luminance_alphamask:
-    case Texture::F_luminance_alpha:
-      copy_la_image(dest, xsize, ysize, gtc, level);
-      break;
+      case Texture::F_luminance_alphamask:
+      case Texture::F_luminance_alpha:
+        copy_la_image(dest, xsize, ysize, gtc, level);
+        break;
 
 
-    default:
-      tinydisplay_cat.error()
-        << "Unsupported texture format "
-        << tex->get_format() << "!\n";
-      return false;
+      default:
+        tinydisplay_cat.error()
+          << "Unsupported texture format "
+          << tex->get_format() << "!\n";
+        return false;
+      }
+    } else {
+      // Fill the mipmap with a solid color.
+      LColor scaled = tex->get_clear_color().fmin(LColor(1)).fmax(LColor::zero());
+      scaled *= 255;
+      unsigned int clear = RGBA8_TO_PIXEL((int)scaled[0], (int)scaled[1],
+                                          (int)scaled[2], (int)scaled[3]);
+      unsigned int *dpix = (unsigned int *)dest->pixmap;
+      int pixel_count = xsize * ysize;
+      while (pixel_count-- > 0) {
+        *dpix = clear;
+        ++dpix;
+      }
     }
     }
 
 
     bytecount += xsize * ysize * 4;
     bytecount += xsize * ysize * 4;
@@ -2559,6 +2570,13 @@ upload_simple_texture(TinyTextureContext *gtc) {
  */
  */
 bool TinyGraphicsStateGuardian::
 bool TinyGraphicsStateGuardian::
 setup_gltex(GLTexture *gltex, int x_size, int y_size, int num_levels) {
 setup_gltex(GLTexture *gltex, int x_size, int y_size, int num_levels) {
+  if (x_size == 0 || y_size == 0) {
+    // A texture without pixels gets turned into a 1x1 texture.
+    x_size = 1;
+    y_size = 1;
+    num_levels = 1;
+  }
+
   int s_bits = get_tex_shift(x_size);
   int s_bits = get_tex_shift(x_size);
   int t_bits = get_tex_shift(y_size);
   int t_bits = get_tex_shift(y_size);
 
 

+ 6 - 0
panda/src/x11display/x11GraphicsWindow.cxx

@@ -670,6 +670,12 @@ set_properties_now(WindowProperties &properties) {
     } else {
     } else {
       XDefineCursor(_display, _xwindow, None);
       XDefineCursor(_display, _xwindow, None);
     }
     }
+
+    // Regrab the mouse if we changed the cursor, otherwise it won't update.
+    if (!properties.has_mouse_mode() &&
+        _properties.get_mouse_mode() != WindowProperties::M_absolute) {
+      properties.set_mouse_mode(_properties.get_mouse_mode());
+    }
   }
   }
 
 
   if (properties.has_foreground()) {
   if (properties.has_foreground()) {

+ 74 - 0
tests/pgraph/test_lightattrib.py

@@ -0,0 +1,74 @@
+from panda3d import core
+
+# Some dummy lights we can use for our light attributes.
+spot = core.NodePath(core.Spotlight("spot"))
+point = core.NodePath(core.PointLight("point"))
+ambient = core.NodePath(core.AmbientLight("ambient"))
+
+
+def test_lightattrib_compose_add():
+    # Tests a case in which a child node adds another light.
+    lattr1 = core.LightAttrib.make()
+    lattr1 = lattr1.add_on_light(spot)
+
+    lattr2 = core.LightAttrib.make()
+    lattr2 = lattr2.add_on_light(point)
+
+    lattr3 = lattr1.compose(lattr2)
+    assert lattr3.get_num_on_lights() == 2
+
+    assert spot in lattr3.on_lights
+    assert point in lattr3.on_lights
+
+
+def test_lightattrib_compose_subtract():
+    # Tests a case in which a child node disables a light.
+    lattr1 = core.LightAttrib.make()
+    lattr1 = lattr1.add_on_light(spot)
+    lattr1 = lattr1.add_on_light(point)
+
+    lattr2 = core.LightAttrib.make()
+    lattr2 = lattr2.add_off_light(ambient)
+    lattr2 = lattr2.add_off_light(point)
+
+    lattr3 = lattr1.compose(lattr2)
+    assert lattr3.get_num_on_lights() == 1
+
+    assert spot in lattr3.on_lights
+    assert point not in lattr3.on_lights
+    assert ambient not in lattr3.on_lights
+
+
+def test_lightattrib_compose_both():
+    # Tests a case in which a child node both enables and disables a light.
+    lattr1 = core.LightAttrib.make()
+    lattr1 = lattr1.add_on_light(spot)
+    lattr1 = lattr1.add_on_light(point)
+
+    lattr2 = core.LightAttrib.make()
+    lattr2 = lattr2.add_on_light(ambient)
+    lattr2 = lattr2.add_on_light(spot)
+    lattr2 = lattr2.add_off_light(point)
+
+    lattr3 = lattr1.compose(lattr2)
+    assert lattr3.get_num_on_lights() == 2
+
+    assert spot in lattr3.on_lights
+    assert point not in lattr3.on_lights
+    assert ambient in lattr3.on_lights
+
+
+def test_lightattrib_compose_alloff():
+    # Tests a case in which a child node disables all lights.
+    lattr1 = core.LightAttrib.make()
+    lattr1 = lattr1.add_on_light(spot)
+    lattr1 = lattr1.add_on_light(point)
+    assert lattr1.get_num_on_lights() == 2
+
+    lattr2 = core.LightAttrib.make_all_off()
+    assert lattr2.has_all_off()
+
+    lattr3 = lattr1.compose(lattr2)
+    assert lattr3.get_num_on_lights() == 0
+    assert lattr3.get_num_off_lights() == 0
+    assert lattr3.has_all_off()