From 5761ae9d2c9a0654778fa48cbe1815ce8ff4b81c Mon Sep 17 00:00:00 2001 From: David Rose Date: Fri, 3 Dec 2004 00:01:02 +0000 Subject: [PATCH] first pass at MultitexReducer --- panda/src/display/frameBufferProperties.h | 2 +- panda/src/display/graphicsEngine.cxx | 19 +- panda/src/display/graphicsOutput.I | 53 ++ panda/src/display/graphicsOutput.cxx | 119 +++-- panda/src/display/graphicsOutput.h | 17 +- panda/src/display/graphicsWindow.cxx | 2 +- panda/src/display/parasiteBuffer.cxx | 2 +- panda/src/egg2pg/eggLoader.cxx | 14 +- .../glstuff/glGraphicsStateGuardian_src.cxx | 54 +- .../src/glstuff/glGraphicsStateGuardian_src.h | 2 +- panda/src/gobj/texture.I | 17 +- panda/src/gobj/texture.cxx | 10 +- panda/src/gobj/texture.h | 1 + panda/src/grutil/Sources.pp | 3 +- panda/src/grutil/multitexReducer.I | 50 ++ panda/src/grutil/multitexReducer.cxx | 486 ++++++++++++++++++ panda/src/grutil/multitexReducer.h | 119 +++++ panda/src/pgraph/sceneGraphReducer.cxx | 7 +- panda/src/pgraph/transformState.cxx | 5 +- panda/src/testbed/pview.cxx | 23 + 20 files changed, 910 insertions(+), 95 deletions(-) create mode 100644 panda/src/grutil/multitexReducer.I create mode 100644 panda/src/grutil/multitexReducer.cxx create mode 100644 panda/src/grutil/multitexReducer.h diff --git a/panda/src/display/frameBufferProperties.h b/panda/src/display/frameBufferProperties.h index 7a7f65eb56..cde8492394 100644 --- a/panda/src/display/frameBufferProperties.h +++ b/panda/src/display/frameBufferProperties.h @@ -38,7 +38,6 @@ PUBLISHED: INLINE bool operator != (const FrameBufferProperties &other) const; enum FrameBufferMode { - FM_rgba = 0x0000, FM_rgb = 0x0000, FM_index = 0x0001, FM_single_buffer = 0x0000, @@ -47,6 +46,7 @@ PUBLISHED: FM_buffer = 0x0006, // == (FM_single_buffer | FM_double_buffer | FM_triple_buffer) FM_accum = 0x0008, FM_alpha = 0x0010, + FM_rgba = 0x0010, // == (FM_rgb | FM_alpha) FM_depth = 0x0020, FM_stencil = 0x0040, FM_multisample = 0x0080, diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index b997d60b08..068957c9b1 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -383,7 +383,7 @@ remove_all_windows() { GraphicsOutput *win = (*wi); do_remove_window(win); } - + _windows.clear(); _app.do_release(this); @@ -449,6 +449,21 @@ render_frame() { if (_flip_state != FS_flip) { do_flip_frame(); } + + // Are any of the windows ready to be deleted? + Windows new_windows; + new_windows.reserve(_windows.size()); + Windows::iterator wi; + for (wi = _windows.begin(); wi != _windows.end(); ++wi) { + GraphicsOutput *win = (*wi); + if (win->get_delete_flag()) { + do_remove_window(win); + + } else { + new_windows.push_back(win); + } + } + new_windows.swap(_windows); // Grab each thread's mutex again after all windows have flipped. Threads::const_iterator ti; @@ -1303,7 +1318,7 @@ add_window(Windows &wlist, GraphicsOutput *window) { } //////////////////////////////////////////////////////////////////// -// Function: GraphicsEngine::WindowRenderer::remove_window_now +// Function: GraphicsEngine::WindowRenderer::remove_window // Access: Public // Description: Immediately removes the indicated window from all // lists. If the window is currently open and is diff --git a/panda/src/display/graphicsOutput.I b/panda/src/display/graphicsOutput.I index 1a822d7c53..4397849889 100644 --- a/panda/src/display/graphicsOutput.I +++ b/panda/src/display/graphicsOutput.I @@ -160,6 +160,59 @@ is_valid() const { return _is_valid; } +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::set_one_shot +// Access: Published +// Description: Changes the current setting of the one-shot flag. +// When this is true, the GraphicsOutput will +// automatically detach its texture (if it has one) and +// remove itself from the GraphicsEngine after it +// renders the next frame. +//////////////////////////////////////////////////////////////////// +INLINE void GraphicsOutput:: +set_one_shot(bool one_shot) { + _one_shot = one_shot; +} + +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::get_one_shot +// Access: Published +// Description: Returns the current setting of the one-shot flag. +// When this is true, the GraphicsOutput will +// automatically detach its texture (if it has one) and +// remove itself from the GraphicsEngine after it +// renders the next frame. +//////////////////////////////////////////////////////////////////// +INLINE bool GraphicsOutput:: +get_one_shot() const { + return _one_shot; +} + +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::clear_delete_flag +// Access: Published +// Description: Resets the delete flag, so the GraphicsOutput will +// not be automatically deleted before the beginning of +// the next frame. +//////////////////////////////////////////////////////////////////// +INLINE void GraphicsOutput:: +clear_delete_flag() { + _delete_flag = false; +} + +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::get_delete_flag +// Access: Published +// Description: Returns the current setting of the delete flag. When +// this is true, the GraphicsOutput will automatically +// be removed before the beginning of the next frame by +// the GraphicsEngine. +//////////////////////////////////////////////////////////////////// +INLINE bool GraphicsOutput:: +get_delete_flag() const { + return _delete_flag; +} + //////////////////////////////////////////////////////////////////// // Function: GraphicsOutput::get_sort // Access: Published diff --git a/panda/src/display/graphicsOutput.cxx b/panda/src/display/graphicsOutput.cxx index 9fccfa357a..f3acdda96f 100644 --- a/panda/src/display/graphicsOutput.cxx +++ b/panda/src/display/graphicsOutput.cxx @@ -19,6 +19,7 @@ #include "graphicsOutput.h" #include "graphicsPipe.h" #include "graphicsEngine.h" +#include "graphicsWindow.h" #include "config_display.h" #include "mutexHolder.h" #include "hardwareChannel.h" @@ -54,6 +55,9 @@ GraphicsOutput(GraphicsPipe *pipe, GraphicsStateGuardian *gsg, _flip_ready = false; _needs_context = true; _sort = 0; + _active = true; + _one_shot = false; + _delete_flag = false; int mode = gsg->get_properties().get_frame_buffer_mode(); if ((mode & FrameBufferProperties::FM_buffer) == FrameBufferProperties::FM_single_buffer) { @@ -112,6 +116,73 @@ GraphicsOutput:: } } +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::detach_texture +// Access: Published +// Description: Disassociates the texture from the GraphicsOutput. +// It will no longer be filled as the frame renders, and +// it may be used (with its current contents) as a +// texture in its own right. +//////////////////////////////////////////////////////////////////// +void GraphicsOutput:: +detach_texture() { + MutexHolder holder(_lock); + _texture = NULL; + _copy_texture = false; +} + +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::setup_copy_texture +// Access: Published +// Description: Creates a new Texture object, suitable for copying +// the contents of this buffer into, and stores it in +// _texture. This also disassociates the previous +// texture (if any). +//////////////////////////////////////////////////////////////////// +void GraphicsOutput:: +setup_copy_texture(const string &name) { + MutexHolder holder(_lock); + + _texture = new Texture(); + _texture->set_name(name); + _texture->set_wrapu(Texture::WM_clamp); + _texture->set_wrapv(Texture::WM_clamp); + + // We should match the texture format up with the framebuffer + // format. Easier said than done! + if (_gsg != (GraphicsStateGuardian *)NULL) { + int mode = _gsg->get_properties().get_frame_buffer_mode(); + PixelBuffer *pb = _texture->_pbuffer; + + if (mode & FrameBufferProperties::FM_alpha) { + pb->set_format(PixelBuffer::F_rgba8); + pb->set_num_components(4); + pb->set_component_width(1); + pb->set_image_type(PixelBuffer::T_unsigned_byte); + + } else { + pb->set_format(PixelBuffer::F_rgb8); + pb->set_num_components(3); + pb->set_component_width(1); + pb->set_image_type(PixelBuffer::T_unsigned_byte); + } + } + + _copy_texture = true; +} + +//////////////////////////////////////////////////////////////////// +// Function: GraphicsOutput::set_active +// Access: Published +// Description: Sets the active flag associated with the +// GraphicsOutput. If the GraphicsOutput is marked +// inactive, nothing is rendered. +//////////////////////////////////////////////////////////////////// +void GraphicsOutput:: +set_active(bool active) { + _active = active; +} + //////////////////////////////////////////////////////////////////// // Function: GraphicsOutput::is_active // Access: Published, Virtual @@ -120,7 +191,7 @@ GraphicsOutput:: //////////////////////////////////////////////////////////////////// bool GraphicsOutput:: is_active() const { - return is_valid(); + return _active && is_valid(); } //////////////////////////////////////////////////////////////////// @@ -571,6 +642,15 @@ end_frame() { if (!_gsg->get_properties().is_single_buffered()) { _flip_ready = true; } + + if (_one_shot && !show_buffers) { + // In one-shot mode, we request the GraphicsEngine to delete the + // window after we have rendered a frame. But when show-buffers + // mode is enabled, we don't do this, to give the user a chance to + // see the output. + _active = false; + _delete_flag = true; + } } //////////////////////////////////////////////////////////////////// @@ -684,43 +764,6 @@ declare_channel(int index, GraphicsChannel *chan) { _channels[index] = chan; } -//////////////////////////////////////////////////////////////////// -// Function: GraphicsOutput::setup_copy_texture -// Access: Protected -// Description: Creates a new Texture object, suitable for copying -// the contents of this buffer into, and stores it in -// _texture. -//////////////////////////////////////////////////////////////////// -void GraphicsOutput:: -setup_copy_texture(const string &name) { - _texture = new Texture(); - _texture->set_name(name); - _texture->set_wrapu(Texture::WM_clamp); - _texture->set_wrapv(Texture::WM_clamp); - - // We should match the texture format up with the framebuffer - // format. Easier said than done! - if (_gsg != (GraphicsStateGuardian *)NULL) { - int mode = _gsg->get_properties().get_frame_buffer_mode(); - PixelBuffer *pb = _texture->_pbuffer; - - if (mode & FrameBufferProperties::FM_alpha) { - pb->set_format(PixelBuffer::F_rgba8); - pb->set_num_components(4); - pb->set_component_width(1); - pb->set_image_type(PixelBuffer::T_unsigned_byte); - - } else { - pb->set_format(PixelBuffer::F_rgb8); - pb->set_num_components(3); - pb->set_component_width(1); - pb->set_image_type(PixelBuffer::T_unsigned_byte); - } - } - - _copy_texture = true; -} - //////////////////////////////////////////////////////////////////// // Function: GraphicsOutput::do_determine_display_regions // Access: Private diff --git a/panda/src/display/graphicsOutput.h b/panda/src/display/graphicsOutput.h index 4fc8551389..68c6f94771 100644 --- a/panda/src/display/graphicsOutput.h +++ b/panda/src/display/graphicsOutput.h @@ -75,16 +75,25 @@ PUBLISHED: INLINE bool has_texture() const; INLINE Texture *get_texture() const; + void detach_texture(); + void setup_copy_texture(const string &name); INLINE int get_x_size() const; INLINE int get_y_size() const; INLINE bool has_size() const; INLINE bool is_valid() const; + void set_active(bool active); virtual bool is_active() const; - INLINE int get_sort() const; + INLINE void set_one_shot(bool one_shot); + INLINE bool get_one_shot() const; + + INLINE void clear_delete_flag(); + INLINE bool get_delete_flag() const; + void set_sort(int sort); + INLINE int get_sort() const; GraphicsChannel *get_channel(int index); void remove_channel(int index); @@ -145,7 +154,6 @@ public: protected: void declare_channel(int index, GraphicsChannel *chan); - void setup_copy_texture(const string &name); protected: PT(GraphicsStateGuardian) _gsg; @@ -162,6 +170,11 @@ private: int _sort; +protected: + bool _active; + bool _one_shot; + bool _delete_flag; + protected: Mutex _lock; // protects _channels, _display_regions. diff --git a/panda/src/display/graphicsWindow.cxx b/panda/src/display/graphicsWindow.cxx index 1dd110f57b..8e0018102f 100644 --- a/panda/src/display/graphicsWindow.cxx +++ b/panda/src/display/graphicsWindow.cxx @@ -160,7 +160,7 @@ request_properties(const WindowProperties &requested_properties) { bool GraphicsWindow:: is_active() const { // Make this smarter? - return _properties.get_open() && !_properties.get_minimized(); + return _active && _properties.get_open() && !_properties.get_minimized(); } //////////////////////////////////////////////////////////////////// diff --git a/panda/src/display/parasiteBuffer.cxx b/panda/src/display/parasiteBuffer.cxx index b1317e5fa0..69a27c7459 100644 --- a/panda/src/display/parasiteBuffer.cxx +++ b/panda/src/display/parasiteBuffer.cxx @@ -73,7 +73,7 @@ ParasiteBuffer:: //////////////////////////////////////////////////////////////////// bool ParasiteBuffer:: is_active() const { - return _host->is_active(); + return _active && _host->is_active(); } //////////////////////////////////////////////////////////////////// diff --git a/panda/src/egg2pg/eggLoader.cxx b/panda/src/egg2pg/eggLoader.cxx index 7b8ea9f2f5..aa5d34fb30 100644 --- a/panda/src/egg2pg/eggLoader.cxx +++ b/panda/src/egg2pg/eggLoader.cxx @@ -3109,7 +3109,19 @@ apply_tex_mat(CPT(RenderAttrib) tex_mat_attrib, tex_mat(1, 0), tex_mat(1, 1), 0.0f, tex_mat(1, 2), 0.0f, 0.0f, 1.0f, 0.0f, tex_mat(2, 0), tex_mat(2, 1), 0.0f, tex_mat(2, 2)); - CPT(TransformState) transform = TransformState::make_mat(mat4); + CPT(TransformState) transform; + + LVecBase3f scale, shear, hpr, translate; + if (decompose_matrix(mat4, scale, shear, hpr, translate)) { + // If the texture matrix can be represented componentwise, do + // so. + transform = TransformState::make_pos_hpr_scale_shear + (translate, hpr, scale, shear); + + } else { + // Otherwise, make a matrix transform. + transform = TransformState::make_mat(mat4); + } if (tex_mat_attrib == (const RenderAttrib *)NULL) { tex_mat_attrib = TexMatrixAttrib::make(); diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index d4a2ded63b..fbfc6ecbf7 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -2030,27 +2030,7 @@ copy_texture(Texture *tex, const DisplayRegion *dr) { nassertv(tex != NULL && dr != NULL); int xo, yo, w, h; - -#if 0 - // Determine the size of the grab from the given display region - // If the requested region is not a power of two, grab a region that is - // a power of two that contains the requested region - int req_w, req_h; - dr->get_region_pixels(xo, yo, req_w, req_h); - w = binary_log_cap(req_w); - h = binary_log_cap(req_h); - if (w != req_w || h != req_h) { - tex->_requested_w = req_w; - tex->_requested_h = req_h; - tex->_has_requested_size = true; - } -#else - // doing the above is bad unless you also provide some way - // for the caller to adjust his texture coordinates accordingly - // this was done for 'draw_texture' but not for anything else - dr->get_region_pixels(xo, yo, w, h); -#endif PixelBuffer *pb = tex->_pbuffer; pb->set_xsize(w); @@ -3230,8 +3210,7 @@ specify_texture(Texture *tex) { Texture::FilterType minfilter = tex->get_minfilter(); Texture::FilterType magfilter = tex->get_magfilter(); - - bool uses_mipmaps = tex->uses_mipmaps(); + bool uses_mipmaps = tex->uses_mipmaps() && !CLP(ignore_mipmaps); #ifndef NDEBUG if (CLP(force_mipmaps)) { @@ -3240,11 +3219,25 @@ specify_texture(Texture *tex) { uses_mipmaps = true; } #endif + + if (_supports_generate_mipmap) { + // If the hardware can automatically generate mipmaps, ask it to + // do so now, but only if the texture requires them. + GLP(TexParameteri)(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, uses_mipmaps); + + } else if (!tex->might_have_ram_image()) { + // If the hardware can't automatically generate mipmaps, but it's + // a dynamically generated texture (that is, the RAM image isn't + // available so it didn't pass through the CPU), then we'd better + // not try to enable mipmap filtering, since we can't generate + // mipmaps. + uses_mipmaps = false; + } GLP(TexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, - get_texture_filter_type(tex->get_minfilter())); + get_texture_filter_type(minfilter, !uses_mipmaps)); GLP(TexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, - get_texture_filter_type(tex->get_magfilter())); + get_texture_filter_type(magfilter, true)); report_my_gl_errors(); } @@ -3378,10 +3371,7 @@ apply_texture_immediate(CLP(TextureContext) *gtc, Texture *tex) { } else #endif - if (_supports_generate_mipmap) { - GLP(TexParameteri)(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); - - } else { + if (!_supports_generate_mipmap) { // We only need to build the mipmaps by hand if the GL // doesn't support generating them automatically. GLUP(Build2DMipmaps)(GL_TEXTURE_2D, internal_format, @@ -3402,10 +3392,6 @@ apply_texture_immediate(CLP(TextureContext) *gtc, Texture *tex) { report_my_gl_errors(); return true; } - } else { - if (_supports_generate_mipmap) { - GLP(TexParameteri)(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE); - } } GLint border_width = tex->get_border_width(); @@ -3711,11 +3697,11 @@ get_texture_wrap_mode(Texture::WrapMode wm) const { // to GL's. //////////////////////////////////////////////////////////////////// GLenum CLP(GraphicsStateGuardian):: -get_texture_filter_type(Texture::FilterType ft) { +get_texture_filter_type(Texture::FilterType ft, bool ignore_mipmaps) { if (CLP(ignore_filters)) { return GL_NEAREST; - } else if (CLP(ignore_mipmaps)) { + } else if (ignore_mipmaps) { switch (ft) { case Texture::FT_nearest_mipmap_nearest: case Texture::FT_nearest: diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 98f51dbfe7..8165e9f464 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -217,7 +217,7 @@ protected: const RenderBuffer &rb); GLenum get_texture_wrap_mode(Texture::WrapMode wm) const; - static GLenum get_texture_filter_type(Texture::FilterType ft); + static GLenum get_texture_filter_type(Texture::FilterType ft, bool ignore_mipmaps); static GLenum get_image_type(PixelBuffer::Type type); GLint get_external_image_format(PixelBuffer::Format format) const; static GLint get_internal_image_format(PixelBuffer::Format format); diff --git a/panda/src/gobj/texture.I b/panda/src/gobj/texture.I index 80ee66da36..241ae1e1c7 100644 --- a/panda/src/gobj/texture.I +++ b/panda/src/gobj/texture.I @@ -110,7 +110,7 @@ uses_mipmaps() const { // Description: Returns true if the Texture has its image contents // available in main RAM, false if it exists only in // texture memory or in the prepared GSG context. - +// // Note that this has nothing to do with whether // get_ram_image() will fail or not. Even if // has_ram_image() returns false, get_ram_image() may @@ -137,6 +137,21 @@ has_ram_image() const { return !_pbuffer->_image.empty(); } +//////////////////////////////////////////////////////////////////// +// Function: Texture::might_have_ram_image +// Access: Public +// Description: Returns true if the texture's image contents are +// currently available in main RAM, or there is reason +// to believe it can be loaded on demand. That is, this +// function returns a "best guess" as to whether +// get_ram_image() will succeed without actually calling +// it first. +//////////////////////////////////////////////////////////////////// +INLINE bool Texture:: +might_have_ram_image() const { + return (has_ram_image() || has_filename()); +} + //////////////////////////////////////////////////////////////////// // Function: Texture::set_keep_ram_image // Access: Public diff --git a/panda/src/gobj/texture.cxx b/panda/src/gobj/texture.cxx index 60d7a90433..86ad0d4db0 100644 --- a/panda/src/gobj/texture.cxx +++ b/panda/src/gobj/texture.cxx @@ -619,12 +619,16 @@ release_all() { // false return value from has_ram_image() indicates // only that get_ram_image() may need to reload the // texture from disk, which it will do automatically. +// However, you can call might_have_ram_image(), which +// will return true if the ram image exists, or there is +// a reasonable reason to believe it can be loaded. // // On the other hand, it is possible that the texture // cannot be found on disk or is otherwise unavailable. -// If that happens, this function returns NULL. There -// is no way to predict whether get_ram_image() will -// return NULL without calling it first. +// If that happens, this function will return NULL. +// There is no way to predict with 100% accuracy whether +// get_ram_image() will return NULL without calling it +// first; might_have_ram_image() is the closest. //////////////////////////////////////////////////////////////////// PixelBuffer *Texture:: get_ram_image() { diff --git a/panda/src/gobj/texture.h b/panda/src/gobj/texture.h index 124ee2cb56..e968e3b1f4 100644 --- a/panda/src/gobj/texture.h +++ b/panda/src/gobj/texture.h @@ -116,6 +116,7 @@ public: int release_all(); INLINE bool has_ram_image() const; + INLINE bool might_have_ram_image() const; PixelBuffer *get_ram_image(); INLINE void set_keep_ram_image(bool keep_ram_image); INLINE bool get_keep_ram_image() const; diff --git a/panda/src/grutil/Sources.pp b/panda/src/grutil/Sources.pp index 2df7284c32..5d0f401172 100644 --- a/panda/src/grutil/Sources.pp +++ b/panda/src/grutil/Sources.pp @@ -12,7 +12,8 @@ cardMaker.I cardMaker.h \ config_grutil.h \ frameRateMeter.I frameRateMeter.h \ - lineSegs.I lineSegs.h + lineSegs.I lineSegs.h \ + multitexReducer.I multitexReducer.h multitexReducer.cxx #define INCLUDED_SOURCES \ cardMaker.cxx \ diff --git a/panda/src/grutil/multitexReducer.I b/panda/src/grutil/multitexReducer.I new file mode 100644 index 0000000000..e13bd587be --- /dev/null +++ b/panda/src/grutil/multitexReducer.I @@ -0,0 +1,50 @@ +// Filename: multitexReducer.I +// Created by: drose (30Nov04) +// +//////////////////////////////////////////////////////////////////// +// +// PANDA 3D SOFTWARE +// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved +// +// All use of this software is subject to the terms of the Panda 3d +// Software license. You should have received a copy of this license +// along with this source code; you will also find a current copy of +// the license at http://etc.cmu.edu/panda3d/docs/license/ . +// +// To contact the maintainers of this program write to +// panda3d-general@lists.sourceforge.net . +// +//////////////////////////////////////////////////////////////////// + + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::StageInfo::operator < +// Access: Public +// Description: +//////////////////////////////////////////////////////////////////// +INLINE bool MultitexReducer::StageInfo:: +operator < (const MultitexReducer::StageInfo &other) const { + if (_stage != other._stage) { + return _stage < other._stage; + } + if (_tex != other._tex) { + return _tex < other._tex; + } + if (_tex_mat != other._tex_mat) { + return _tex_mat < other._tex_mat; + } + + return false; +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::GeomInfo::Constructor +// Access: Public +// Description: +//////////////////////////////////////////////////////////////////// +INLINE MultitexReducer::GeomInfo:: +GeomInfo(GeomNode *geom_node, int index) : + _geom_node(geom_node), + _index(index) +{ +} diff --git a/panda/src/grutil/multitexReducer.cxx b/panda/src/grutil/multitexReducer.cxx new file mode 100644 index 0000000000..4a4258bbb1 --- /dev/null +++ b/panda/src/grutil/multitexReducer.cxx @@ -0,0 +1,486 @@ +// Filename: multitexReducer.cxx +// Created by: drose (30Nov04) +// +//////////////////////////////////////////////////////////////////// +// +// PANDA 3D SOFTWARE +// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved +// +// All use of this software is subject to the terms of the Panda 3d +// Software license. You should have received a copy of this license +// along with this source code; you will also find a current copy of +// the license at http://etc.cmu.edu/panda3d/docs/license/ . +// +// To contact the maintainers of this program write to +// panda3d-general@lists.sourceforge.net . +// +//////////////////////////////////////////////////////////////////// + +#include "multitexReducer.h" +#include "pandaNode.h" +#include "geomNode.h" +#include "geom.h" +#include "renderState.h" +#include "transformState.h" +#include "graphicsOutput.h" +#include "graphicsChannel.h" +#include "graphicsLayer.h" +#include "displayRegion.h" +#include "nodePath.h" +#include "camera.h" +#include "orthographicLens.h" +#include "cardMaker.h" +#include "colorBlendAttrib.h" +#include "config_grutil.h" +#include "dcast.h" + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::Constructor +// Access: Published +// Description: +//////////////////////////////////////////////////////////////////// +MultitexReducer:: +MultitexReducer() { + _target_stage = TextureStage::get_default(); +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::Destructor +// Access: Published +// Description: +//////////////////////////////////////////////////////////////////// +MultitexReducer:: +~MultitexReducer() { +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::clear +// Access: Published +// Description: Removes the record of nodes that were previously +// discovered by scan(). +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +clear() { + _stages.clear(); +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::scan +// Access: Published +// Description: Starts scanning the hierarchy beginning at the +// indicated node. Any GeomNodes discovered in the +// hierarchy with multitexture will be added to internal +// structures in the MultitexReducer so that a future +// call to flatten() will operate on all of these at +// once. +// +// The indicated transform and state are the state +// inherited from the node's ancestors; any multitexture +// operations will be accumulated from the indicated +// starting state. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +scan(PandaNode *node, const RenderState *state, const TransformState *transform) { + CPT(RenderState) next_state = node->get_state()->compose(state); + CPT(TransformState) next_transform = node->get_transform()->compose(transform); + + if (node->is_geom_node()) { + scan_geom_node(DCAST(GeomNode, node), next_state, next_transform); + } + + PandaNode::Children cr = node->get_children(); + int num_children = cr.get_num_children(); + for (int i = 0; i < num_children; i++) { + scan(cr.get_child(i), next_state, next_transform); + } +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::set_target +// Access: Published +// Description: Specifies the target TextureStage (and TexCoordName) +// that will be left on each multitexture node after the +// flatten operation has completed. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +set_target(TextureStage *stage) { + _target_stage = stage; +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::flatten +// Access: Published +// Description: Actually performs the reducing operations on the +// nodes that were previously scanned. +// +// A window that can be used to create texture buffers +// suitable for rendering this geometry must be +// supplied. This specifies the particular GSG that +// will be used to composite the textures. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +flatten(GraphicsOutput *window) { + if (grutil_cat.is_debug()) { + grutil_cat.debug() + << "Beginning flatten operation\n"; + Stages::const_iterator mi; + for (mi = _stages.begin(); mi != _stages.end(); ++mi) { + const StageList &stage_list = (*mi).first; + const GeomList &geom_list = (*mi).second; + grutil_cat.debug(false) + << "stage_list for:"; + for (GeomList::const_iterator gi = geom_list.begin(); + gi != geom_list.end(); + ++gi) { + const GeomInfo &geom_info = (*gi); + grutil_cat.debug(false) + << " (" << geom_info._geom_node->get_name() << " g" + << geom_info._index << ")"; + } + grutil_cat.debug(false) << ":\n"; + + StageList::const_iterator si; + for (si = stage_list.begin(); si != stage_list.end(); ++si) { + const StageInfo &stage_info = (*si); + grutil_cat.debug(false) + << " " << *stage_info._stage << " " << *stage_info._tex + << " " << *stage_info._tex_mat << "\n"; + } + } + } + + Stages::const_iterator mi; + for (mi = _stages.begin(); mi != _stages.end(); ++mi) { + const StageList &stage_list = (*mi).first; + const GeomList &geom_list = (*mi).second; + + // Create an offscreen buffer in which to render the new texture. + int x_size, y_size, aniso_degree; + Texture::FilterType minfilter, magfilter; + determine_size(x_size, y_size, aniso_degree, + minfilter, magfilter, stage_list); + + GraphicsOutput *buffer = + window->make_texture_buffer("multitex", x_size, y_size); + buffer->set_one_shot(true); + Texture *tex = buffer->get_texture(); + tex->set_anisotropic_degree(aniso_degree); + tex->set_minfilter(minfilter); + tex->set_magfilter(magfilter); + + // Set up the offscreen buffer to render 0,0 to 1,1. This will be + // the whole texture, but nothing outside the texture. + GraphicsChannel *chan = buffer->get_channel(0); + GraphicsLayer *layer = chan->make_layer(0); + DisplayRegion *dr = layer->make_display_region(); + PT(Camera) cam_node = new Camera("multitexCam"); + PT(Lens) lens = new OrthographicLens(); + lens->set_film_size(1.0f, 1.0f); + lens->set_film_offset(0.5f, 0.5f); + lens->set_near_far(-1000.0f, 1000.0f); + cam_node->set_lens(lens); + + // Create a root node for the buffer's scene graph, and set up + // some appropriate properties for it. + NodePath render("buffer"); + cam_node->set_scene(render); + render.set_bin("unsorted", 0); + render.set_depth_test(false); + render.set_depth_write(false); + + NodePath cam = render.attach_new_node(cam_node); + dr->set_camera(cam); + + // Put one plain white card in the background for the first + // texture layer to apply onto. + CardMaker cm("background"); + cm.set_frame(0.0f, 1.0f, 0.0f, 1.0f); + render.attach_new_node(cm.generate()); + + StageList::const_iterator si; + for (si = stage_list.begin(); si != stage_list.end(); ++si) { + const StageInfo &stage_info = (*si); + + make_texture_layer(render, stage_info, geom_list); + } + + // Now modify the geometry to apply the new texture, instead of + // the old multitexture. + CPT(RenderAttrib) new_ta = DCAST(TextureAttrib, TextureAttrib::make())-> + add_on_stage(_target_stage, tex); + + GeomList::const_iterator gi; + for (gi = geom_list.begin(); gi != geom_list.end(); ++gi) { + const GeomInfo &geom_info = (*gi); + + CPT(RenderState) geom_state = + geom_info._geom_node->get_geom_state(geom_info._index); + geom_state = geom_state->add_attrib(new_ta); + geom_info._geom_node->set_geom_state(geom_info._index, geom_state); + } + } +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::scan_geom_node +// Access: Private +// Description: Adds the Geoms in the indicated GeomNode to the +// internal database of multitexture elements. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +scan_geom_node(GeomNode *node, const RenderState *state, + const TransformState *transform) { + if (grutil_cat.is_debug()) { + grutil_cat.debug() + << "scan_geom_node(" << *node << ", " << *state << ", " + << *transform << ")\n"; + } + + int num_geoms = node->get_num_geoms(); + for (int gi = 0; gi < num_geoms; gi++) { + CPT(RenderState) geom_net_state = + node->get_geom_state(gi)->compose(state); + + // Get out the net TextureAttrib and TexMatrixAttrib from the state. + const RenderAttrib *attrib; + const TextureAttrib *ta = NULL; + + attrib = geom_net_state->get_attrib(TextureAttrib::get_class_type()); + if (attrib != (const RenderAttrib *)NULL) { + ta = DCAST(TextureAttrib, attrib); + } + + if (ta != (TextureAttrib *)NULL && ta->get_num_on_stages() >= 2) { + // Ok, we have multitexture. Record the Geom. + CPT(TexMatrixAttrib) tma = DCAST(TexMatrixAttrib, TexMatrixAttrib::make()); + attrib = geom_net_state->get_attrib(TexMatrixAttrib::get_class_type()); + if (attrib != (const RenderAttrib *)NULL) { + tma = DCAST(TexMatrixAttrib, attrib); + } + + StageList stage_list; + + int num_stages = ta->get_num_on_stages(); + for (int si = 0; si < num_stages; si++) { + TextureStage *stage = ta->get_on_stage(si); + + stage_list.push_back(StageInfo(stage, ta, tma)); + } + + record_stage_list(stage_list, GeomInfo(node, gi)); + } + } +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::record_stage_list +// Access: Private +// Description: Adds the record of this one Geom and its associated +// StageList. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +record_stage_list(const MultitexReducer::StageList &stage_list, + const MultitexReducer::GeomInfo &geom_info) { + if (grutil_cat.is_debug()) { + grutil_cat.debug() + << "record_stage_list for " << geom_info._geom_node->get_name() << " g" + << geom_info._index << ":\n"; + StageList::const_iterator si; + for (si = stage_list.begin(); si != stage_list.end(); ++si) { + const StageInfo &stage_info = (*si); + grutil_cat.debug(false) + << " " << *stage_info._stage << " " << *stage_info._tex + << " " << *stage_info._tex_mat << "\n"; + } + } + + _stages[stage_list].push_back(geom_info); +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::determine_size +// Access: Private +// Description: Tries to guess what size to make the new, collapsed +// texture based on the sizes of all of the textures +// used in the stage_list. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +determine_size(int &x_size, int &y_size, int &aniso_degree, + Texture::FilterType &minfilter, Texture::FilterType &magfilter, + const MultitexReducer::StageList &stage_list) const { + x_size = 0; + y_size = 0; + aniso_degree = 0; + minfilter = Texture::FT_nearest; + magfilter = Texture::FT_nearest; + + StageList::const_iterator si; + for (si = stage_list.begin(); si != stage_list.end(); ++si) { + const StageInfo &stage_info = (*si); + Texture *tex = stage_info._tex; + PixelBuffer *pbuffer = tex->_pbuffer; + + if (stage_info._stage == _target_stage) { + // If we find the target stage, use that. + x_size = pbuffer->get_xsize(); + y_size = pbuffer->get_ysize(); + aniso_degree = tex->get_anisotropic_degree(); + minfilter = tex->get_minfilter(); + magfilter = tex->get_magfilter(); + return; + } + + // If we never run across the target stage, just use the maximum + // of all encountered textures. + x_size = max(x_size, pbuffer->get_xsize()); + y_size = max(y_size, pbuffer->get_ysize()); + aniso_degree = max(aniso_degree, tex->get_anisotropic_degree()); + minfilter = (Texture::FilterType)max((int)minfilter, (int)tex->get_minfilter()); + magfilter = (Texture::FilterType)max((int)magfilter, (int)tex->get_magfilter()); + } +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::make_texture_layer +// Access: Private +// Description: Creates geometry to render the texture into the +// offscreen buffer using the same effects that were +// requested by its multitexture specification. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +make_texture_layer(const NodePath &render, + const MultitexReducer::StageInfo &stage_info, + const MultitexReducer::GeomList &geom_list) { + CPT(RenderAttrib) cba; + + switch (stage_info._stage->get_mode()) { + case TextureStage::M_modulate: + cba = ColorBlendAttrib::make + (ColorBlendAttrib::M_add, ColorBlendAttrib::O_fbuffer_color, + ColorBlendAttrib::O_zero); + break; + + case TextureStage::M_decal: + cba = ColorBlendAttrib::make + (ColorBlendAttrib::M_add, ColorBlendAttrib::O_incoming_alpha, + ColorBlendAttrib::O_one_minus_incoming_alpha); + break; + + case TextureStage::M_blend: + cba = ColorBlendAttrib::make + (ColorBlendAttrib::M_add, ColorBlendAttrib::O_constant_color, + ColorBlendAttrib::O_one_minus_incoming_color, + stage_info._stage->get_color()); + break; + + case TextureStage::M_replace: + cba = ColorBlendAttrib::make_off(); + break; + + case TextureStage::M_add: + cba = ColorBlendAttrib::make + (ColorBlendAttrib::M_add, ColorBlendAttrib::O_one, + ColorBlendAttrib::O_one); + break; + + case TextureStage::M_combine: + // We don't support the texture combiner here right now. For now, + // this is the same as modulate. + cba = ColorBlendAttrib::make + (ColorBlendAttrib::M_add, ColorBlendAttrib::O_fbuffer_color, + ColorBlendAttrib::O_zero); + break; + } + + NodePath geom; + + if (stage_info._stage->get_texcoord_name() == _target_stage->get_texcoord_name()) { + // If this TextureStage uses the target texcoords, we can just + // generate a simple card the fills the entire buffer. + CardMaker cm(stage_info._tex->get_name()); + cm.set_uv_range(TexCoordf(0.0f, 0.0f), TexCoordf(1.0f, 1.0f)); + cm.set_has_uvs(true); + cm.set_frame(0.0f, 1.0f, 0.0f, 1.0f); + + geom = render.attach_new_node(cm.generate()); + + } else { + // If this TextureStage uses some other texcoords, we have to + // generate geometry that maps the texcoords to the target space. + // This will work only for very simple cases where the geometry is + // not too extensive and doesn't repeat over the same UV's. + PT(GeomNode) geom_node = new GeomNode(stage_info._tex->get_name()); + transfer_geom(geom_node, stage_info._stage->get_texcoord_name(), + geom_list); + + geom = render.attach_new_node(geom_node); + + // Make sure we override the vertex color, so we don't pollute the + // texture with geometry color. + geom.set_color(Colorf(1.0f, 1.0f, 1.0f, 1.0f)); + } + + if (!stage_info._tex_mat->is_identity()) { + geom.set_tex_transform(TextureStage::get_default(), stage_info._tex_mat); + } + + geom.set_texture(stage_info._tex); + geom.node()->set_attrib(cba); +} + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::transfer_geom +// Access: Private +// Description: Copy the vertices from the indicated geom_list, +// mapping the vertex coordinates so that the geometry +// will render the appropriate distortion on the texture +// to map UV's from the specified set of texture +// coordinates to the target set. +//////////////////////////////////////////////////////////////////// +void MultitexReducer:: +transfer_geom(GeomNode *geom_node, const TexCoordName *texcoord_name, + const MultitexReducer::GeomList &geom_list) { + GeomList::const_iterator gi; + for (gi = geom_list.begin(); gi != geom_list.end(); ++gi) { + const GeomInfo &geom_info = (*gi); + + PT(Geom) geom = + geom_info._geom_node->get_geom(geom_info._index)->make_copy(); + + PTA_Vertexf coords = PTA_Vertexf::empty_array(0); + PTA_TexCoordf texcoords = geom->get_texcoords_array(_target_stage->get_texcoord_name()); + if (!texcoords.empty()) { + coords.reserve(texcoords.size()); + for (size_t i = 0; i < texcoords.size(); i++) { + const TexCoordf &tc = texcoords[i]; + Vertexf v(tc[0], 0.0f, tc[1]); + coords.push_back(v); + } + + geom->set_coords(coords, geom->get_texcoords_index(_target_stage->get_texcoord_name())); + geom->set_texcoords(TexCoordName::get_default(), + geom->get_texcoords_array(texcoord_name), + geom->get_texcoords_index(texcoord_name)); + + geom_node->add_geom(geom); + } + } +} + + +//////////////////////////////////////////////////////////////////// +// Function: MultitexReducer::StageInfo::Constructor +// Access: Public +// Description: +//////////////////////////////////////////////////////////////////// +MultitexReducer::StageInfo:: +StageInfo(TextureStage *stage, const TextureAttrib *ta, + const TexMatrixAttrib *tma) : + _stage(stage), + _tex_mat(TransformState::make_identity()) +{ + _tex = ta->get_on_texture(_stage); + if (tma->has_stage(stage)) { + _tex_mat = tma->get_transform(stage); + } +} diff --git a/panda/src/grutil/multitexReducer.h b/panda/src/grutil/multitexReducer.h new file mode 100644 index 0000000000..c5620bed53 --- /dev/null +++ b/panda/src/grutil/multitexReducer.h @@ -0,0 +1,119 @@ +// Filename: multitexReducer.h +// Created by: drose (30Nov04) +// +//////////////////////////////////////////////////////////////////// +// +// PANDA 3D SOFTWARE +// Copyright (c) 2001 - 2004, Disney Enterprises, Inc. All rights reserved +// +// All use of this software is subject to the terms of the Panda 3d +// Software license. You should have received a copy of this license +// along with this source code; you will also find a current copy of +// the license at http://etc.cmu.edu/panda3d/docs/license/ . +// +// To contact the maintainers of this program write to +// panda3d-general@lists.sourceforge.net . +// +//////////////////////////////////////////////////////////////////// + +#ifndef MULTITEXREDUCER_H +#define MULTITEXREDUCER_H + +#include "pandabase.h" +#include "texture.h" +#include "textureAttrib.h" +#include "textureStage.h" +#include "texMatrixAttrib.h" +#include "transformState.h" +#include "geomNode.h" +#include "luse.h" +#include "pointerTo.h" +#include "pmap.h" +#include "pvector.h" + +class GraphicsOutput; +class PandaNode; +class RenderState; +class TransformState; + +/////////////////////////////////////////////////////////////////// +// Class : MultitexReducer +// Description : This object presents an interface for generating new +// texture images that represent the combined images +// from one or more individual textures, reproducing +// certain kinds of multitexture effects without +// depending on multitexture support in the hardware. +// +// This also flattens out texture matrices and removes +// extra texture coordinates from the Geoms. It is thus +// not a complete substitute for true multitexturing, +// because it does not lend itself well to dynamic +// animation of the textures once they have been +// flattened. It is, however, useful for "baking in" a +// particular multitexture effect. +//////////////////////////////////////////////////////////////////// +class EXPCL_PANDA MultitexReducer { +PUBLISHED: + MultitexReducer(); + ~MultitexReducer(); + + void clear(); + void scan(PandaNode *node, const RenderState *state, + const TransformState *transform); + + void set_target(TextureStage *stage); + + void flatten(GraphicsOutput *window); + +private: + class StageInfo { + public: + StageInfo(TextureStage *stage, const TextureAttrib *ta, + const TexMatrixAttrib *tma); + + INLINE bool operator < (const StageInfo &other) const; + + PT(TextureStage) _stage; + PT(Texture) _tex; + CPT(TransformState) _tex_mat; + }; + + typedef pvector StageList; + + class GeomInfo { + public: + INLINE GeomInfo(GeomNode *geom_node, int index); + + PT(GeomNode) _geom_node; + int _index; + }; + + typedef pvector GeomList; + + typedef pmap Stages; + Stages _stages; + + PT(TextureStage) _target_stage; + +private: + void scan_geom_node(GeomNode *node, const RenderState *state, + const TransformState *transform); + + void record_stage_list(const StageList &stage_list, + const GeomInfo &geom_info); + + void determine_size(int &x_size, int &y_size, int &aniso_degree, + Texture::FilterType &minfilter, + Texture::FilterType &magfilter, + const MultitexReducer::StageList &stage_list) const; + + void make_texture_layer(const NodePath &render, + const StageInfo &stage_info, + const GeomList &geom_list); + void transfer_geom(GeomNode *geom_node, const TexCoordName *texcoord_name, + const MultitexReducer::GeomList &geom_list); +}; + +#include "multitexReducer.I" + +#endif diff --git a/panda/src/pgraph/sceneGraphReducer.cxx b/panda/src/pgraph/sceneGraphReducer.cxx index 21fcc5b029..607cb22893 100644 --- a/panda/src/pgraph/sceneGraphReducer.cxx +++ b/panda/src/pgraph/sceneGraphReducer.cxx @@ -18,16 +18,11 @@ #include "sceneGraphReducer.h" #include "config_pgraph.h" -#include "colorAttrib.h" -#include "texMatrixAttrib.h" -#include "colorScaleAttrib.h" #include "accumulatedAttribs.h" -#include "geomNode.h" #include "pointerTo.h" -#include "geom.h" -#include "indent.h" #include "plist.h" +#include "pmap.h" //////////////////////////////////////////////////////////////////// // Function: SceneGraphReducer::flatten diff --git a/panda/src/pgraph/transformState.cxx b/panda/src/pgraph/transformState.cxx index 558bec5dda..2c1066348b 100644 --- a/panda/src/pgraph/transformState.cxx +++ b/panda/src/pgraph/transformState.cxx @@ -205,7 +205,7 @@ TransformState:: bool TransformState:: operator < (const TransformState &other) const { static const int significant_flags = - (F_is_invalid | F_is_identity | F_components_given | F_hpr_given); + (F_is_invalid | F_is_identity | F_components_given | F_hpr_given | F_quat_given); int flags = (_flags & significant_flags); int other_flags = (other._flags & significant_flags); @@ -219,8 +219,7 @@ operator < (const TransformState &other) const { return 0; } - if ((_flags & (F_components_given | F_hpr_given | F_quat_given)) == - (F_components_given | F_hpr_given | F_quat_given)) { + if ((_flags & F_components_given) != 0) { // If the transform was specified componentwise, compare them // componentwise. int c = _pos.compare_to(other._pos); diff --git a/panda/src/testbed/pview.cxx b/panda/src/testbed/pview.cxx index 6758b15aa8..fbdadd22ef 100644 --- a/panda/src/testbed/pview.cxx +++ b/panda/src/testbed/pview.cxx @@ -18,6 +18,8 @@ #include "pandaFramework.h" #include "textNode.h" +#include "multitexReducer.h" +#include "configVariableBool.h" #ifndef HAVE_GETOPT #include "gnu_getopt.h" @@ -29,6 +31,11 @@ PandaFramework framework; +ConfigVariableBool pview_test_hack +("pview-test-hack", false, + "Enable the '0' key in pview to run whatever hacky test happens to be in " + "there right now."); + void event_W(CPT_Event, void *) { // shift-W: open a new window on the same scene. @@ -98,6 +105,19 @@ event_2(CPT_Event event, void *) { } } +void +event_0(CPT_Event event, void *) { + // 0: run hacky test. + MultitexReducer mr; + + NodePath models = framework.get_models(); + mr.scan(models.node(), models.get_net_state(), models.get_net_transform()); + + WindowFramework *wf = framework.get_window(0); + GraphicsWindow *win = wf->get_graphics_window(); + mr.flatten(win); +} + void usage() { cerr << @@ -210,6 +230,9 @@ main(int argc, char *argv[]) { framework.define_key("shift-w", "open a new window", event_W, NULL); framework.define_key("alt-enter", "toggle between window/fullscreen", event_Enter, NULL); framework.define_key("2", "split the window", event_2, NULL); + if (pview_test_hack) { + framework.define_key("0", "run quick hacky test", event_0, NULL); + } framework.main_loop(); framework.report_frame_rate(nout); }