Add a shared ResourcePool to share resources between EffectChains.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 17 Jan 2014 21:25:12 +0000 (22:25 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 17 Jan 2014 21:25:12 +0000 (22:25 +0100)
Right now in this first implementation, we only share shaders,
which mainly saves compile time when multiple EffectChains are
similar (e.g., many clips all are modified by white balance only).
However, in the future, the plan is for them to also be able to
share temporary textures.

Makefile.in
effect_chain.cpp
effect_chain.h
resource_pool.cpp [new file with mode: 0644]
resource_pool.h [new file with mode: 0644]
test_util.cpp

index d1eb165..ec5919d 100644 (file)
@@ -61,7 +61,7 @@ EFFECTS = $(TESTED_EFFECTS) $(UNTESTED_EFFECTS)
 # Unit tests.
 TESTS=effect_chain_test $(TESTED_INPUTS:=_test) $(TESTED_EFFECTS:=_test)
 
-LIB_OBJS=effect_util.o util.o widgets.o effect.o effect_chain.o init.o $(INPUTS:=.o) $(EFFECTS:=.o)
+LIB_OBJS=effect_util.o util.o widgets.o effect.o effect_chain.o init.o resource_pool.o $(INPUTS:=.o) $(EFFECTS:=.o)
 
 # Default target:
 all: libmovit.a $(TESTS)
@@ -122,7 +122,7 @@ coverage: check
        lcov --remove movit.info '*_test.cpp' '*/test_util.{cpp,h}' -o movit.info
        genhtml -o coverage movit.info
 
-HDRS = effect_chain.h effect_util.h effect.h input.h image_format.h init.h util.h defs.h
+HDRS = effect_chain.h effect_util.h effect.h input.h image_format.h init.h util.h defs.h resource_pool.h
 HDRS += $(INPUTS:=.h)
 HDRS += $(EFFECTS:=.h)
 
index 6b7a857..e6ba841 100644 (file)
 #include "gamma_expansion_effect.h"
 #include "init.h"
 #include "input.h"
+#include "resource_pool.h"
 #include "util.h"
 
-EffectChain::EffectChain(float aspect_nom, float aspect_denom)
+EffectChain::EffectChain(float aspect_nom, float aspect_denom, ResourcePool *resource_pool)
        : aspect_nom(aspect_nom),
          aspect_denom(aspect_denom),
          dither_effect(NULL),
          num_dither_bits(0),
-         finalized(false) {}
+         finalized(false),
+         resource_pool(resource_pool) {
+       if (resource_pool == NULL) {
+               this->resource_pool = new ResourcePool();
+               owns_resource_pool = true;
+       } else {
+               owns_resource_pool = false;
+       }
+}
 
 EffectChain::~EffectChain()
 {
@@ -42,11 +51,12 @@ EffectChain::~EffectChain()
                delete nodes[i];
        }
        for (unsigned i = 0; i < phases.size(); ++i) {
-               glDeleteProgram(phases[i]->glsl_program_num);
-               glDeleteShader(phases[i]->vertex_shader);
-               glDeleteShader(phases[i]->fragment_shader);
+               resource_pool->release_glsl_program(phases[i]->glsl_program_num);
                delete phases[i];
        }
+       if (owns_resource_pool) {
+               delete resource_pool;
+       }
 }
 
 Input *EffectChain::add_input(Input *input)
