X-Git-Url: https://git.sesse.net/?p=movit;a=blobdiff_plain;f=effect_chain.h;h=35931f8058fb1a7d763fdb7572ccd2f94bb4d945;hp=f0138a99d48bd5a6783f6357229f37fa3d5b03d7;hb=7366e74b75fa1ac3267709c12417179819b86acb;hpb=c8ddccde29b5dd2c207cc67fa8af2707904557d8 diff --git a/effect_chain.h b/effect_chain.h index f0138a9..35931f8 100644 --- a/effect_chain.h +++ b/effect_chain.h @@ -1,15 +1,45 @@ -#ifndef _EFFECT_CHAIN_H -#define _EFFECT_CHAIN_H 1 - +#ifndef _MOVIT_EFFECT_CHAIN_H +#define _MOVIT_EFFECT_CHAIN_H 1 + +// An EffectChain is the largest basic entity in Movit; it contains everything +// needed to connects a series of effects, from inputs to outputs, and render +// them. Generally you set up your effect chain once and then call its render +// functions once per frame; setting one up can be relatively expensive, +// but rendering is fast. +// +// Threading considerations: EffectChain is “thread-compatible”; you can use +// different EffectChains in multiple threads at the same time (assuming the +// threads do not use the same OpenGL context, but this is a good idea anyway), +// but you may not use one EffectChain from multiple threads simultaneously. +// You _are_ allowed to use one EffectChain from multiple threads as long as +// you only use it from one at a time (possibly by doing your own locking), +// but if so, the threads' contexts need to be set up to share resources, since +// the EffectChain holds textures and other OpenGL objects that are tied to the +// context. +// +// Memory management (only relevant if you use multiple contexts): +// See corresponding comment in resource_pool.h. This holds even if you don't +// allocate your own ResourcePool, but let EffectChain hold its own. + +#include +#include +#include +#include #include +#include #include +#include #include "effect.h" #include "image_format.h" -#include "input.h" +#include "ycbcr.h" + +namespace movit { -class EffectChain; -class Phase; +class Effect; +class Input; +struct Phase; +class ResourcePool; // For internal use within Node. enum AlphaType { @@ -22,8 +52,64 @@ enum AlphaType { // Whether you want pre- or postmultiplied alpha in the output // (see effect.h for a discussion of pre- versus postmultiplied alpha). enum OutputAlphaFormat { - OUTPUT_ALPHA_PREMULTIPLIED, - OUTPUT_ALPHA_POSTMULTIPLIED, + OUTPUT_ALPHA_FORMAT_PREMULTIPLIED, + OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED, +}; + +// RGBA output is nearly always packed; Y'CbCr, however, is often planar +// due to chroma subsampling. This enum controls how add_ycbcr_output() +// distributes the color channels between the fragment shader outputs. +// Obviously, anything except YCBCR_OUTPUT_INTERLEAVED will be meaningless +// unless you use render_to_fbo() and have an FBO with multiple render +// targets attached (the other outputs will be discarded). +enum YCbCrOutputSplitting { + // Only one output: Store Y'CbCr into the first three output channels, + // respectively, plus alpha. This is also called “chunked” or + // ”packed” mode. + YCBCR_OUTPUT_INTERLEAVED, + + // Store Y' and alpha into the first output (in the red and alpha + // channels; effect to the others is undefined), and Cb and Cr into + // the first two channels of the second output. This is particularly + // useful if you want to end up in a format like NV12, where all the + // Y' samples come first and then Cb and Cr come interlevaed afterwards. + // You will still need to do the chroma subsampling yourself to actually + // get down to NV12, though. + YCBCR_OUTPUT_SPLIT_Y_AND_CBCR, + + // Store Y' and alpha into the first output, Cb into the first channel + // of the second output and Cr into the first channel of the third output. + // (Effect on the other channels is undefined.) Essentially gives you + // 4:4:4 planar, or ”yuv444p”. + YCBCR_OUTPUT_PLANAR, +}; + +// Where (0,0) is taken to be in the output. If you want to render to an +// OpenGL screen, you should keep the default of bottom-left, as that is +// OpenGL's natural coordinate system. However, there are cases, such as if you +// render to an FBO and read the pixels back into some other system, where +// you'd want a top-left origin; if so, an additional flip step will be added +// at the very end (but done in a vertex shader, so it will have zero extra +// cost). +// +// Note that Movit's coordinate system in general consistently puts (0,0) in +// the top left for _input_, no matter what you set as output origin. +enum OutputOrigin { + OUTPUT_ORIGIN_BOTTOM_LEFT, + OUTPUT_ORIGIN_TOP_LEFT, +}; + +// Transformation to apply (if any) to pixel data in temporary buffers. +// See set_intermediate_format() below for more information. +enum FramebufferTransformation { + // The default; just store the value. This is what you usually want. + NO_FRAMEBUFFER_TRANSFORMATION, + + // If the values are in linear light, store sqrt(x) to the framebuffer + // instead of x itself, of course undoing it with x² on read. Useful as + // a rough approximation to the sRGB curve. (If the values are not in + // linear light, just store them as-is.) + SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION, }; // A node in the graph; basically an effect and some associated information. @@ -36,10 +122,11 @@ public: std::vector outgoing_links; std::vector incoming_links; -private: - // Identifier used to create unique variables in GLSL. - std::string effect_id; + // For unit tests only. Do not use from other code. + // Will contain an arbitrary choice if the node is in multiple phases. + Phase *containing_phase; +private: // Logical size of the output of this effect, ie. the resolution // you would get if you sampled it as a texture. If it is undefined // (since the inputs differ in resolution), it will be 0x0. @@ -47,37 +134,93 @@ private: // they will be equal. unsigned output_width, output_height; - // If output goes to RTT (otherwise, none of these are set). - // The Phase pointer is a but ugly; we should probably fix so - // that Phase takes other phases as inputs, instead of Node. - GLuint output_texture; - unsigned output_texture_width, output_texture_height; - Phase *phase; + // If the effect has is_single_texture(), or if the output went to RTT + // and that texture has been bound to a sampler, the sampler number + // will be stored here. + // + // TODO: Can an RTT texture be used as inputs to multiple effects + // within the same phase? If so, we have a problem with modifying + // sampler state here. + int bound_sampler_num; // Used during the building of the effect chain. Colorspace output_color_space; GammaCurve output_gamma_curve; AlphaType output_alpha_type; + bool needs_mipmaps; // Directly or indirectly. + + // Set if this effect, and all effects consuming output from this node + // (in the same phase) have one_to_one_sampling() set. + bool one_to_one_sampling; friend class EffectChain; }; // A rendering phase; a single GLSL program rendering a single quad. struct Phase { - GLint glsl_program_num, vertex_shader, fragment_shader; + Node *output_node; + + GLuint glsl_program_num; // Owned by the resource_pool. + + // Position and texcoord attribute indexes, although it doesn't matter + // which is which, because they contain the same data. + std::set attribute_indexes; + bool input_needs_mipmaps; // Inputs are only inputs from other phases (ie., those that come from RTT); - // input textures are not counted here. - std::vector inputs; - + // input textures are counted as part of . + std::vector inputs; + // Bound sampler numbers for each input. Redundant in a sense + // (it always corresponds to the index), but we need somewhere + // to hold the value for the uniform. + std::vector input_samplers; std::vector effects; // In order. - unsigned output_width, output_height; + unsigned output_width, output_height, virtual_output_width, virtual_output_height; + + // Whether this phase is compiled as a compute shader, ie., the last effect is + // marked as one. + bool is_compute_shader; + + // If , which image unit the output buffer is bound to. + // This is used as source for a Uniform below. + int outbuf_image_unit; + + // These are used in transforming from unnormalized to normalized coordinates + // in compute shaders. + Point2D inv_output_size, output_texcoord_adjust; + + // Identifier used to create unique variables in GLSL. + // Unique per-phase to increase cacheability of compiled shaders. + std::map effect_ids; + + // Uniforms for this phase; combined from all the effects. + std::vector> uniforms_image2d; + std::vector> uniforms_sampler2d; + std::vector> uniforms_bool; + std::vector> uniforms_int; + std::vector> uniforms_float; + std::vector> uniforms_vec2; + std::vector> uniforms_vec3; + std::vector> uniforms_vec4; + std::vector> uniforms_mat3; + + // For measurement of GPU time used. + std::list timer_query_objects_running; + std::list timer_query_objects_free; + uint64_t time_elapsed_ns; + uint64_t num_measured_iterations; }; 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 nullptr (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 = nullptr); ~EffectChain(); // User API: @@ -103,22 +246,140 @@ public: inputs.push_back(input2); return add_effect(effect, inputs); } + Effect *add_effect(Effect *effect, Effect *input1, Effect *input2, Effect *input3) { + std::vector inputs; + inputs.push_back(input1); + inputs.push_back(input2); + inputs.push_back(input3); + return add_effect(effect, inputs); + } + Effect *add_effect(Effect *effect, Effect *input1, Effect *input2, Effect *input3, Effect *input4) { + std::vector inputs; + inputs.push_back(input1); + inputs.push_back(input2); + inputs.push_back(input3); + inputs.push_back(input4); + return add_effect(effect, inputs); + } + Effect *add_effect(Effect *effect, Effect *input1, Effect *input2, Effect *input3, Effect *input4, Effect *input5) { + std::vector inputs; + inputs.push_back(input1); + inputs.push_back(input2); + inputs.push_back(input3); + inputs.push_back(input4); + inputs.push_back(input5); + return add_effect(effect, inputs); + } Effect *add_effect(Effect *effect, const std::vector &inputs); + // Adds an RGBA output. Note that you can have at most one RGBA output and two + // Y'CbCr outputs (see below for details). void add_output(const ImageFormat &format, OutputAlphaFormat alpha_format); + // Adds an YCbCr output. Note that you can only have at most two Y'CbCr + // outputs, and they must have the same and . + // (This limitation may be lifted in the future, to allow e.g. simultaneous + // 8- and 10-bit output. Currently, multiple Y'CbCr outputs are only + // useful in some very limited circumstances, like if one texture goes + // to some place you cannot easily read from later.) + // + // Only 4:4:4 output is supported due to fragment shader limitations, + // so chroma_subsampling_x and chroma_subsampling_y must both be 1. + // should match the data type of the FBO you are rendering to, + // so that if you use 16-bit output (GL_UNSIGNED_SHORT), you will get + // 8-, 10- or 12-bit output correctly as determined by . + // Using e.g. ycbcr_format.num_levels == 1024 with GL_UNSIGNED_BYTE is + // nonsensical and invokes undefined behavior. + // + // If you have both RGBA and Y'CbCr output(s), the RGBA output will come + // in the last draw buffer. Also, and must be + // identical between the two. + void add_ycbcr_output(const ImageFormat &format, OutputAlphaFormat alpha_format, + const YCbCrFormat &ycbcr_format, + YCbCrOutputSplitting output_splitting = YCBCR_OUTPUT_INTERLEAVED, + GLenum output_type = GL_UNSIGNED_BYTE); + + // Change Y'CbCr output format. (This can be done also after finalize()). + // Note that you are not allowed to change subsampling parameters; + // however, you can change the color space parameters, ie., + // luma_coefficients, full_range and num_levels. + void change_ycbcr_output_format(const YCbCrFormat &ycbcr_format); + // Set number of output bits, to scale the dither. // 8 is the right value for most outputs. + // + // Special note for 10- and 12-bit Y'CbCr packed into GL_UNSIGNED_SHORT: + // This is relative to the actual output, not the logical one, so you should + // specify 16 here, not 10 or 12. + // // The default, 0, is a special value that means no dither. void set_dither_bits(unsigned num_bits) { this->num_dither_bits = num_bits; } - void finalize(); + // Set where (0,0) is taken to be in the output. The default is + // OUTPUT_ORIGIN_BOTTOM_LEFT, which is usually what you want + // (see OutputOrigin above for more details). + void set_output_origin(OutputOrigin output_origin) + { + this->output_origin = output_origin; + } + // Set intermediate format for framebuffers used when we need to bounce + // to a temporary texture. The default, GL_RGBA16F, is good for most uses; + // it is precise, has good range, and is relatively efficient. However, + // if you need even more speed and your chain can do with some loss of + // accuracy, you can change the format here (before calling finalize). + // Calculations between bounce buffers are still in 32-bit floating-point + // no matter what you specify. + // + // Of special interest is GL_SRGB8_ALPHA8, which stores sRGB-encoded RGB + // and linear alpha; this is half the memory bandwidth of GL_RGBA16F, + // while retaining reasonable precision for typical image data. It will, + // however, cause some gamut clipping if your colorspace is far from sRGB, + // as it cannot represent values outside [0,1]. NOTE: If you construct + // a chain where you end up bouncing pixels in non-linear light + // (gamma different from GAMMA_LINEAR), this will be the wrong thing. + // However, it's hard to see how this could happen in a non-contrived + // chain; few effects ever need texture bounce or resizing without also + // combining multiple pixels, which really needs linear light and thus + // triggers a conversion before the bounce. + // + // If you don't need alpha (or can do with very little of it), GL_RGB10_A2 + // is even better, as it has two more bits for each color component. There + // is no GL_SRGB10, unfortunately, so on its own, it is somewhat worse than + // GL_SRGB8, but you can set to SQUARE_ROOT_FRAMEBUFFER_TRANSFORMATION, + // and sqrt(x) will be stored instead of x. This is a rough approximation to + // the sRGB curve, and reduces maximum error (in sRGB distance) by almost an + // order of magnitude, well below what you can get from 8-bit true sRGB. + // (Note that this strategy avoids the problem with bounced non-linear data + // above, since the square root is turned off in that case.) However, texture + // filtering will happen on the transformed values, so if you have heavy + // downscaling or the likes (e.g. mipmaps), you could get subtly bad results. + // You'll need to see which of the two that works the best for you in practice. + void set_intermediate_format( + GLenum intermediate_format, + FramebufferTransformation transformation = NO_FRAMEBUFFER_TRANSFORMATION) + { + this->intermediate_format = intermediate_format; + this->intermediate_transformation = transformation; + } - //void render(unsigned char *src, unsigned char *dst); + void finalize(); + + // Measure the GPU time used for each actual phase during rendering. + // Note that this is only available if GL_ARB_timer_query + // (or, equivalently, OpenGL 3.3) is available. Also note that measurement + // will incur a performance cost, as we wait for the measurements to + // complete at the end of rendering. + void enable_phase_timing(bool enable); + void reset_phase_timing(); + void print_phase_timing(); + + // Note: If you already know the width and height of the viewport, + // calling render_to_fbo() directly will be slightly more efficient, + // as it saves it from getting it from OpenGL. void render_to_screen() { render_to_fbo(0, 0, 0); @@ -128,9 +389,25 @@ public: // the current viewport. void render_to_fbo(GLuint fbo, unsigned width, unsigned height); + // Render the effect chain to the given set of textures. This is equivalent + // to render_to_fbo() with a freshly created FBO bound to the given textures, + // except that it is more efficient if the last phase contains a compute shader. + // Thus, prefer this to render_to_fbo() where possible. + // + // The format must currently be GL_RGBA16F, and only one destination + // texture is supported. Both of these restrictions will be lifted in the future. + // + // All destination textures must be exactly of size x . + // width and height can not be zero. + struct DestinationTexture { + GLuint texnum; + GLenum format; + }; + void render_to_texture(const std::vector &destinations, unsigned width, unsigned height); + Effect *last_added_effect() { if (nodes.empty()) { - return NULL; + return nullptr; } else { return nodes.back()->effect; } @@ -146,11 +423,35 @@ public: void replace_receiver(Node *old_receiver, Node *new_receiver); void replace_sender(Node *new_sender, Node *receiver); void insert_node_between(Node *sender, Node *middle, Node *receiver); + Node *find_node_for_effect(Effect *effect) { return node_map[effect]; } + + // Get the OpenGL sampler (GL_TEXTURE0, GL_TEXTURE1, etc.) for the + // input of the given node, so that one can modify the sampler state + // directly. Only valid to call during set_gl_state(). + // + // Also, for this to be allowed, 's effect must have + // needs_texture_bounce() set, so that it samples directly from a + // single-sampler input, or from an RTT texture. + GLenum get_input_sampler(Node *node, unsigned input_num) const; + + // Whether input of corresponds to a single sampler + // (see get_input_sampler()). Normally, you should not need to call this; + // however, if the input Effect has set override_texture_bounce(), + // this will return false, and you could be flexible and check it first + // if you want. + GLenum has_input_sampler(Node *node, unsigned input_num) const; + + // Get the current resource pool assigned to this EffectChain. + // Primarily to let effects allocate textures as needed. + // Any resources you get from the pool must be returned to the pool + // no later than in the Effect's destructor. + ResourcePool *get_resource_pool() { return resource_pool; } private: - // Fits a rectangle of the given size to the current aspect ratio - // (aspect_nom/aspect_denom) and returns the new width and height. - unsigned fit_rectangle_to_aspect(unsigned width, unsigned height); + // Make sure the output rectangle is at least large enough to hold + // the given input rectangle in both dimensions, and is of the + // current aspect ratio (aspect_nom/aspect_denom). + void size_rectangle_to_fit(unsigned width, unsigned height, unsigned *output_width, unsigned *output_height); // Compute the input sizes for all inputs for all effects in a given phase, // and inform the effects about the results. @@ -164,13 +465,31 @@ private: // output gamma different from GAMMA_LINEAR. void find_all_nonlinear_inputs(Node *effect, std::vector *nonlinear_inputs); - // Create a GLSL program computing the given effects in order. - Phase *compile_glsl_program(const std::vector &inputs, - const std::vector &effects); + // Create a GLSL program computing the effects for this phase in order. + void compile_glsl_program(Phase *phase); // Create all GLSL programs needed to compute the given effect, and all outputs - // that depends on it (whenever possible). - void construct_glsl_programs(Node *output); + // that depend on it (whenever possible). Returns the phase that has + // as the last effect. Also pushes all phases in order onto . + Phase *construct_phase(Node *output, std::map *completed_effects); + + // Do the actual rendering of the chain. If is not (GLuint)-1, + // renders to that FBO. If is non-empty, render to that set + // of textures (last phase, save for the dummy phase, must be a compute shader), + // with x/y ignored. Having both set is an error. + void render(GLuint dest_fbo, const std::vector &destinations, + unsigned x, unsigned y, unsigned width, unsigned height); + + // Execute one phase, ie. set up all inputs, effects and outputs, and render the quad. + void execute_phase(Phase *phase, bool render_to_texture, + std::map *output_textures, + std::set *generated_mipmaps); + + // Set up uniforms for one phase. The program must already be bound. + void setup_uniforms(Phase *phase); + + // Set up the given sampler number for sampling from an RTT texture. + void setup_rtt_sampler(int sampler_num, bool use_mipmaps); // Output the current graph to the given file in a Graphviz-compatible format; // only useful for debugging. @@ -214,23 +533,47 @@ private: void fix_internal_gamma_by_asking_inputs(unsigned step); void fix_internal_gamma_by_inserting_nodes(unsigned step); void fix_output_gamma(); + void add_ycbcr_conversion_if_needed(); void add_dither_if_needed(); + void add_dummy_effect_if_needed(); float aspect_nom, aspect_denom; ImageFormat output_format; OutputAlphaFormat output_alpha_format; + bool output_color_rgba; + int num_output_color_ycbcr; // Max 2. + YCbCrFormat output_ycbcr_format; // If num_output_color_ycbcr is > 0. + GLenum output_ycbcr_type; // If num_output_color_ycbcr is > 0. + YCbCrOutputSplitting output_ycbcr_splitting[2]; // If num_output_color_ycbcr is > N. + std::vector nodes; std::map node_map; Effect *dither_effect; + Node *ycbcr_conversion_effect_node; std::vector inputs; // Also contained in nodes. - - GLuint fbo; std::vector phases; + GLenum intermediate_format; + FramebufferTransformation intermediate_transformation; unsigned num_dither_bits; + OutputOrigin output_origin; bool finalized; + GLuint vbo; // Contains vertex and texture coordinate data. + + // Whether the last effect (which will then be in a phase all by itself) + // is a dummy effect that is only added because the last phase uses a compute + // shader, which cannot output directly to the backbuffer. This means that + // the phase can be skipped if we are _not_ rendering to the backbuffer. + bool has_dummy_effect = false; + + ResourcePool *resource_pool; + bool owns_resource_pool; + + bool do_phase_timing; }; -#endif // !defined(_EFFECT_CHAIN_H) +} // namespace movit + +#endif // !defined(_MOVIT_EFFECT_CHAIN_H)