@@ -278,34 +288,8 @@ Phase *EffectChain::compile_glsl_program(
        frag_shader += std::string("#define INPUT ") + sorted_effects.back()->effect_id + "\n";
        frag_shader.append(read_file("footer.frag"));
 
-       if (movit_debug_level == MOVIT_DEBUG_ON) {
-               // Output shader to a temporary file, for easier debugging.
-               static int compiled_shader_num = 0;
-               char filename[256];
-               sprintf(filename, "chain-%03d.frag", compiled_shader_num++);
-               FILE *fp = fopen(filename, "w");
-               if (fp == NULL) {
-                       perror(filename);
-                       exit(1);
-               }
-               fprintf(fp, "%s\n", frag_shader.c_str());
-               fclose(fp);
-       }
-       
-       GLuint glsl_program_num = glCreateProgram();
-       GLuint vs_obj = compile_shader(read_file("vs.vert"), GL_VERTEX_SHADER);
-       GLuint fs_obj = compile_shader(frag_shader, GL_FRAGMENT_SHADER);
-       glAttachShader(glsl_program_num, vs_obj);
-       check_error();
-       glAttachShader(glsl_program_num, fs_obj);
-       check_error();
-       glLinkProgram(glsl_program_num);
-       check_error();
-
        Phase *phase = new Phase;
-       phase->glsl_program_num = glsl_program_num;
-       phase->vertex_shader = vs_obj;
-       phase->fragment_shader = fs_obj;
+       phase->glsl_program_num = resource_pool->compile_glsl_program(read_file("vs.vert"), frag_shader);
        phase->input_needs_mipmaps = input_needs_mipmaps;
        phase->inputs = true_inputs;
        phase->effects = sorted_effects;
index db3b870..1078027 100644 (file)
@@ -29,6 +29,7 @@
 class Effect;
 class Input;
 struct Phase;
+class ResourcePool;
 
 // For internal use within Node.
 enum AlphaType {
@@ -83,7 +84,7 @@ private:
 
 // A rendering phase; a single GLSL program rendering a single quad.
 struct Phase {
-       GLint glsl_program_num, vertex_shader, fragment_shader;
+       GLuint glsl_program_num;  // Owned by the resource_pool.
        bool input_needs_mipmaps;
 
        // Inputs are only inputs from other phases (ie., those that come from RTT);
@@ -96,7 +97,13 @@ struct Phase {
 
 class EffectChain {
 public:
-       EffectChain(float aspect_nom, float aspect_denom);  // E.g., 16.0f, 9.0f for 16:9.
+       // Aspect: e.g. 16.0f, 9.0f for 16:9.
+       // resource_pool is a pointer to a ResourcePool with which to share shaders
+       // and other resources (see resource_pool.h). If NULL (the default),
+       // will create its own that is not shared with anything else. Does not take
+       // ownership of the passed-in ResourcePool, but will naturally take ownership
+       // of its own internal one if created.
+       EffectChain(float aspect_nom, float aspect_denom, ResourcePool *resource_pool = NULL);
        ~EffectChain();
 
        // User API:
@@ -249,6 +256,9 @@ private:
 
        unsigned num_dither_bits;
        bool finalized;
+
+       ResourcePool *resource_pool;
+       bool owns_resource_pool;
 };
 
 #endif // !defined(_MOVIT_EFFECT_CHAIN_H)
diff --git a/resource_pool.cpp b/resource_pool.cpp
new file mode 100644 (file)
index 0000000..e65d0a9
--- /dev/null
@@ -0,0 +1,129 @@
+#include "resource_pool.h"
+
+#include <stdio.h>
+#include <pthread.h>
+
+#include <algorithm>
+#include <map>
+#include <string>
+#include <utility>
+
+#include "init.h"
+#include "util.h"
+
+using namespace std;
+
+ResourcePool::ResourcePool(size_t program_freelist_max_length)
+       : program_freelist_max_length(program_freelist_max_length) {
+       pthread_mutex_init(&lock, NULL);
+}
+
+ResourcePool::~ResourcePool()
+{
+       assert(program_refcount.empty());
+
+       for (list<GLuint>::const_iterator freelist_it = program_freelist.begin();
+            freelist_it != program_freelist.end();
+            ++freelist_it) {
+               delete_program(*freelist_it);
+       }
+       assert(programs.empty());
+       assert(program_shaders.empty());
+}
+
+void ResourcePool::delete_program(GLuint glsl_program_num)
+{
+       bool found_program = false;
+       for (std::map<std::pair<std::string, std::string>, GLuint>::iterator program_it = programs.begin();
+            program_it != programs.end();
+            ++program_it) {
+               if (program_it->second == glsl_program_num) {
+                       programs.erase(program_it);
+                       found_program = true;
+                       break;
+               }
+       }
+       assert(found_program);
+       glDeleteProgram(glsl_program_num);
+
+       std::map<GLuint, std::pair<GLuint, GLuint> >::iterator shader_it =
+               program_shaders.find(glsl_program_num);
+       assert(shader_it != program_shaders.end());
+
+       glDeleteShader(shader_it->second.first);
+       glDeleteShader(shader_it->second.second);
+       program_shaders.erase(shader_it);
+}
+
+GLuint ResourcePool::compile_glsl_program(const string& vertex_shader, const string& fragment_shader)
+{
+       GLuint glsl_program_num;
+       pthread_mutex_lock(&lock);
+       const pair<string, string> key(vertex_shader, fragment_shader);
+       if (programs.count(key)) {
+               // Already in the cache. Increment the refcount, or take it off the freelist
+               // if it's zero.
+               glsl_program_num = programs[key];
+               map<GLuint, int>::iterator refcount_it = program_refcount.find(glsl_program_num);
+               if (refcount_it != program_refcount.end()) {
+                       ++refcount_it->second;
+               } else {
+                       list<GLuint>::iterator freelist_it =
+                               find(program_freelist.begin(), program_freelist.end(), glsl_program_num);
+                       assert(freelist_it != program_freelist.end());
+                       program_freelist.erase(freelist_it);
+                       program_refcount.insert(make_pair(glsl_program_num, 1));
+               }
+       } else {
+               // Not in the cache. Compile the shaders.
+               glsl_program_num = glCreateProgram();
+               GLuint vs_obj = compile_shader(vertex_shader, GL_VERTEX_SHADER);
+               GLuint fs_obj = compile_shader(fragment_shader, GL_FRAGMENT_SHADER);
+               glAttachShader(glsl_program_num, vs_obj);
+               check_error();
+               glAttachShader(glsl_program_num, fs_obj);
+               check_error();
+               glLinkProgram(glsl_program_num);
+               check_error();
+
+               if (movit_debug_level == MOVIT_DEBUG_ON) {
+                       // Output shader to a temporary file, for easier debugging.
+                       static int compiled_shader_num = 0;
+                       char filename[256];
+                       sprintf(filename, "chain-%03d.frag", compiled_shader_num++);
+                       FILE *fp = fopen(filename, "w");
+                       if (fp == NULL) {
+                               perror(filename);
+                               exit(1);
+                       }
+                       fprintf(fp, "%s\n", fragment_shader.c_str());
+                       fclose(fp);
+               }
+
+               programs.insert(make_pair(key, glsl_program_num));
+               program_refcount.insert(make_pair(glsl_program_num, 1));
+               program_shaders.insert(make_pair(glsl_program_num, make_pair(vs_obj, fs_obj)));
+       }
+       pthread_mutex_unlock(&lock);
+       return glsl_program_num;
+}
+
+void ResourcePool::release_glsl_program(GLuint glsl_program_num)
+{
+       pthread_mutex_lock(&lock);
+       map<GLuint, int>::iterator refcount_it = program_refcount.find(glsl_program_num);
+       assert(refcount_it != program_refcount.end());
+
+       if (--refcount_it->second == 0) {
+               program_refcount.erase(refcount_it);
+               assert(find(program_freelist.begin(), program_freelist.end(), glsl_program_num)
+                       == program_freelist.end());
+               program_freelist.push_front(glsl_program_num);
+               if (program_freelist.size() > program_freelist_max_length) {
+                       delete_program(program_freelist.back());
+                       program_freelist.pop_back();
+               }
+       }
+
+       pthread_mutex_unlock(&lock);
+}
diff --git a/resource_pool.h b/resource_pool.h
new file mode 100644 (file)
index 0000000..a6e4327
--- /dev/null
@@ -0,0 +1,69 @@
+#ifndef _MOVIT_RESOURCE_POOL_H
+#define _MOVIT_RESOURCE_POOL_H 1
+
+// A ResourcePool governs resources that are shared between multiple EffectChains;
+// in particular, resources that might be expensive to acquire or hold. Thus,
+// if you have many EffectChains, hooking them up to the same ResourcePool is
+// probably a good idea.
+//
+// However, hooking an EffectChain to a ResourcePool extends the OpenGL context
+// demands (see effect_chain.h) to that of the ResourcePool; all chains must then
+// only be used in OpenGL contexts sharing resources with each other. This is
+// the reason why there isn't just one global ResourcePool singleton (although
+// most practical users will just want one).
+//
+// Thread-safety: All functions except the constructor and destructor can be
+// safely called from multiple threads at the same time, provided they have
+// separate (but sharing) OpenGL contexts.
+
+#include <list>
+#include <map>
+#include <string>
+#include <utility>
+#include <GL/glew.h>
+#include <pthread.h>
+
+class ResourcePool {
+public:
+       // program_freelist_max_length is how many compiled programs that are unused to keep
+       // around after they are no longer in use (in case another EffectChain
+       // wants that exact program later). Shaders are expensive to compile and do not
+       // need a lot of resources to keep around, so this should be a reasonable number.
+       ResourcePool(size_t program_freelist_max_length = 100);
+       ~ResourcePool();
+
+       // All remaining functions are intended for calls from EffectChain only.
+
+       // Compile the given vertex+fragment shader pair, or fetch an already
+       // compiled program from the cache if possible. Keeps ownership of the
+       // program; you must call release_glsl_program() instead of deleting it
+       // when you no longer want it.
+       GLuint compile_glsl_program(const std::string& vertex_shader, const std::string& fragment_shader);
+       void release_glsl_program(GLuint glsl_program_num);
+
+private:
+       // Delete the given program and both its shaders.
+       void delete_program(GLuint program_num);
+
+       // Protects all the other elements in the class.
+       pthread_mutex_t lock;
+
+       size_t program_freelist_max_length;
+               
+       // A mapping from vertex/fragment shader source strings to compiled program number.
+       std::map<std::pair<std::string, std::string>, GLuint> programs;
+
+       // A mapping from compiled program number to number of current users.
+       // Once this reaches zero, the program is taken out of this map and instead
+       // put on the freelist (after which it may be deleted).
+       std::map<GLuint, int> program_refcount;
+
+       // A mapping from program number to vertex and fragment shaders.
+       std::map<GLuint, std::pair<GLuint, GLuint> > program_shaders;
+
+       // A list of programs that are no longer in use, most recently freed first.
+       // Once this reaches <program_freelist_max_length>, 
+       std::list<GLuint> program_freelist;
+};
+
+#endif  // !defined(_MOVIT_RESOURCE_POOL_H)
index a35bbea..33a6916 100644 (file)
@@ -7,6 +7,7 @@
 #include "flat_input.h"
 #include "gtest/gtest.h"
 #include "init.h"
+#include "resource_pool.h"
 #include "test_util.h"
 #include "util.h"
 
@@ -14,6 +15,16 @@ class Input;
 
 namespace {
 
+// Not thread-safe, but this isn't a big problem for testing.
+ResourcePool *get_static_pool()
+{
+       static ResourcePool *resource_pool = NULL;
+       if (!resource_pool) {
+               resource_pool = new ResourcePool();
+       }
+       return resource_pool;
+}
+
 // Flip upside-down to compensate for different origin.
 template<class T>
 void vertical_flip(T *data, unsigned width, unsigned height)
@@ -31,7 +42,7 @@ void vertical_flip(T *data, unsigned width, unsigned height)
 EffectChainTester::EffectChainTester(const float *data, unsigned width, unsigned height,
                                      MovitPixelFormat pixel_format, Colorspace color_space, GammaCurve gamma_curve,
                                      GLenum framebuffer_format)
-       : chain(width, height), width(width), height(height), finalized(false)
+       : chain(width, height, get_static_pool()), width(width), height(height), finalized(false)
 {
        init_movit(".", MOVIT_DEBUG_ON);