mirror of
https://github.com/panda3d/panda3d.git
synced 2025-10-04 10:54:24 -04:00
2605 lines
87 KiB
C++
2605 lines
87 KiB
C++
/**
|
|
* PANDA 3D SOFTWARE
|
|
* Copyright (c) Carnegie Mellon University. All rights reserved.
|
|
*
|
|
* All use of this software is subject to the terms of the revised BSD
|
|
* license. You should have received a copy of this license along
|
|
* with this source code in a file named "LICENSE."
|
|
*
|
|
* @file graphicsEngine.cxx
|
|
* @author drose
|
|
* @date 2002-02-24
|
|
*/
|
|
|
|
#include "graphicsEngine.h"
|
|
#include "graphicsPipe.h"
|
|
#include "parasiteBuffer.h"
|
|
#include "config_gobj.h"
|
|
#include "config_display.h"
|
|
#include "pipeline.h"
|
|
#include "drawCullHandler.h"
|
|
#include "binCullHandler.h"
|
|
#include "cullResult.h"
|
|
#include "cullTraverser.h"
|
|
#include "clockObject.h"
|
|
#include "pStatTimer.h"
|
|
#include "pStatGPUTimer.h"
|
|
#include "pStatClient.h"
|
|
#include "pStatCollector.h"
|
|
#include "mutexHolder.h"
|
|
#include "reMutexHolder.h"
|
|
#include "lightReMutexHolder.h"
|
|
#include "cullFaceAttrib.h"
|
|
#include "string_utils.h"
|
|
#include "geomCacheManager.h"
|
|
#include "renderState.h"
|
|
#include "transformState.h"
|
|
#include "thread.h"
|
|
#include "pipeline.h"
|
|
#include "throw_event.h"
|
|
#include "bamCache.h"
|
|
#include "cullableObject.h"
|
|
#include "geomVertexArrayData.h"
|
|
#include "vertexDataSaveFile.h"
|
|
#include "vertexDataBook.h"
|
|
#include "vertexDataPage.h"
|
|
#include "config_pgraph.h"
|
|
#include "displayRegionCullCallbackData.h"
|
|
#include "displayRegionDrawCallbackData.h"
|
|
#include "callbackGraphicsWindow.h"
|
|
|
|
#if defined(WIN32)
|
|
#define WINDOWS_LEAN_AND_MEAN
|
|
#include <WinSock2.h>
|
|
#include <wtypes.h>
|
|
#undef WINDOWS_LEAN_AND_MEAN
|
|
#else
|
|
#include <sys/time.h>
|
|
#endif
|
|
|
|
PT(GraphicsEngine) GraphicsEngine::_global_ptr;
|
|
|
|
PStatCollector GraphicsEngine::_wait_pcollector("Wait:Thread sync");
|
|
PStatCollector GraphicsEngine::_cycle_pcollector("App:Cycle");
|
|
PStatCollector GraphicsEngine::_app_pcollector("App:Show code:General");
|
|
PStatCollector GraphicsEngine::_render_frame_pcollector("App:render_frame");
|
|
PStatCollector GraphicsEngine::_do_frame_pcollector("*:do_frame");
|
|
PStatCollector GraphicsEngine::_yield_pcollector("App:Yield");
|
|
PStatCollector GraphicsEngine::_cull_pcollector("Cull");
|
|
PStatCollector GraphicsEngine::_cull_setup_pcollector("Cull:Setup");
|
|
PStatCollector GraphicsEngine::_cull_sort_pcollector("Cull:Sort");
|
|
PStatCollector GraphicsEngine::_draw_pcollector("Draw");
|
|
PStatCollector GraphicsEngine::_sync_pcollector("Draw:Sync");
|
|
PStatCollector GraphicsEngine::_flip_pcollector("Wait:Flip");
|
|
PStatCollector GraphicsEngine::_flip_begin_pcollector("Wait:Flip:Begin");
|
|
PStatCollector GraphicsEngine::_flip_end_pcollector("Wait:Flip:End");
|
|
PStatCollector GraphicsEngine::_transform_states_pcollector("TransformStates");
|
|
PStatCollector GraphicsEngine::_transform_states_unused_pcollector("TransformStates:Unused");
|
|
PStatCollector GraphicsEngine::_render_states_pcollector("RenderStates");
|
|
PStatCollector GraphicsEngine::_render_states_unused_pcollector("RenderStates:Unused");
|
|
PStatCollector GraphicsEngine::_cyclers_pcollector("PipelineCyclers");
|
|
PStatCollector GraphicsEngine::_dirty_cyclers_pcollector("Dirty PipelineCyclers");
|
|
PStatCollector GraphicsEngine::_delete_pcollector("App:Delete");
|
|
|
|
|
|
PStatCollector GraphicsEngine::_sw_sprites_pcollector("SW Sprites");
|
|
PStatCollector GraphicsEngine::_vertex_data_small_pcollector("Vertex Data:Small");
|
|
PStatCollector GraphicsEngine::_vertex_data_independent_pcollector("Vertex Data:Independent");
|
|
PStatCollector GraphicsEngine::_vertex_data_pending_pcollector("Vertex Data:Pending");
|
|
PStatCollector GraphicsEngine::_vertex_data_resident_pcollector("Vertex Data:Resident");
|
|
PStatCollector GraphicsEngine::_vertex_data_compressed_pcollector("Vertex Data:Compressed");
|
|
PStatCollector GraphicsEngine::_vertex_data_unused_disk_pcollector("Vertex Data:Disk:Unused");
|
|
PStatCollector GraphicsEngine::_vertex_data_used_disk_pcollector("Vertex Data:Disk:Used");
|
|
|
|
// These are counted independently by the collision system; we redefine them
|
|
// here so we can reset them at each frame.
|
|
PStatCollector GraphicsEngine::_cnode_volume_pcollector("Collision Volumes:CollisionNode");
|
|
PStatCollector GraphicsEngine::_gnode_volume_pcollector("Collision Volumes:GeomNode");
|
|
PStatCollector GraphicsEngine::_geom_volume_pcollector("Collision Volumes:Geom");
|
|
PStatCollector GraphicsEngine::_node_volume_pcollector("Collision Volumes:PandaNode");
|
|
PStatCollector GraphicsEngine::_volume_pcollector("Collision Volumes:CollisionSolid");
|
|
PStatCollector GraphicsEngine::_test_pcollector("Collision Tests:CollisionSolid");
|
|
PStatCollector GraphicsEngine::_volume_polygon_pcollector("Collision Volumes:CollisionPolygon");
|
|
PStatCollector GraphicsEngine::_test_polygon_pcollector("Collision Tests:CollisionPolygon");
|
|
PStatCollector GraphicsEngine::_volume_plane_pcollector("Collision Volumes:CollisionPlane");
|
|
PStatCollector GraphicsEngine::_test_plane_pcollector("Collision Tests:CollisionPlane");
|
|
PStatCollector GraphicsEngine::_volume_sphere_pcollector("Collision Volumes:CollisionSphere");
|
|
PStatCollector GraphicsEngine::_test_sphere_pcollector("Collision Tests:CollisionSphere");
|
|
PStatCollector GraphicsEngine::_volume_box_pcollector("Collision Volumes:CollisionBox");
|
|
PStatCollector GraphicsEngine::_test_box_pcollector("Collision Tests:CollisionBox");
|
|
PStatCollector GraphicsEngine::_volume_tube_pcollector("Collision Volumes:CollisionTube");
|
|
PStatCollector GraphicsEngine::_test_tube_pcollector("Collision Tests:CollisionTube");
|
|
PStatCollector GraphicsEngine::_volume_inv_sphere_pcollector("Collision Volumes:CollisionInvSphere");
|
|
PStatCollector GraphicsEngine::_test_inv_sphere_pcollector("Collision Tests:CollisionInvSphere");
|
|
PStatCollector GraphicsEngine::_volume_geom_pcollector("Collision Volumes:CollisionGeom");
|
|
PStatCollector GraphicsEngine::_test_geom_pcollector("Collision Tests:CollisionGeom");
|
|
PStatCollector GraphicsEngine::_occlusion_untested_pcollector("Occlusion results:Not tested");
|
|
PStatCollector GraphicsEngine::_occlusion_passed_pcollector("Occlusion results:Visible");
|
|
PStatCollector GraphicsEngine::_occlusion_failed_pcollector("Occlusion results:Occluded");
|
|
PStatCollector GraphicsEngine::_occlusion_tests_pcollector("Occlusion tests");
|
|
|
|
// This is used to keep track of which scenes we have already culled.
|
|
struct CullKey {
|
|
GraphicsStateGuardian *_gsg;
|
|
NodePath _camera;
|
|
int _lens_index;
|
|
};
|
|
|
|
INLINE static bool operator < (const CullKey &a, const CullKey &b) {
|
|
if (a._gsg != b._gsg) {
|
|
return a._gsg < b._gsg;
|
|
}
|
|
if (a._camera != b._camera) {
|
|
return a._camera < b._camera;
|
|
}
|
|
return a._lens_index < b._lens_index;
|
|
}
|
|
|
|
/**
|
|
* Creates a new GraphicsEngine object. The Pipeline is normally left to
|
|
* default to NULL, which indicates the global render pipeline, but it may be
|
|
* any Pipeline you choose.
|
|
*/
|
|
GraphicsEngine::
|
|
GraphicsEngine(Pipeline *pipeline) :
|
|
_pipeline(pipeline),
|
|
_app("app"),
|
|
_lock("GraphicsEngine::_lock"),
|
|
_loaded_textures_lock("GraphicsEngine::_loaded_textures_lock")
|
|
{
|
|
if (_pipeline == (Pipeline *)NULL) {
|
|
_pipeline = Pipeline::get_render_pipeline();
|
|
}
|
|
|
|
_windows_sorted = true;
|
|
_window_sort_index = 0;
|
|
|
|
set_threading_model(GraphicsThreadingModel(threading_model));
|
|
if (!_threading_model.is_default()) {
|
|
display_cat.info()
|
|
<< "Using threading model " << _threading_model << "\n";
|
|
}
|
|
_auto_flip = auto_flip;
|
|
_portal_enabled = false;
|
|
_flip_state = FS_flip;
|
|
|
|
_singular_warning_last_frame = false;
|
|
_singular_warning_this_frame = false;
|
|
}
|
|
|
|
/**
|
|
* Gracefully cleans up the graphics engine and its related threads and
|
|
* windows.
|
|
*/
|
|
GraphicsEngine::
|
|
~GraphicsEngine() {
|
|
#ifdef DO_PSTATS
|
|
if (_app_pcollector.is_started()) {
|
|
_app_pcollector.stop();
|
|
}
|
|
#endif
|
|
|
|
remove_all_windows();
|
|
}
|
|
|
|
/**
|
|
* Specifies how future objects created via make_gsg(), make_buffer(), and
|
|
* make_window() will be threaded. This does not affect any already-created
|
|
* objects.
|
|
*/
|
|
void GraphicsEngine::
|
|
set_threading_model(const GraphicsThreadingModel &threading_model) {
|
|
if (!Thread::is_threading_supported()) {
|
|
if (!threading_model.is_single_threaded()) {
|
|
display_cat.warning()
|
|
<< "Threading model " << threading_model
|
|
<< " requested but threading is not available. Ignoring.\n";
|
|
return;
|
|
}
|
|
}
|
|
|
|
#ifndef THREADED_PIPELINE
|
|
if (!threading_model.is_single_threaded()) {
|
|
display_cat.warning()
|
|
<< "Threading model " << threading_model
|
|
<< " requested but multithreaded render pipelines not enabled in build.\n";
|
|
if (!allow_nonpipeline_threads) {
|
|
display_cat.warning()
|
|
<< "Ignoring requested threading model.\n";
|
|
return;
|
|
}
|
|
display_cat.warning()
|
|
<< "Danger! Creating requested render threads anyway!\n";
|
|
}
|
|
#endif // THREADED_PIPELINE
|
|
ReMutexHolder holder(_lock);
|
|
_threading_model = threading_model;
|
|
}
|
|
|
|
/**
|
|
* Returns the threading model that will be applied to future objects. See
|
|
* set_threading_model().
|
|
*/
|
|
GraphicsThreadingModel GraphicsEngine::
|
|
get_threading_model() const {
|
|
GraphicsThreadingModel result;
|
|
{
|
|
ReMutexHolder holder(_lock);
|
|
result = _threading_model;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// THIS IS THE OLD CODE FOR make_gsg PT(GraphicsStateGuardian) gsg =
|
|
// pipe->make_gsg(properties, share_with);
|
|
|
|
|
|
|
|
/**
|
|
* Creates a new window (or buffer) and returns it. The GraphicsEngine
|
|
* becomes the owner of the window, it will persist at least until
|
|
* remove_window() is called later.
|
|
*
|
|
* If a null pointer is supplied for the gsg, then this routine will create a
|
|
* new gsg.
|
|
*
|
|
* This routine is only called from the app thread.
|
|
*/
|
|
|
|
GraphicsOutput *GraphicsEngine::
|
|
make_output(GraphicsPipe *pipe,
|
|
const string &name, int sort,
|
|
const FrameBufferProperties &fb_prop,
|
|
const WindowProperties &win_prop,
|
|
int flags,
|
|
GraphicsStateGuardian *gsg,
|
|
GraphicsOutput *host) {
|
|
|
|
/*
|
|
* The code here is tricky because the gsg that is passed in might be in the
|
|
* uninitialized state. As a result, pipe::make_output may not be able to
|
|
* tell which DirectX capabilities or OpenGL extensions are supported and
|
|
* which are not. Worse yet, it can't query the API, because that can only be
|
|
* done from the draw thread, and this is the app thread. So here's the
|
|
* workaround: this routine calls pipe::make_output, which returns a "non-
|
|
* certified" window. That means that the pipe doesn't promise that the draw
|
|
* thread will actually succeed in initializing the window. This routine then
|
|
* calls open_windows, which attempts to initialize the window. If
|
|
* open_windows fails to initialize the window, then this routine will ask
|
|
* pipe::make_output to try again, this time using a different set of OpenGL
|
|
* extensions or DirectX capabilities. This is what the "retry" parameter to
|
|
* pipe::make_output is for - it specifies, in an abstract manner, which set
|
|
* of capabiltiesextensions to try. The only problem with this design is that
|
|
* it requires the engine to call open_windows, which is slow. To make things
|
|
* faster, the pipe can choose to "precertify" its creations. If it chooses
|
|
* to do so, this is a guarantee that the windows it returns will not fail in
|
|
* open_windows. However, most graphics pipes will only precertify if you
|
|
* pass them an already-initialized gsg. Long story short, if you want
|
|
* make_output to be fast, use an already-initialized gsg.
|
|
*/
|
|
|
|
// Simplify the input parameters.
|
|
|
|
int x_size = 0, y_size = 0;
|
|
if (win_prop.has_size()) {
|
|
x_size = win_prop.get_x_size();
|
|
y_size = win_prop.get_y_size();
|
|
}
|
|
if ((x_size == 0)&&(y_size == 0)) {
|
|
flags |= GraphicsPipe::BF_size_track_host;
|
|
}
|
|
if (host != 0) {
|
|
host = host->get_host();
|
|
}
|
|
|
|
// If a gsg or host was supplied, and either is not yet initialized, then
|
|
// call open_windows to get both ready. If that fails, give up on using the
|
|
// supplied gsg and host.
|
|
|
|
if (host == (GraphicsOutput *)NULL) {
|
|
if (gsg != (GraphicsStateGuardian*)NULL) {
|
|
if ((!gsg->is_valid())||(gsg->needs_reset())) {
|
|
open_windows();
|
|
}
|
|
if ((!gsg->is_valid())||(gsg->needs_reset())) {
|
|
gsg = NULL;
|
|
}
|
|
}
|
|
} else {
|
|
if ((host->get_gsg()==0)||
|
|
(!host->is_valid())||
|
|
(!host->get_gsg()->is_valid())||
|
|
(host->get_gsg()->needs_reset())) {
|
|
open_windows();
|
|
}
|
|
if ((host->get_gsg()==0)||
|
|
(!host->is_valid())||
|
|
(!host->get_gsg()->is_valid())||
|
|
(host->get_gsg()->needs_reset())) {
|
|
host = NULL;
|
|
gsg = NULL;
|
|
} else {
|
|
gsg = host->get_gsg();
|
|
}
|
|
}
|
|
|
|
// Sanity check everything.
|
|
|
|
nassertr(pipe != (GraphicsPipe *)NULL, NULL);
|
|
if (gsg != (GraphicsStateGuardian *)NULL) {
|
|
nassertr(pipe == gsg->get_pipe(), NULL);
|
|
nassertr(this == gsg->get_engine(), NULL);
|
|
}
|
|
|
|
// Are we really asking for a callback window?
|
|
if ((flags & GraphicsPipe::BF_require_callback_window)!=0) {
|
|
PT(GraphicsStateGuardian) this_gsg = gsg;
|
|
if (this_gsg == (GraphicsStateGuardian *)NULL) {
|
|
// If we don't already have a GSG, we have to ask the pipe to make a new
|
|
// one, unencumbered by window dressing.
|
|
this_gsg = pipe->make_callback_gsg(this);
|
|
}
|
|
if (this_gsg != (GraphicsStateGuardian *)NULL) {
|
|
CallbackGraphicsWindow *window = new CallbackGraphicsWindow(this, pipe, name, fb_prop, win_prop, flags, this_gsg);
|
|
window->_sort = sort;
|
|
do_add_window(window);
|
|
do_add_gsg(window->get_gsg(), pipe);
|
|
display_cat.info() << "Created output of type CallbackGraphicsWindow\n";
|
|
return window;
|
|
}
|
|
|
|
// Couldn't make a callback window, because the pipe wouldn't make an
|
|
// unencumbered GSG.
|
|
return NULL;
|
|
}
|
|
|
|
// Determine if a parasite buffer meets the user's specs.
|
|
|
|
bool can_use_parasite = false;
|
|
if ((host != 0)&&
|
|
((flags&GraphicsPipe::BF_require_window)==0)&&
|
|
((flags&GraphicsPipe::BF_require_callback_window)==0)&&
|
|
((flags&GraphicsPipe::BF_refuse_parasite)==0)&&
|
|
((flags&GraphicsPipe::BF_can_bind_color)==0)&&
|
|
((flags&GraphicsPipe::BF_can_bind_every)==0)&&
|
|
((flags&GraphicsPipe::BF_rtt_cumulative)==0)&&
|
|
((flags&GraphicsPipe::BF_can_bind_layered)==0)) {
|
|
if ((flags&GraphicsPipe::BF_fb_props_optional) ||
|
|
(host->get_fb_properties().subsumes(fb_prop))) {
|
|
can_use_parasite = true;
|
|
}
|
|
}
|
|
|
|
// If parasite buffers are preferred, then try a parasite first. Even if
|
|
// prefer-parasite-buffer is set, parasites are not preferred if the host
|
|
// window is too small, or if the host window does not have the requested
|
|
// properties.
|
|
|
|
if ((prefer_parasite_buffer) &&
|
|
(can_use_parasite) &&
|
|
(x_size <= host->get_x_size())&&
|
|
(y_size <= host->get_y_size())&&
|
|
(host->get_fb_properties().subsumes(fb_prop))) {
|
|
ParasiteBuffer *buffer = new ParasiteBuffer(host, name, x_size, y_size, flags);
|
|
buffer->_sort = sort;
|
|
do_add_window(buffer);
|
|
do_add_gsg(host->get_gsg(), pipe);
|
|
display_cat.info() << "Created output of type ParasiteBuffer\n";
|
|
return buffer;
|
|
}
|
|
|
|
// If force-parasite-buffer is set, we create a parasite buffer even if it's
|
|
// less than ideal. You might set this if you really don't trust your
|
|
// graphics driver's support for offscreen buffers.
|
|
if (force_parasite_buffer && can_use_parasite) {
|
|
ParasiteBuffer *buffer = new ParasiteBuffer(host, name, x_size, y_size, flags);
|
|
buffer->_sort = sort;
|
|
do_add_window(buffer);
|
|
do_add_gsg(host->get_gsg(), pipe);
|
|
display_cat.info() << "Created output of type ParasiteBuffer\n";
|
|
return buffer;
|
|
}
|
|
|
|
// Ask the pipe to create a window.
|
|
|
|
for (int retry=0; retry<10; retry++) {
|
|
bool precertify = false;
|
|
PT(GraphicsOutput) window =
|
|
pipe->make_output(name, fb_prop, win_prop, flags, this, gsg, host, retry, precertify);
|
|
if (window != (GraphicsOutput *)NULL) {
|
|
window->_sort = sort;
|
|
if (precertify && gsg != nullptr && window->get_gsg() == gsg) {
|
|
do_add_window(window);
|
|
display_cat.info()
|
|
<< "Created output of type " << window->get_type() << "\n";
|
|
return window;
|
|
}
|
|
do_add_window(window);
|
|
open_windows();
|
|
if (window->is_valid()) {
|
|
display_cat.info()
|
|
<< "Created output of type " << window->get_type() << "\n";
|
|
|
|
if (window->get_fb_properties().subsumes(fb_prop)) {
|
|
return window;
|
|
} else {
|
|
if (flags & GraphicsPipe::BF_fb_props_optional) {
|
|
display_cat.warning()
|
|
<< "FrameBufferProperties available less than requested.\n";
|
|
display_cat.warning(false)
|
|
<< " requested: " << fb_prop << "\n"
|
|
<< " got: " << window->get_fb_properties() << "\n";
|
|
return window;
|
|
}
|
|
display_cat.error()
|
|
<< "Could not get requested FrameBufferProperties; abandoning window.\n";
|
|
display_cat.error(false)
|
|
<< " requested: " << fb_prop << "\n"
|
|
<< " got: " << window->get_fb_properties() << "\n";
|
|
}
|
|
} else {
|
|
display_cat.info()
|
|
<< window->get_type() << " wouldn't open; abandoning.\n";
|
|
display_cat.debug(false)
|
|
<< " requested: " << fb_prop << "\n";
|
|
}
|
|
|
|
// No good; delete the window and keep trying.
|
|
bool removed = remove_window(window);
|
|
nassertr(removed, NULL);
|
|
}
|
|
}
|
|
|
|
// Parasite buffers were not preferred, but the pipe could not create a
|
|
// window to the user's specs. Try a parasite as a last hope.
|
|
|
|
if (can_use_parasite) {
|
|
ParasiteBuffer *buffer = new ParasiteBuffer(host, name, x_size, y_size, flags);
|
|
buffer->_sort = sort;
|
|
do_add_window(buffer);
|
|
do_add_gsg(host->get_gsg(), pipe);
|
|
display_cat.info() << "Created output of type ParasiteBuffer\n";
|
|
return buffer;
|
|
}
|
|
|
|
// Could not create a window to the user's specs.
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* This can be used to add a newly-created GraphicsOutput object (and its GSG)
|
|
* to the engine's list of windows, and requests that it be opened. This
|
|
* shouldn't be called by user code as make_output normally does this under
|
|
* the hood; it may be useful in esoteric cases in which a custom window
|
|
* object is used.
|
|
*
|
|
* This can be called during the rendering loop, unlike make_output(); the
|
|
* window will be opened before the next frame begins rendering. Because it
|
|
* doesn't call open_windows(), however, it's not guaranteed that the window
|
|
* will succeed opening even if it returns true.
|
|
*/
|
|
bool GraphicsEngine::
|
|
add_window(GraphicsOutput *window, int sort) {
|
|
nassertr(window != nullptr, false);
|
|
nassertr(this == window->get_engine(), false);
|
|
|
|
window->_sort = sort;
|
|
do_add_window(window);
|
|
|
|
display_cat.info()
|
|
<< "Added output of type " << window->get_type() << "\n";
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Removes the indicated window or offscreen buffer from the set of windows
|
|
* that will be processed when render_frame() is called. This also closes the
|
|
* window if it is open, and removes the window from its GraphicsPipe,
|
|
* allowing the window to be destructed if there are no other references to
|
|
* it. (However, the window may not be actually closed until next frame, if
|
|
* it is controlled by a sub-thread.)
|
|
*
|
|
* The return value is true if the window was removed, false if it was not
|
|
* found.
|
|
*
|
|
* Unlike remove_all_windows(), this function does not terminate any of the
|
|
* threads that may have been started to service this window; they are left
|
|
* running (since you might open a new window later on these threads). If
|
|
* your intention is to clean up before shutting down, it is better to call
|
|
* remove_all_windows() then to call remove_window() one at a time.
|
|
*/
|
|
bool GraphicsEngine::
|
|
remove_window(GraphicsOutput *window) {
|
|
nassertr(window != NULL, false);
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
|
|
// First, make sure we know what this window is.
|
|
PT(GraphicsOutput) ptwin = window;
|
|
size_t count;
|
|
{
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
if (!_windows_sorted) {
|
|
do_resort_windows();
|
|
}
|
|
count = _windows.erase(ptwin);
|
|
}
|
|
|
|
// Also check whether it is in _new_windows.
|
|
{
|
|
MutexHolder new_windows_holder(_new_windows_lock, current_thread);
|
|
_new_windows.erase(std::remove(_new_windows.begin(), _new_windows.end(), ptwin));
|
|
}
|
|
|
|
if (count == 0) {
|
|
// Never heard of this window. Do nothing.
|
|
return false;
|
|
}
|
|
|
|
do_remove_window(window, current_thread);
|
|
|
|
GraphicsStateGuardian *gsg = window->get_gsg();
|
|
if (gsg != (GraphicsStateGuardian *)NULL) {
|
|
PreparedGraphicsObjects *pgo = gsg->get_prepared_objects();
|
|
if (pgo != (PreparedGraphicsObjects *)NULL) {
|
|
// Check to see if any other still-active windows share this context.
|
|
bool any_common = false;
|
|
{
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
Windows::iterator wi;
|
|
for (wi = _windows.begin(); wi != _windows.end() && !any_common; ++wi) {
|
|
GraphicsStateGuardian *gsg2 = (*wi)->get_gsg();
|
|
if (gsg2 != (GraphicsStateGuardian *)NULL &&
|
|
gsg2->get_prepared_objects() == pgo) {
|
|
any_common = true;
|
|
}
|
|
}
|
|
}
|
|
if (!any_common) {
|
|
// If no windows still use this context, release all textures, etc.
|
|
// We do this in case there is a floating pointer somewhere keeping
|
|
// the GSG from destructing when its window goes away. A leaked GSG
|
|
// pointer is bad enough, but there's no reason we also need to keep
|
|
// around all of the objects allocated on graphics memory.
|
|
pgo->release_all();
|
|
}
|
|
}
|
|
}
|
|
|
|
nassertr(count == 1, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Removes and closes all windows from the engine. This also cleans up and
|
|
* terminates any threads that have been started to service those windows.
|
|
*/
|
|
void GraphicsEngine::
|
|
remove_all_windows() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
|
|
// 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
|
|
// modify _windows. I don't know what code would be doing this, but it
|
|
// appeared to be happening, and this worked around it.
|
|
Windows old_windows;
|
|
old_windows.swap(_windows);
|
|
Windows::iterator wi;
|
|
for (wi = old_windows.begin(); wi != old_windows.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
nassertv(win != NULL);
|
|
do_remove_window(win, current_thread);
|
|
GraphicsStateGuardian *gsg = win->get_gsg();
|
|
if (gsg != (GraphicsStateGuardian *)NULL) {
|
|
gsg->release_all();
|
|
}
|
|
}
|
|
|
|
{
|
|
MutexHolder new_windows_holder(_new_windows_lock, current_thread);
|
|
_new_windows.clear();
|
|
}
|
|
|
|
_app.do_close(this, current_thread);
|
|
_app.do_pending(this, current_thread);
|
|
terminate_threads(current_thread);
|
|
|
|
// It seems a safe assumption that we're about to exit the application or
|
|
// otherwise shut down Panda. Although it's a bit of a hack, since it's not
|
|
// really related to removing windows, this would nevertheless be a fine
|
|
// time to ensure the model cache (if any) has been flushed to disk.
|
|
BamCache::flush_global_index();
|
|
|
|
// And, hey, let's stop the vertex paging threads, if any.
|
|
VertexDataPage::stop_threads();
|
|
|
|
// Stopping the tasks means we have to release the Python GIL while
|
|
// this method runs (hence it is marked BLOCKING), so that any
|
|
// Python tasks on other threads won't deadlock grabbing the GIL.
|
|
AsyncTaskManager::get_global_ptr()->stop_threads();
|
|
|
|
#ifdef DO_PSTATS
|
|
PStatClient::get_global_pstats()->disconnect();
|
|
#endif
|
|
|
|
// Well, and why not clean up all threads here?
|
|
Thread::prepare_for_exit();
|
|
}
|
|
|
|
/**
|
|
* Resets the framebuffer of the current window. This is currently used by
|
|
* DirectX 8 only. It calls a reset_window function on each active window to
|
|
* release/create old/new framebuffer
|
|
*/
|
|
void GraphicsEngine::
|
|
reset_all_windows(bool swapchain) {
|
|
Windows::iterator wi;
|
|
for (wi = _windows.begin(); wi != _windows.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
// if (win->is_active())
|
|
win->reset_window(swapchain);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are no windows or buffers managed by the engine,
|
|
* false if there is at least one.
|
|
*/
|
|
bool GraphicsEngine::
|
|
is_empty() const {
|
|
return _windows.empty();
|
|
}
|
|
|
|
/**
|
|
* Returns the number of windows (or buffers) managed by the engine.
|
|
*/
|
|
int GraphicsEngine::
|
|
get_num_windows() const {
|
|
return _windows.size();
|
|
}
|
|
|
|
/**
|
|
* Returns the nth window or buffers managed by the engine, in sorted order.
|
|
*/
|
|
GraphicsOutput *GraphicsEngine::
|
|
get_window(int n) const {
|
|
nassertr(n >= 0 && n < (int)_windows.size(), NULL);
|
|
|
|
if (!_windows_sorted) {
|
|
((GraphicsEngine *)this)->do_resort_windows();
|
|
}
|
|
return _windows[n];
|
|
}
|
|
|
|
/**
|
|
* Renders the next frame in all the registered windows, and flips all of the
|
|
* frame buffers.
|
|
*/
|
|
void GraphicsEngine::
|
|
render_frame() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
ReMutexHolder public_holder(_public_lock);
|
|
|
|
// Since this gets called every frame, we should take advantage of the
|
|
// opportunity to flush the cache if necessary.
|
|
BamCache::consider_flush_global_index();
|
|
|
|
// Anything that happens outside of GraphicsEngine::render_frame() is deemed
|
|
// to be App.
|
|
#ifdef DO_PSTATS
|
|
_render_frame_pcollector.start();
|
|
if (_app_pcollector.is_started()) {
|
|
_app_pcollector.stop();
|
|
}
|
|
#endif
|
|
|
|
// Make sure our buffers and windows are fully realized before we render a
|
|
// frame. We do this particularly to realize our offscreen buffers, so
|
|
// that we don't render a frame before the offscreen buffers are ready
|
|
// (which might result in a frame going by without some textures having
|
|
// been rendered).
|
|
open_windows();
|
|
|
|
ClockObject *global_clock = ClockObject::get_global_clock();
|
|
|
|
if (display_cat.is_spam()) {
|
|
display_cat.spam()
|
|
<< "render_frame() - frame " << global_clock->get_frame_count() << "\n";
|
|
}
|
|
|
|
{
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
|
|
if (!_windows_sorted) {
|
|
do_resort_windows();
|
|
}
|
|
|
|
if (sync_flip && _flip_state != FS_flip) {
|
|
do_flip_frame(current_thread);
|
|
}
|
|
|
|
// 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);
|
|
nassertv(win != NULL);
|
|
if (win->get_delete_flag()) {
|
|
do_remove_window(win, current_thread);
|
|
|
|
} else {
|
|
new_windows.push_back(win);
|
|
|
|
// Let's calculate each scene's bounding volume here in App, before we
|
|
// cycle the pipeline. The cull traversal will calculate it anyway,
|
|
// but if we calculate it in App first before it gets calculated in
|
|
// the Cull thread, it will be more likely to stick for subsequent
|
|
// frames, so we won't have to recompute it each frame.
|
|
int num_drs = win->get_num_active_display_regions();
|
|
for (int i = 0; i < num_drs; ++i) {
|
|
DisplayRegion *dr = win->get_active_display_region(i);
|
|
if (dr != (DisplayRegion *)NULL) {
|
|
NodePath camera_np = dr->get_camera(current_thread);
|
|
if (!camera_np.is_empty()) {
|
|
Camera *camera = DCAST(Camera, camera_np.node());
|
|
NodePath scene = camera->get_scene();
|
|
if (scene.is_empty()) {
|
|
scene = camera_np.get_top(current_thread);
|
|
}
|
|
if (!scene.is_empty()) {
|
|
scene.get_bounds(current_thread);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_windows.swap(new_windows);
|
|
|
|
// Go ahead and release any textures' ram images for textures that were
|
|
// drawn in the previous frame.
|
|
{
|
|
MutexHolder holder2(_loaded_textures_lock);
|
|
LoadedTextures::iterator lti;
|
|
for (lti = _loaded_textures.begin(); lti != _loaded_textures.end(); ++lti) {
|
|
LoadedTexture < = (*lti);
|
|
if (lt._tex->get_image_modified() == lt._image_modified) {
|
|
lt._tex->texture_uploaded();
|
|
}
|
|
}
|
|
_loaded_textures.clear();
|
|
}
|
|
|
|
// Now it's time to do any drawing from the main frame--after all of the
|
|
// App code has executed, but before we begin the next frame.
|
|
_app.do_frame(this, current_thread);
|
|
|
|
// Grab each thread's mutex again after all windows have flipped, and wait
|
|
// for the thread to finish.
|
|
{
|
|
PStatTimer timer(_wait_pcollector, current_thread);
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_cv_mutex.acquire();
|
|
|
|
while (thread->_thread_state != TS_wait) {
|
|
thread->_cv_done.wait();
|
|
}
|
|
}
|
|
}
|
|
|
|
#if defined(THREADED_PIPELINE) && defined(DO_PSTATS)
|
|
_cyclers_pcollector.set_level(_pipeline->get_num_cyclers());
|
|
_dirty_cyclers_pcollector.set_level(_pipeline->get_num_dirty_cyclers());
|
|
|
|
#ifdef DEBUG_THREADS
|
|
if (PStatClient::is_connected()) {
|
|
_pipeline->iterate_all_cycler_types(pstats_count_cycler_type, this);
|
|
_pipeline->iterate_dirty_cycler_types(pstats_count_dirty_cycler_type, this);
|
|
}
|
|
#endif // DEBUG_THREADS
|
|
|
|
#endif // THREADED_PIPELINE && DO_PSTATS
|
|
|
|
GeomCacheManager::flush_level();
|
|
CullTraverser::flush_level();
|
|
RenderState::flush_level();
|
|
TransformState::flush_level();
|
|
CullableObject::flush_level();
|
|
|
|
// Now cycle the pipeline and officially begin the next frame.
|
|
#ifdef THREADED_PIPELINE
|
|
{
|
|
PStatTimer timer(_cycle_pcollector, current_thread);
|
|
_pipeline->cycle();
|
|
}
|
|
#endif // THREADED_PIPELINE
|
|
|
|
global_clock->tick(current_thread);
|
|
if (global_clock->check_errors(current_thread)) {
|
|
throw_event("clock_error");
|
|
}
|
|
|
|
#ifdef DO_PSTATS
|
|
PStatClient::main_tick();
|
|
|
|
// Reset our pcollectors that track data across the frame.
|
|
CullTraverser::_nodes_pcollector.clear_level();
|
|
CullTraverser::_geom_nodes_pcollector.clear_level();
|
|
CullTraverser::_geoms_pcollector.clear_level();
|
|
GeomCacheManager::_geom_cache_active_pcollector.clear_level();
|
|
GeomCacheManager::_geom_cache_record_pcollector.clear_level();
|
|
GeomCacheManager::_geom_cache_erase_pcollector.clear_level();
|
|
GeomCacheManager::_geom_cache_evict_pcollector.clear_level();
|
|
|
|
GraphicsStateGuardian::init_frame_pstats();
|
|
|
|
_transform_states_pcollector.set_level(TransformState::get_num_states());
|
|
_render_states_pcollector.set_level(RenderState::get_num_states());
|
|
if (pstats_unused_states) {
|
|
_transform_states_unused_pcollector.set_level(TransformState::get_num_unused_states());
|
|
_render_states_unused_pcollector.set_level(RenderState::get_num_unused_states());
|
|
}
|
|
|
|
_sw_sprites_pcollector.clear_level();
|
|
|
|
_cnode_volume_pcollector.clear_level();
|
|
_gnode_volume_pcollector.clear_level();
|
|
_geom_volume_pcollector.clear_level();
|
|
_node_volume_pcollector.clear_level();
|
|
_volume_pcollector.clear_level();
|
|
_test_pcollector.clear_level();
|
|
_volume_polygon_pcollector.clear_level();
|
|
_test_polygon_pcollector.clear_level();
|
|
_volume_plane_pcollector.clear_level();
|
|
_test_plane_pcollector.clear_level();
|
|
_volume_sphere_pcollector.clear_level();
|
|
_test_sphere_pcollector.clear_level();
|
|
_volume_box_pcollector.clear_level();
|
|
_test_box_pcollector.clear_level();
|
|
_volume_tube_pcollector.clear_level();
|
|
_test_tube_pcollector.clear_level();
|
|
_volume_inv_sphere_pcollector.clear_level();
|
|
_test_inv_sphere_pcollector.clear_level();
|
|
_volume_geom_pcollector.clear_level();
|
|
_test_geom_pcollector.clear_level();
|
|
_occlusion_untested_pcollector.clear_level();
|
|
_occlusion_passed_pcollector.clear_level();
|
|
_occlusion_failed_pcollector.clear_level();
|
|
_occlusion_tests_pcollector.clear_level();
|
|
|
|
if (PStatClient::is_connected()) {
|
|
size_t small_buf = GeomVertexArrayData::get_small_lru()->get_total_size();
|
|
size_t independent = GeomVertexArrayData::get_independent_lru()->get_total_size();
|
|
size_t resident = VertexDataPage::get_global_lru(VertexDataPage::RC_resident)->get_total_size();
|
|
size_t compressed = VertexDataPage::get_global_lru(VertexDataPage::RC_compressed)->get_total_size();
|
|
size_t pending = VertexDataPage::get_pending_lru()->get_total_size();
|
|
|
|
VertexDataSaveFile *save_file = VertexDataPage::get_save_file();
|
|
size_t total_disk = save_file->get_total_file_size();
|
|
size_t used_disk = save_file->get_used_file_size();
|
|
|
|
_vertex_data_small_pcollector.set_level(small_buf);
|
|
_vertex_data_independent_pcollector.set_level(independent);
|
|
_vertex_data_pending_pcollector.set_level(pending);
|
|
_vertex_data_resident_pcollector.set_level(resident);
|
|
_vertex_data_compressed_pcollector.set_level(compressed);
|
|
_vertex_data_unused_disk_pcollector.set_level(total_disk - used_disk);
|
|
_vertex_data_used_disk_pcollector.set_level(used_disk);
|
|
}
|
|
|
|
#endif // DO_PSTATS
|
|
|
|
GeomVertexArrayData::lru_epoch();
|
|
|
|
// Now signal all of our threads to begin their next frame.
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
if (thread->_thread_state == TS_wait) {
|
|
thread->_thread_state = TS_do_frame;
|
|
thread->_cv_start.notify();
|
|
}
|
|
thread->_cv_mutex.release();
|
|
}
|
|
|
|
// Some threads may still be drawing, so indicate that we have to wait for
|
|
// those threads before we can flip.
|
|
_flip_state = _auto_flip ? FS_flip : FS_draw;
|
|
}
|
|
|
|
// Now the lock is released.
|
|
|
|
if (yield_timeslice) {
|
|
// Nap for a moment to yield the timeslice, to be polite to other running
|
|
// applications.
|
|
PStatTimer timer(_yield_pcollector, current_thread);
|
|
Thread::force_yield();
|
|
} else if (!Thread::is_true_threads()) {
|
|
PStatTimer timer(_yield_pcollector, current_thread);
|
|
Thread::consider_yield();
|
|
}
|
|
|
|
// Anything that happens outside of GraphicsEngine::render_frame() is deemed
|
|
// to be App.
|
|
_app_pcollector.start();
|
|
_render_frame_pcollector.stop();
|
|
}
|
|
|
|
|
|
/**
|
|
* Fully opens (or closes) any windows that have recently been requested open
|
|
* or closed, without rendering any frames. It is not necessary to call this
|
|
* explicitly, since windows will be automatically opened or closed when the
|
|
* next frame is rendered, but you may call this if you want your windows now
|
|
* without seeing a frame go by.
|
|
*/
|
|
void GraphicsEngine::
|
|
open_windows() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
|
|
pvector<PT(GraphicsOutput)> new_windows;
|
|
{
|
|
MutexHolder new_windows_holder(_new_windows_lock, current_thread);
|
|
if (_new_windows.empty()) {
|
|
return;
|
|
}
|
|
|
|
for (auto it = _new_windows.begin(); it != _new_windows.end(); ++it) {
|
|
GraphicsOutput *window = *it;
|
|
|
|
WindowRenderer *cull =
|
|
get_window_renderer(_threading_model.get_cull_name(),
|
|
_threading_model.get_cull_stage());
|
|
WindowRenderer *draw =
|
|
get_window_renderer(_threading_model.get_draw_name(),
|
|
_threading_model.get_draw_stage());
|
|
|
|
if (_threading_model.get_cull_sorting()) {
|
|
cull->add_window(cull->_cull, window);
|
|
draw->add_window(draw->_draw, window);
|
|
} else {
|
|
cull->add_window(cull->_cdraw, window);
|
|
}
|
|
|
|
// Ask the pipe which thread it prefers to run its windowing commands in
|
|
// (the "window thread"). This is the thread that handles the commands
|
|
// to open, resize, etc. the window. X requires this to be done in the
|
|
// app thread (along with all the other windows, since X is strictly
|
|
// single-threaded), but Windows requires this to be done in draw
|
|
// (because once an OpenGL context has been bound in a given thread, it
|
|
// cannot subsequently be bound in any other thread, and we have to bind
|
|
// a context in open_window()).
|
|
|
|
switch (window->get_pipe()->get_preferred_window_thread()) {
|
|
case GraphicsPipe::PWT_app:
|
|
_app.add_window(_app._window, window);
|
|
break;
|
|
|
|
case GraphicsPipe::PWT_draw:
|
|
draw->add_window(draw->_window, window);
|
|
break;
|
|
}
|
|
|
|
_windows.push_back(window);
|
|
}
|
|
|
|
// Steal the list, since remove_window() may remove from _new_windows.
|
|
new_windows.swap(_new_windows);
|
|
}
|
|
|
|
do_resort_windows();
|
|
|
|
// We do it twice, to allow both cull and draw to process the window.
|
|
for (int i = 0; i < 2; ++i) {
|
|
_app.do_windows(this, current_thread);
|
|
_app.do_pending(this, current_thread);
|
|
|
|
PStatTimer timer(_wait_pcollector, current_thread);
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_cv_mutex.acquire();
|
|
|
|
while (thread->_thread_state != TS_wait) {
|
|
thread->_cv_done.wait();
|
|
}
|
|
|
|
thread->_thread_state = TS_do_windows;
|
|
thread->_cv_start.notify();
|
|
thread->_cv_mutex.release();
|
|
}
|
|
}
|
|
|
|
// Now go through the list again to check whether they opened successfully.
|
|
for (auto it = new_windows.begin(); it != new_windows.end(); ++it) {
|
|
GraphicsOutput *window = *it;
|
|
if (window->is_valid()) {
|
|
do_add_gsg(window->get_gsg(), window->get_pipe());
|
|
} else {
|
|
remove_window(window);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Waits for all the threads that started drawing their last frame to finish
|
|
* drawing. The windows are not yet flipped when this returns; see also
|
|
* flip_frame(). It is not usually necessary to call this explicitly, unless
|
|
* you need to see the previous frame right away.
|
|
*/
|
|
void GraphicsEngine::
|
|
sync_frame() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
|
|
if (_flip_state == FS_draw) {
|
|
do_sync_frame(current_thread);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Waits for all the threads that started drawing their last frame to finish
|
|
* drawing. Returns when all threads have actually finished drawing, as
|
|
* opposed to 'sync_frame' we seems to return once all draw calls have been
|
|
* submitted. Calling 'flip_frame' after this function should immediately
|
|
* cause a buffer flip. This function will only work in opengl right now, for
|
|
* all other graphics pipelines it will simply return immediately. In opengl
|
|
* it's a bit of a hack: it will attempt to read a single pixel from the frame
|
|
* buffer to force the graphics card to finish drawing before it returns
|
|
*/
|
|
void GraphicsEngine::
|
|
ready_flip() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
|
|
if (_flip_state == FS_draw) {
|
|
do_ready_flip(current_thread);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Waits for all the threads that started drawing their last frame to finish
|
|
* drawing, and then flips all the windows. It is not usually necessary to
|
|
* call this explicitly, unless you need to see the previous frame right away.
|
|
*/
|
|
void GraphicsEngine::
|
|
flip_frame() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
|
|
if (_flip_state != FS_flip) {
|
|
do_flip_frame(current_thread);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asks the indicated GraphicsStateGuardian to retrieve the texture memory
|
|
* image of the indicated texture and store it in the texture's ram_image
|
|
* field. The image can then be written to disk via Texture::write(), or
|
|
* otherwise manipulated on the CPU.
|
|
*
|
|
* This is useful for retrieving the contents of a texture that has been
|
|
* somehow generated on the graphics card, instead of having been loaded the
|
|
* normal way via Texture::read() or Texture::load(). It is particularly
|
|
* useful for getting the data associated with a compressed texture image.
|
|
*
|
|
* Since this requires a round-trip to the draw thread, it may require waiting
|
|
* for the current thread to finish rendering if it is called in a
|
|
* multithreaded environment. However, you can call this several consecutive
|
|
* times on different textures for little additional cost.
|
|
*
|
|
* If the texture has not yet been loaded to the GSG in question, it will be
|
|
* loaded immediately.
|
|
*
|
|
* The return value is true if the operation is successful, false otherwise.
|
|
*/
|
|
bool GraphicsEngine::
|
|
extract_texture_data(Texture *tex, GraphicsStateGuardian *gsg) {
|
|
ReMutexHolder holder(_lock);
|
|
|
|
string draw_name = gsg->get_threading_model().get_draw_name();
|
|
if (draw_name.empty()) {
|
|
// A single-threaded environment. No problem.
|
|
return gsg->extract_texture_data(tex);
|
|
|
|
} else {
|
|
// A multi-threaded environment. We have to wait until the draw thread
|
|
// has finished its current task.
|
|
WindowRenderer *wr = get_window_renderer(draw_name, 0);
|
|
RenderThread *thread = (RenderThread *)wr;
|
|
MutexHolder holder2(thread->_cv_mutex);
|
|
|
|
while (thread->_thread_state != TS_wait) {
|
|
thread->_cv_done.wait();
|
|
}
|
|
|
|
// OK, now the draw thread is idle. That's really good enough for our
|
|
// purposes; we don't *actually* need to make the draw thread do the work
|
|
// --it's sufficient that it's not doing anything else while we access the
|
|
// GSG.
|
|
return gsg->extract_texture_data(tex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Asks the indicated GraphicsStateGuardian to dispatch the compute shader in
|
|
* the given ShaderAttrib using the given work group counts. This can act as
|
|
* an interface for running a one-off compute shader, without having to store
|
|
* it in the scene graph using a ComputeNode.
|
|
*
|
|
* Since this requires a round-trip to the draw thread, it may require waiting
|
|
* for the current thread to finish rendering if it is called in a
|
|
* multithreaded environment. However, you can call this several consecutive
|
|
* times on different textures for little additional cost.
|
|
*
|
|
* The return value is true if the operation is successful, false otherwise.
|
|
*/
|
|
void GraphicsEngine::
|
|
dispatch_compute(const LVecBase3i &work_groups, const ShaderAttrib *sattr, GraphicsStateGuardian *gsg) {
|
|
nassertv(sattr->get_shader() != (Shader *)NULL);
|
|
|
|
ReMutexHolder holder(_lock);
|
|
|
|
CPT(RenderState) state = RenderState::make(sattr);
|
|
|
|
string draw_name = gsg->get_threading_model().get_draw_name();
|
|
if (draw_name.empty()) {
|
|
// A single-threaded environment. No problem.
|
|
|
|
} else {
|
|
// A multi-threaded environment. We have to wait until the draw thread
|
|
// has finished its current task.
|
|
WindowRenderer *wr = get_window_renderer(draw_name, 0);
|
|
RenderThread *thread = (RenderThread *)wr;
|
|
MutexHolder holder2(thread->_cv_mutex);
|
|
|
|
while (thread->_thread_state != TS_wait) {
|
|
thread->_cv_done.wait();
|
|
}
|
|
|
|
// OK, now the draw thread is idle. That's really good enough for our
|
|
// purposes; we don't *actually* need to make the draw thread do the work
|
|
// --it's sufficient that it's not doing anything else while we access the
|
|
// GSG.
|
|
}
|
|
|
|
gsg->set_state_and_transform(state, TransformState::make_identity());
|
|
gsg->dispatch_compute(work_groups[0], work_groups[1], work_groups[2]);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
GraphicsEngine *GraphicsEngine::
|
|
get_global_ptr() {
|
|
if (_global_ptr == NULL) {
|
|
_global_ptr = new GraphicsEngine;
|
|
PandaNode::set_scene_root_func(&scene_root_func);
|
|
}
|
|
return _global_ptr;
|
|
}
|
|
|
|
/**
|
|
* This method is called by the GraphicsStateGuardian after a texture has been
|
|
* successfully uploaded to graphics memory. It is intended as a callback so
|
|
* the texture can release its RAM image, if _keep_ram_image is false.
|
|
*
|
|
* Normally, this is not called directly except by the GraphicsStateGuardian.
|
|
* It will be called in the draw thread.
|
|
*/
|
|
void GraphicsEngine::
|
|
texture_uploaded(Texture *tex) {
|
|
MutexHolder holder(_loaded_textures_lock);
|
|
// We defer this until the end of the frame; multiple GSG's might be
|
|
// rendering the texture within the same frame, and we don't want to dump
|
|
// the texture image until they've all had a chance at it.
|
|
_loaded_textures.push_back(LoadedTexture());
|
|
LoadedTexture < = _loaded_textures.back();
|
|
lt._tex = tex;
|
|
lt._image_modified = tex->get_image_modified();
|
|
// Usually only called by DisplayRegion::do_cull.
|
|
}
|
|
|
|
/**
|
|
* Fires off a cull traversal using the indicated camera.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_cull(CullHandler *cull_handler, SceneSetup *scene_setup,
|
|
GraphicsStateGuardian *gsg, Thread *current_thread) {
|
|
DisplayRegion *dr = scene_setup->get_display_region();
|
|
PStatTimer timer(dr->get_cull_region_pcollector(), current_thread);
|
|
|
|
CullTraverser *trav = dr->get_cull_traverser();
|
|
trav->set_cull_handler(cull_handler);
|
|
trav->set_scene(scene_setup, gsg, dr->get_incomplete_render());
|
|
|
|
trav->set_view_frustum(NULL);
|
|
if (view_frustum_cull) {
|
|
// If we're to be performing view-frustum culling, determine the bounding
|
|
// volume associated with the current viewing frustum.
|
|
|
|
// First, we have to get the current viewing frustum, which comes from the
|
|
// lens.
|
|
PT(BoundingVolume) bv = scene_setup->get_cull_bounds();
|
|
|
|
if (bv != (BoundingVolume *)NULL && !bv->is_infinite() &&
|
|
bv->as_geometric_bounding_volume() != NULL) {
|
|
// Transform it into the appropriate coordinate space.
|
|
PT(GeometricBoundingVolume) local_frustum;
|
|
local_frustum = bv->make_copy()->as_geometric_bounding_volume();
|
|
nassertv(!local_frustum.is_null());
|
|
|
|
NodePath scene_parent = scene_setup->get_scene_root().get_parent(current_thread);
|
|
CPT(TransformState) cull_center_transform =
|
|
scene_setup->get_cull_center().get_transform(scene_parent, current_thread);
|
|
local_frustum->xform(cull_center_transform->get_mat());
|
|
|
|
trav->set_view_frustum(local_frustum);
|
|
}
|
|
}
|
|
|
|
trav->traverse(scene_setup->get_scene_root());
|
|
trav->end_traverse();
|
|
}
|
|
|
|
|
|
/**
|
|
* This function is added to PandaNode::scene_root_func to implement
|
|
* PandaNode::is_scene_root().
|
|
*/
|
|
bool GraphicsEngine::
|
|
scene_root_func(const PandaNode *node) {
|
|
return _global_ptr->is_scene_root(node);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the indicated node is known to be the render root of some
|
|
* active DisplayRegion associated with this GraphicsEngine, false otherwise.
|
|
*/
|
|
bool GraphicsEngine::
|
|
is_scene_root(const PandaNode *node) {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
|
|
Windows::const_iterator wi;
|
|
for (wi = _windows.begin(); wi != _windows.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
if (win->is_active() && win->get_gsg()->is_active()) {
|
|
int num_display_regions = win->get_num_active_display_regions();
|
|
for (int i = 0; i < num_display_regions; i++) {
|
|
DisplayRegion *dr = win->get_active_display_region(i);
|
|
if (dr != (DisplayRegion *)NULL) {
|
|
NodePath camera = dr->get_camera();
|
|
if (camera.is_empty()) {
|
|
continue;
|
|
}
|
|
|
|
Camera *camera_node;
|
|
DCAST_INTO_R(camera_node, camera.node(), false);
|
|
if (!camera_node->is_active()) {
|
|
continue;
|
|
}
|
|
|
|
NodePath scene_root = camera_node->get_scene();
|
|
if (scene_root.is_empty()) {
|
|
// If there's no explicit scene specified, use whatever scene the
|
|
// camera is parented within. This is the normal and preferred
|
|
// case; the use of an explicit scene is now deprecated.
|
|
scene_root = camera.get_top(current_thread);
|
|
}
|
|
|
|
if (scene_root.node() == node) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Changes the sort value of a particular window (or buffer) on the
|
|
* GraphicsEngine. This requires securing the mutex.
|
|
*
|
|
* Users shouldn't call this directly; use GraphicsOutput::set_sort() instead.
|
|
*/
|
|
void GraphicsEngine::
|
|
set_window_sort(GraphicsOutput *window, int sort) {
|
|
ReMutexHolder holder(_lock);
|
|
window->_sort = sort;
|
|
_windows_sorted = false;
|
|
}
|
|
|
|
/**
|
|
* This is called in the cull+draw thread by individual RenderThread objects
|
|
* during the frame rendering. It culls the geometry and immediately draws
|
|
* it, without first collecting it into bins. This is used when the threading
|
|
* model begins with the "-" character.
|
|
*/
|
|
void GraphicsEngine::
|
|
cull_and_draw_together(GraphicsEngine::Windows wlist,
|
|
Thread *current_thread) {
|
|
PStatTimer timer(_cull_pcollector, current_thread);
|
|
|
|
size_t wlist_size = wlist.size();
|
|
for (size_t wi = 0; wi < wlist_size; ++wi) {
|
|
GraphicsOutput *win = wlist[wi];
|
|
if (win->is_active() && win->get_gsg()->is_active()) {
|
|
if (win->flip_ready()) {
|
|
{
|
|
PStatTimer timer(GraphicsEngine::_flip_begin_pcollector, current_thread);
|
|
win->begin_flip();
|
|
}
|
|
{
|
|
PStatTimer timer(GraphicsEngine::_flip_end_pcollector, current_thread);
|
|
win->end_flip();
|
|
}
|
|
}
|
|
|
|
if (win->begin_frame(GraphicsOutput::FM_render, current_thread)) {
|
|
win->clear(current_thread);
|
|
|
|
int num_display_regions = win->get_num_active_display_regions();
|
|
for (int i = 0; i < num_display_regions; i++) {
|
|
DisplayRegion *dr = win->get_active_display_region(i);
|
|
if (dr != (DisplayRegion *)NULL) {
|
|
cull_and_draw_together(win, dr, current_thread);
|
|
}
|
|
}
|
|
win->end_frame(GraphicsOutput::FM_render, current_thread);
|
|
|
|
if (_auto_flip) {
|
|
if (win->flip_ready()) {
|
|
{
|
|
PStatTimer timer(GraphicsEngine::_flip_begin_pcollector, current_thread);
|
|
win->begin_flip();
|
|
}
|
|
{
|
|
PStatTimer timer(GraphicsEngine::_flip_end_pcollector, current_thread);
|
|
win->end_flip();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called only from within the inner loop in cull_and_draw_together(), above.
|
|
*/
|
|
void GraphicsEngine::
|
|
cull_and_draw_together(GraphicsOutput *win, DisplayRegion *dr,
|
|
Thread *current_thread) {
|
|
GraphicsStateGuardian *gsg = win->get_gsg();
|
|
nassertv(gsg != (GraphicsStateGuardian *)NULL);
|
|
|
|
PT(SceneSetup) scene_setup;
|
|
|
|
{
|
|
DisplayRegionPipelineReader dr_reader(dr, current_thread);
|
|
win->change_scenes(&dr_reader);
|
|
gsg->prepare_display_region(&dr_reader);
|
|
|
|
if (dr_reader.is_any_clear_active()) {
|
|
gsg->clear(dr);
|
|
}
|
|
|
|
scene_setup = setup_scene(gsg, &dr_reader);
|
|
}
|
|
|
|
if (scene_setup == (SceneSetup *)NULL) {
|
|
// Never mind.
|
|
|
|
} else if (dr->is_stereo()) {
|
|
// Don't draw stereo DisplayRegions directly.
|
|
|
|
} else if (!gsg->set_scene(scene_setup)) {
|
|
// The scene or lens is inappropriate somehow.
|
|
display_cat.error()
|
|
<< gsg->get_type() << " cannot render scene with specified lens.\n";
|
|
|
|
} else {
|
|
DrawCullHandler cull_handler(gsg);
|
|
if (gsg->begin_scene()) {
|
|
CallbackObject *cbobj = dr->get_cull_callback();
|
|
if (cbobj != (CallbackObject *)NULL) {
|
|
// Issue the cull callback on this DisplayRegion.
|
|
DisplayRegionCullCallbackData cbdata(&cull_handler, scene_setup);
|
|
cbobj->do_callback(&cbdata);
|
|
|
|
// The callback has taken care of the culling.
|
|
|
|
} else {
|
|
// Perform the cull normally.
|
|
dr->do_cull(&cull_handler, scene_setup, gsg, current_thread);
|
|
}
|
|
|
|
gsg->end_scene();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is called in the cull thread by individual RenderThread objects during
|
|
* the frame rendering. It collects the geometry into bins in preparation for
|
|
* drawing.
|
|
*/
|
|
void GraphicsEngine::
|
|
cull_to_bins(GraphicsEngine::Windows wlist, Thread *current_thread) {
|
|
PStatTimer timer(_cull_pcollector, current_thread);
|
|
|
|
_singular_warning_last_frame = _singular_warning_this_frame;
|
|
_singular_warning_this_frame = false;
|
|
|
|
// Keep track of the cameras we have already used in this thread to render
|
|
// DisplayRegions.
|
|
typedef pmap<CullKey, DisplayRegion *> AlreadyCulled;
|
|
AlreadyCulled already_culled;
|
|
|
|
size_t wlist_size = wlist.size();
|
|
for (size_t wi = 0; wi < wlist_size; ++wi) {
|
|
GraphicsOutput *win = wlist[wi];
|
|
if (win->is_active() && win->get_gsg()->is_active()) {
|
|
GraphicsStateGuardian *gsg = win->get_gsg();
|
|
PStatTimer timer(win->get_cull_window_pcollector(), current_thread);
|
|
int num_display_regions = win->get_num_active_display_regions();
|
|
for (int i = 0; i < num_display_regions; ++i) {
|
|
DisplayRegion *dr = win->get_active_display_region(i);
|
|
if (dr != nullptr) {
|
|
PT(SceneSetup) scene_setup;
|
|
PT(CullResult) cull_result;
|
|
CullKey key;
|
|
{
|
|
PStatTimer timer(_cull_setup_pcollector, current_thread);
|
|
DisplayRegionPipelineReader dr_reader(dr, current_thread);
|
|
scene_setup = setup_scene(gsg, &dr_reader);
|
|
if (scene_setup == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
key._gsg = gsg;
|
|
key._camera = dr_reader.get_camera();
|
|
key._lens_index = dr_reader.get_lens_index();
|
|
}
|
|
|
|
AlreadyCulled::iterator aci = already_culled.insert(AlreadyCulled::value_type(move(key), nullptr)).first;
|
|
if ((*aci).second == nullptr) {
|
|
// We have not used this camera already in this thread. Perform
|
|
// the cull operation.
|
|
cull_result = dr->get_cull_result(current_thread);
|
|
if (cull_result != nullptr) {
|
|
cull_result = cull_result->make_next();
|
|
} else {
|
|
// This DisplayRegion has no cull results; draw it.
|
|
cull_result = new CullResult(gsg, dr->get_draw_region_pcollector());
|
|
}
|
|
(*aci).second = dr;
|
|
cull_to_bins(win, gsg, dr, scene_setup, cull_result, current_thread);
|
|
|
|
} else {
|
|
// We have already culled a scene using this camera in this
|
|
// thread, and now we're being asked to cull another scene using
|
|
// the same camera. (Maybe this represents two different
|
|
// DisplayRegions for the left and right channels of a stereo
|
|
// image.) Of course, the cull result will be the same, so just
|
|
// use the result from the other DisplayRegion.
|
|
DisplayRegion *other_dr = (*aci).second;
|
|
cull_result = other_dr->get_cull_result(current_thread);
|
|
}
|
|
|
|
// Save the results for next frame.
|
|
dr->set_cull_result(MOVE(cull_result), MOVE(scene_setup), current_thread);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called only within the inner loop of cull_to_bins(), above.
|
|
*/
|
|
void GraphicsEngine::
|
|
cull_to_bins(GraphicsOutput *win, GraphicsStateGuardian *gsg,
|
|
DisplayRegion *dr, SceneSetup *scene_setup,
|
|
CullResult *cull_result, Thread *current_thread) {
|
|
|
|
BinCullHandler cull_handler(cull_result);
|
|
CallbackObject *cbobj = dr->get_cull_callback();
|
|
if (cbobj != (CallbackObject *)NULL) {
|
|
// Issue the cull callback on this DisplayRegion.
|
|
DisplayRegionCullCallbackData cbdata(&cull_handler, scene_setup);
|
|
cbobj->do_callback(&cbdata);
|
|
|
|
// The callback has taken care of the culling.
|
|
|
|
} else {
|
|
// Perform the cull normally.
|
|
dr->do_cull(&cull_handler, scene_setup, gsg, current_thread);
|
|
}
|
|
|
|
PStatTimer timer(_cull_sort_pcollector, current_thread);
|
|
cull_result->finish_cull(scene_setup, current_thread);
|
|
}
|
|
|
|
/**
|
|
* This is called in the draw thread by individual RenderThread objects during
|
|
* the frame rendering. It issues the graphics commands to draw the objects
|
|
* that have been collected into bins by a previous call to cull_to_bins().
|
|
*/
|
|
void GraphicsEngine::
|
|
draw_bins(const GraphicsEngine::Windows &wlist, Thread *current_thread) {
|
|
nassertv(wlist.verify_list());
|
|
|
|
size_t wlist_size = wlist.size();
|
|
for (size_t wi = 0; wi < wlist_size; ++wi) {
|
|
GraphicsOutput *win = wlist[wi];
|
|
|
|
if (win->is_active()) {
|
|
GraphicsStateGuardian *gsg = win->get_gsg();
|
|
|
|
GraphicsOutput *host = win->get_host();
|
|
if (host->flip_ready()) {
|
|
{
|
|
// We can't use a PStatGPUTimer before begin_frame, so when using
|
|
// GPU timing, it is advisable to set auto-flip to #t.
|
|
PStatTimer timer(GraphicsEngine::_flip_begin_pcollector, current_thread);
|
|
host->begin_flip();
|
|
}
|
|
{
|
|
PStatTimer timer(GraphicsEngine::_flip_end_pcollector, current_thread);
|
|
host->end_flip();
|
|
}
|
|
}
|
|
|
|
if (win->begin_frame(GraphicsOutput::FM_render, current_thread)) {
|
|
// We have to place this collector inside begin_frame, because we need
|
|
// a current context for PStatGPUTimer to work.
|
|
{
|
|
PStatGPUTimer timer(gsg, win->get_draw_window_pcollector(), current_thread);
|
|
win->clear(current_thread);
|
|
|
|
if (display_cat.is_spam()) {
|
|
display_cat.spam()
|
|
<< "Drawing window " << win->get_name() << "\n";
|
|
}
|
|
int num_display_regions = win->get_num_active_display_regions();
|
|
for (int i = 0; i < num_display_regions; ++i) {
|
|
DisplayRegion *dr = win->get_active_display_region(i);
|
|
if (dr != (DisplayRegion *)NULL) {
|
|
do_draw(win, gsg, dr, current_thread);
|
|
}
|
|
}
|
|
}
|
|
win->end_frame(GraphicsOutput::FM_render, current_thread);
|
|
|
|
if (_auto_flip) {
|
|
#ifdef DO_PSTATS
|
|
// This is a good time to perform a latency query.
|
|
if (gsg->get_timer_queries_active()) {
|
|
gsg->issue_timer_query(GraphicsStateGuardian::_command_latency_pcollector.get_index());
|
|
}
|
|
#endif
|
|
|
|
if (win->flip_ready()) {
|
|
{
|
|
// begin_flip doesn't do anything interesting, let's not waste
|
|
// two timer queries on that.
|
|
PStatTimer timer(GraphicsEngine::_flip_begin_pcollector, current_thread);
|
|
win->begin_flip();
|
|
}
|
|
{
|
|
PStatGPUTimer timer(gsg, GraphicsEngine::_flip_end_pcollector, current_thread);
|
|
win->end_flip();
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if (display_cat.is_spam()) {
|
|
display_cat.spam()
|
|
<< "Not drawing window " << win->get_name() << "\n";
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if (display_cat.is_spam()) {
|
|
display_cat.spam()
|
|
<< "Window " << win->get_name() << " is inactive\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called in the draw thread, this calls make_context() on each window on the
|
|
* list to guarantee its gsg and graphics context both get created.
|
|
*/
|
|
void GraphicsEngine::
|
|
make_contexts(const GraphicsEngine::Windows &wlist, Thread *current_thread) {
|
|
Windows::const_iterator wi;
|
|
for (wi = wlist.begin(); wi != wlist.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
if (win->begin_frame(GraphicsOutput::FM_refresh, current_thread)) {
|
|
win->end_frame(GraphicsOutput::FM_refresh, current_thread);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is called by the RenderThread object to process all the windows events
|
|
* (resize, etc.) for the given list of windows. This is run in the window
|
|
* thread.
|
|
*/
|
|
void GraphicsEngine::
|
|
process_events(const GraphicsEngine::Windows &wlist, Thread *current_thread) {
|
|
// We're not using a vector iterator here, since it's possible that the
|
|
// window list changes in an event, which would invalidate the iterator and
|
|
// cause a crash.
|
|
for (size_t i = 0; i < wlist.size(); ++i) {
|
|
wlist[i]->process_events();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is called by the RenderThread object to flip the buffers for all of
|
|
* the non-single-buffered windows in the given list. This is run in the draw
|
|
* thread.
|
|
*/
|
|
void GraphicsEngine::
|
|
flip_windows(const GraphicsEngine::Windows &wlist, Thread *current_thread) {
|
|
size_t num_windows = wlist.size();
|
|
size_t warray_size = num_windows * sizeof(GraphicsOutput *);
|
|
size_t warray_count = 0;
|
|
GraphicsOutput **warray = (GraphicsOutput **)alloca(warray_size);
|
|
|
|
size_t i;
|
|
for (i = 0; i < num_windows; ++i) {
|
|
GraphicsOutput *win = wlist[i];
|
|
if (win->flip_ready()) {
|
|
nassertv(warray_count < num_windows);
|
|
warray[warray_count] = win;
|
|
++warray_count;
|
|
|
|
PStatTimer timer(GraphicsEngine::_flip_begin_pcollector, current_thread);
|
|
win->begin_flip();
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < warray_count; ++i) {
|
|
GraphicsOutput *win = warray[i];
|
|
PStatTimer timer(GraphicsEngine::_flip_end_pcollector, current_thread);
|
|
win->end_flip();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is called by the RenderThread object to flip the buffers for all of
|
|
* the non-single-buffered windows in the given list. This is run in the draw
|
|
* thread.
|
|
*/
|
|
void GraphicsEngine::
|
|
ready_flip_windows(const GraphicsEngine::Windows &wlist, Thread *current_thread) {
|
|
Windows::const_iterator wi;
|
|
for (wi = wlist.begin(); wi != wlist.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
if (win->flip_ready()) {
|
|
PStatTimer timer(GraphicsEngine::_flip_begin_pcollector, current_thread);
|
|
win->ready_flip();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The implementation of sync_frame(). We assume _lock is already held before
|
|
* this method is called.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_sync_frame(Thread *current_thread) {
|
|
nassertv(_lock.debug_is_locked());
|
|
|
|
// Statistics
|
|
PStatTimer timer(_sync_pcollector, current_thread);
|
|
|
|
nassertv(_flip_state == FS_draw);
|
|
|
|
// Wait for all the threads to finish their current frame. Grabbing and
|
|
// releasing the mutex should achieve that.
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_cv_mutex.acquire();
|
|
thread->_cv_mutex.release();
|
|
}
|
|
|
|
_flip_state = FS_sync;
|
|
}
|
|
|
|
/**
|
|
* Wait until all draw calls have finished drawing and the frame is ready to
|
|
* flip
|
|
*/
|
|
void GraphicsEngine::
|
|
do_ready_flip(Thread *current_thread) {
|
|
nassertv(_lock.debug_is_locked());
|
|
|
|
// Statistics
|
|
PStatTimer timer(_sync_pcollector, current_thread);
|
|
|
|
nassertv(_flip_state == FS_draw);
|
|
|
|
// Wait for all the threads to finish their current frame. Grabbing and
|
|
// releasing the mutex should achieve that.
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_cv_mutex.acquire();
|
|
thread->_cv_mutex.release();
|
|
}
|
|
_app.do_ready_flip(this,current_thread);
|
|
_flip_state = FS_sync;
|
|
}
|
|
|
|
/**
|
|
* The implementation of flip_frame(). We assume _lock is already held before
|
|
* this method is called.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_flip_frame(Thread *current_thread) {
|
|
nassertv(_lock.debug_is_locked());
|
|
|
|
// Statistics
|
|
PStatTimer timer(_flip_pcollector, current_thread);
|
|
|
|
nassertv(_flip_state == FS_draw || _flip_state == FS_sync);
|
|
|
|
// First, wait for all the threads to finish their current frame, if
|
|
// necessary. Grabbing the mutex (and waiting for TS_wait) should achieve
|
|
// that.
|
|
{
|
|
PStatTimer timer(_wait_pcollector, current_thread);
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_cv_mutex.acquire();
|
|
|
|
while (thread->_thread_state != TS_wait) {
|
|
thread->_cv_done.wait();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now signal all of our threads to flip the windows.
|
|
_app.do_flip(this, current_thread);
|
|
|
|
{
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
nassertv(thread->_thread_state == TS_wait);
|
|
thread->_thread_state = TS_do_flip;
|
|
thread->_cv_start.notify();
|
|
thread->_cv_mutex.release();
|
|
}
|
|
}
|
|
|
|
_flip_state = FS_flip;
|
|
}
|
|
|
|
/**
|
|
* Returns a new SceneSetup object appropriate for rendering the scene from
|
|
* the indicated camera, or NULL if the scene should not be rendered for some
|
|
* reason.
|
|
*/
|
|
PT(SceneSetup) GraphicsEngine::
|
|
setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) {
|
|
Thread *current_thread = dr->get_current_thread();
|
|
PStatTimer timer(_cull_setup_pcollector, current_thread);
|
|
|
|
GraphicsOutput *window = dr->get_window();
|
|
// The window pointer shouldn't be NULL, since we presumably got to this
|
|
// particular DisplayRegion by walking through a list on a window.
|
|
nassertr(window != (GraphicsOutput *)NULL, NULL);
|
|
|
|
NodePath camera = dr->get_camera();
|
|
if (camera.is_empty()) {
|
|
// No camera, no draw.
|
|
return NULL;
|
|
}
|
|
|
|
Camera *camera_node;
|
|
DCAST_INTO_R(camera_node, camera.node(), NULL);
|
|
|
|
if (!camera_node->is_active()) {
|
|
// Camera inactive, no draw.
|
|
return NULL;
|
|
}
|
|
camera_node->cleanup_aux_scene_data(current_thread);
|
|
|
|
int lens_index = dr->get_lens_index();
|
|
Lens *lens = camera_node->get_lens(lens_index);
|
|
if (lens == (Lens *)NULL || !camera_node->get_lens_active(lens_index)) {
|
|
// No lens, no draw.
|
|
return NULL;
|
|
}
|
|
|
|
NodePath scene_root = camera_node->get_scene();
|
|
if (scene_root.is_empty()) {
|
|
// If there's no explicit scene specified, use whatever scene the camera
|
|
// is parented within. This is the normal and preferred case; the use of
|
|
// an explicit scene is now deprecated.
|
|
scene_root = camera.get_top(current_thread);
|
|
}
|
|
|
|
PT(SceneSetup) scene_setup = new SceneSetup;
|
|
|
|
// We will need both the camera transform (the net transform to the camera
|
|
// from the scene) and the world transform (the camera transform inverse, or
|
|
// the net transform to the scene from the camera). These are actually
|
|
// defined from the parent of the scene_root, because the scene_root's own
|
|
// transform is immediately applied to these during rendering. (Normally,
|
|
// the parent of the scene_root is the empty NodePath, although it need not
|
|
// be.)
|
|
NodePath scene_parent = scene_root.get_parent(current_thread);
|
|
CPT(TransformState) camera_transform = camera.get_transform(scene_parent, current_thread);
|
|
CPT(TransformState) world_transform = scene_parent.get_transform(camera, current_thread);
|
|
|
|
if (camera_transform->is_invalid()) {
|
|
// There must be a singular transform over the scene.
|
|
if (!_singular_warning_last_frame) {
|
|
display_cat.warning()
|
|
<< "Scene " << scene_root << " has net scale ("
|
|
<< scene_root.get_scale(NodePath()) << "); cannot render.\n";
|
|
_singular_warning_this_frame = true;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
if (world_transform->is_invalid()) {
|
|
// There must be a singular transform over the camera.
|
|
if (!_singular_warning_last_frame) {
|
|
display_cat.warning()
|
|
<< "Camera " << camera << " has net scale ("
|
|
<< camera.get_scale(NodePath()) << "); cannot render.\n";
|
|
}
|
|
_singular_warning_this_frame = true;
|
|
return NULL;
|
|
}
|
|
|
|
CPT(RenderState) initial_state = camera_node->get_initial_state();
|
|
|
|
if (window->get_inverted()) {
|
|
// If the window is to be inverted, we must set the inverted flag on the
|
|
// SceneSetup object, so that the GSG will be able to invert the
|
|
// projection matrix at the last minute.
|
|
scene_setup->set_inverted(true);
|
|
|
|
// This also means we need to globally invert the sense of polygon vertex
|
|
// ordering.
|
|
initial_state = initial_state->compose(get_invert_polygon_state());
|
|
}
|
|
|
|
scene_setup->set_display_region(dr->get_object());
|
|
scene_setup->set_viewport_size(dr->get_pixel_width(), dr->get_pixel_height());
|
|
scene_setup->set_scene_root(scene_root);
|
|
scene_setup->set_camera_path(camera);
|
|
scene_setup->set_camera_node(camera_node);
|
|
scene_setup->set_lens(lens);
|
|
scene_setup->set_initial_state(initial_state);
|
|
scene_setup->set_camera_transform(camera_transform);
|
|
scene_setup->set_world_transform(world_transform);
|
|
|
|
CPT(TransformState) cs_transform = gsg->get_cs_transform_for(lens->get_coordinate_system());
|
|
scene_setup->set_cs_transform(cs_transform);
|
|
|
|
CPT(TransformState) cs_world_transform = cs_transform->compose(world_transform);
|
|
scene_setup->set_cs_world_transform(cs_world_transform);
|
|
|
|
return scene_setup;
|
|
}
|
|
|
|
/**
|
|
* Draws the previously-culled scene.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_draw(GraphicsOutput *win, GraphicsStateGuardian *gsg, DisplayRegion *dr, Thread *current_thread) {
|
|
// Statistics
|
|
PStatGPUTimer timer(gsg, dr->get_draw_region_pcollector(), current_thread);
|
|
|
|
PT(CullResult) cull_result;
|
|
PT(SceneSetup) scene_setup;
|
|
{
|
|
DisplayRegion::CDCullReader cdata(dr->_cycler_cull, current_thread);
|
|
cull_result = cdata->_cull_result;
|
|
scene_setup = cdata->_scene_setup;
|
|
}
|
|
|
|
CallbackObject *cbobj;
|
|
|
|
{
|
|
DisplayRegionPipelineReader dr_reader(dr, current_thread);
|
|
win->change_scenes(&dr_reader);
|
|
gsg->prepare_display_region(&dr_reader);
|
|
if (dr_reader.is_any_clear_active()) {
|
|
gsg->clear(dr_reader.get_object());
|
|
}
|
|
|
|
cbobj = dr_reader.get_draw_callback();
|
|
}
|
|
|
|
if (cbobj != (CallbackObject *)NULL) {
|
|
// Issue the draw callback on this DisplayRegion.
|
|
|
|
// Set the GSG to the initial state.
|
|
gsg->clear_before_callback();
|
|
gsg->set_state_and_transform(RenderState::make_empty(), TransformState::make_identity());
|
|
|
|
DisplayRegionDrawCallbackData cbdata(cull_result, scene_setup);
|
|
cbobj->do_callback(&cbdata);
|
|
|
|
// We don't trust the state the callback may have left us in.
|
|
gsg->clear_state_and_transform();
|
|
|
|
// The callback has taken care of the drawing.
|
|
return;
|
|
}
|
|
|
|
if (cull_result == NULL || scene_setup == NULL) {
|
|
// Nothing to see here.
|
|
|
|
} else if (dr->is_stereo()) {
|
|
// We don't actually draw the stereo DisplayRegions. These are just
|
|
// placeholders; we draw the individual left and right eyes instead. (We
|
|
// might still clear the stereo DisplayRegions, though, since it's
|
|
// probably faster to clear right and left channels in one pass, than to
|
|
// clear them in two separate passes.)
|
|
|
|
} else if (!gsg->set_scene(scene_setup)) {
|
|
// The scene or lens is inappropriate somehow.
|
|
display_cat.error()
|
|
<< gsg->get_type() << " cannot render scene with specified lens.\n";
|
|
|
|
} else {
|
|
if (gsg->begin_scene()) {
|
|
cull_result->draw(current_thread);
|
|
gsg->end_scene();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An internal function called by make_window() and make_buffer() and similar
|
|
* functions to add the newly-created GraphicsOutput object to the engine's
|
|
* list of windows, and to request that the window be opened.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_add_window(GraphicsOutput *window) {
|
|
nassertv(window != nullptr);
|
|
|
|
MutexHolder holder(_new_windows_lock);
|
|
nassertv(window->get_engine() == this);
|
|
|
|
// We have a special counter that is unique per window that allows us to
|
|
// assure that recently-added windows end up on the end of the list.
|
|
window->_internal_sort_index = _window_sort_index;
|
|
++_window_sort_index;
|
|
|
|
if (display_cat.is_debug()) {
|
|
display_cat.debug()
|
|
<< "Created " << window->get_type() << " " << (void *)window << "\n";
|
|
}
|
|
|
|
window->request_open();
|
|
_new_windows.push_back(window);
|
|
}
|
|
|
|
/**
|
|
* An internal function called by make_output to add the newly-created gsg
|
|
* object to the engine's list of gsg's. It also adjusts various config
|
|
* variables based on the gsg's capabilities.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_add_gsg(GraphicsStateGuardian *gsg, GraphicsPipe *pipe) {
|
|
nassertv(gsg != NULL);
|
|
|
|
ReMutexHolder holder(_lock);
|
|
nassertv(gsg->get_pipe() == pipe && gsg->get_engine() == this);
|
|
gsg->_threading_model = _threading_model;
|
|
if (!_default_loader.is_null()) {
|
|
gsg->set_loader(_default_loader);
|
|
}
|
|
|
|
auto_adjust_capabilities(gsg);
|
|
|
|
WindowRenderer *draw =
|
|
get_window_renderer(_threading_model.get_draw_name(),
|
|
_threading_model.get_draw_stage());
|
|
|
|
draw->add_gsg(gsg);
|
|
}
|
|
|
|
/**
|
|
* An internal function called by remove_window() and remove_all_windows() to
|
|
* actually remove the indicated window from all relevant structures, except
|
|
* the _windows list itself.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_remove_window(GraphicsOutput *window, Thread *current_thread) {
|
|
nassertv(window != NULL);
|
|
PT(GraphicsPipe) pipe = window->get_pipe();
|
|
window->clear_pipe();
|
|
|
|
if (!_windows_sorted) {
|
|
do_resort_windows();
|
|
}
|
|
|
|
// Now remove the window from all threads that know about it.
|
|
_app.remove_window(window);
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->remove_window(window);
|
|
}
|
|
|
|
// If the window happened to be controlled by the app thread, we might as
|
|
// well close it now rather than waiting for next frame.
|
|
_app.do_pending(this, current_thread);
|
|
|
|
if (display_cat.is_debug()) {
|
|
display_cat.debug()
|
|
<< "Removed " << window->get_type() << " " << (void *)window << "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resorts all of the Windows lists. This may need to be done if one or more
|
|
* of the windows' sort properties has changed.
|
|
*/
|
|
void GraphicsEngine::
|
|
do_resort_windows() {
|
|
_windows_sorted = true;
|
|
|
|
_app.resort_windows();
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->resort_windows();
|
|
}
|
|
|
|
_windows.sort();
|
|
}
|
|
|
|
/**
|
|
* Video card capability flags are stored on a per-gsg basis. However, there
|
|
* are a few cases where panda needs to know not the capabilities of an
|
|
* individual GSG, but rather, the collective capabilities of all the GSGs.
|
|
*
|
|
* Non-power-of-two (NPOT) texture support is the classic example. Panda
|
|
* makes a single global decision to either create NPOT textures, or not.
|
|
* Therefore, it doesn't need to know whether one GSG supports NPOT textures.
|
|
* It needs to know whether ALL the GSGs support NPOT textures.
|
|
*
|
|
* The purpose of this routine is to maintain global capability flags that
|
|
* summarize the collective capabilities of the computer as a whole.
|
|
*
|
|
* These global capability flags are initialized from config variables. Then,
|
|
* they can be auto-reconfigured using built-in heuristic mechanisms if the
|
|
* user so desires. Whether auto-reconfiguration is enabled or not, the
|
|
* configured values are checked against the actual capabilities of the
|
|
* machine and error messages will be printed if there is a mismatch.
|
|
*
|
|
*/
|
|
void GraphicsEngine::
|
|
auto_adjust_capabilities(GraphicsStateGuardian *gsg) {
|
|
|
|
/*
|
|
* The rule we use when auto-reconfiguring is as follows. The global
|
|
* capabilities must initially be set to conservative values. When the first
|
|
* GSG comes into existence, its capabilities will be checked, and the global
|
|
* capabilities may be elevated to more aggressive values. At first glance,
|
|
* this might seem backward, and it might seem better to do it the other way:
|
|
* start with all global capabilities aggressively set, and then disable
|
|
* capabilities when you discover a gsg that doesn't support them. However,
|
|
* that approach doesn't work, because once a global capability is enabled,
|
|
* there is no going back. If textures_power_2 has ever been set to 'none',
|
|
* there may be NPOT textures already floating about the system. Ie, it's too
|
|
* late: you can't turn these global capability flags off, once they've been
|
|
* turned on. That's why we have to start with conservative settings, and
|
|
* then elevate those settings to more aggressive values later when we're
|
|
* fairly sure it's OK to do so. For each global capability, we must: 1. Make
|
|
* sure the initial setting is conservative. 2. Possibly elevate to a more
|
|
* aggressive value. 3. Check that we haven't over-elevated.
|
|
*/
|
|
|
|
if (textures_auto_power_2 && (textures_power_2 == ATS_none)) {
|
|
display_cat.error()
|
|
<< "Invalid panda config file: if you set the config-variable\n"
|
|
<< "textures_auto_power_2 to true, you must set the config-variable"
|
|
<< "textures_power_2 to 'up' or 'down'.\n";
|
|
textures_power_2 = ATS_down; // Not a fix. Just suppresses further error messages.
|
|
}
|
|
|
|
if (textures_auto_power_2 && !Texture::has_textures_power_2()) {
|
|
if (gsg->get_supports_tex_non_pow2()) {
|
|
Texture::set_textures_power_2(ATS_none);
|
|
} else {
|
|
Texture::set_textures_power_2(textures_power_2);
|
|
}
|
|
}
|
|
|
|
if ((Texture::get_textures_power_2() == ATS_none) &&
|
|
(!gsg->get_supports_tex_non_pow2())) {
|
|
|
|
// Overaggressive configuration detected
|
|
|
|
display_cat.error()
|
|
<< "The 'textures_power_2' configuration is set to 'none', meaning \n"
|
|
<< "that non-power-of-two texture support is required, but the video \n"
|
|
<< "driver I'm trying to use does not support non-power-of-two textures.\n";
|
|
|
|
if (textures_power_2 != ATS_none) {
|
|
display_cat.error()
|
|
<< "The 'none' did not come from the config file. In other words,\n"
|
|
<< "the variable 'textures_power_2' was altered procedurally.\n";
|
|
|
|
if (textures_auto_power_2) {
|
|
display_cat.error()
|
|
<< "It is possible that it was set by panda's automatic mechanisms,\n"
|
|
<< "which are currently enabled, because 'textures_auto_power_2' is\n"
|
|
<< "true. Panda's automatic mechanisms assume that if one\n"
|
|
<< "window supports non-power-of-two textures, then they all will.\n"
|
|
<< "This assumption works for most games, but not all.\n"
|
|
<< "In particular, it can fail if the game creates multiple windows\n"
|
|
<< "on multiple displays with different video cards.\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Signals our child threads to terminate and waits for them to clean up.
|
|
*/
|
|
void GraphicsEngine::
|
|
terminate_threads(Thread *current_thread) {
|
|
ReMutexHolder holder(_lock, current_thread);
|
|
|
|
// We spend almost our entire time in this method just waiting for threads.
|
|
// Time it appropriately.
|
|
PStatTimer timer(_wait_pcollector, current_thread);
|
|
|
|
// First, wait for all the threads to finish their current frame. Grabbing
|
|
// the mutex should achieve that.
|
|
Threads::const_iterator ti;
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_cv_mutex.acquire();
|
|
}
|
|
|
|
// Now tell them to close their windows and terminate.
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->_thread_state = TS_terminate;
|
|
thread->_cv_start.notify();
|
|
thread->_cv_mutex.release();
|
|
}
|
|
|
|
// Finally, wait for them all to finish cleaning up.
|
|
for (ti = _threads.begin(); ti != _threads.end(); ++ti) {
|
|
RenderThread *thread = (*ti).second;
|
|
thread->join();
|
|
}
|
|
|
|
_threads.clear();
|
|
}
|
|
|
|
|
|
#ifdef DO_PSTATS
|
|
/**
|
|
* A callback function for Pipeline::iterate_all_cycler_types() to report the
|
|
* cycler types to PStats.
|
|
*/
|
|
void GraphicsEngine::
|
|
pstats_count_cycler_type(TypeHandle type, int count, void *data) {
|
|
GraphicsEngine *self = (GraphicsEngine *)data;
|
|
CyclerTypeCounters::iterator ci = self->_all_cycler_types.find(type);
|
|
if (ci == self->_all_cycler_types.end()) {
|
|
PStatCollector collector(_cyclers_pcollector, type.get_name());
|
|
ci = self->_all_cycler_types.insert(CyclerTypeCounters::value_type(type, collector)).first;
|
|
}
|
|
(*ci).second.set_level(count);
|
|
}
|
|
#endif // DO_PSTATS
|
|
|
|
#ifdef DO_PSTATS
|
|
/**
|
|
* A callback function for Pipeline::iterate_dirty_cycler_types() to report
|
|
* the cycler types to PStats.
|
|
*/
|
|
void GraphicsEngine::
|
|
pstats_count_dirty_cycler_type(TypeHandle type, int count, void *data) {
|
|
GraphicsEngine *self = (GraphicsEngine *)data;
|
|
CyclerTypeCounters::iterator ci = self->_dirty_cycler_types.find(type);
|
|
if (ci == self->_dirty_cycler_types.end()) {
|
|
PStatCollector collector(_dirty_cyclers_pcollector, type.get_name());
|
|
ci = self->_dirty_cycler_types.insert(CyclerTypeCounters::value_type(type, collector)).first;
|
|
}
|
|
(*ci).second.set_level(count);
|
|
}
|
|
#endif // DO_PSTATS
|
|
|
|
/**
|
|
* Returns a RenderState for inverting the sense of polygon vertex ordering:
|
|
* if the scene graph specifies a clockwise ordering, this changes it to
|
|
* counterclockwise, and vice-versa.
|
|
*/
|
|
const RenderState *GraphicsEngine::
|
|
get_invert_polygon_state() {
|
|
// Once someone asks for this pointer, we hold its reference count and never
|
|
// free it.
|
|
static CPT(RenderState) state = (const RenderState *)NULL;
|
|
if (state == (const RenderState *)NULL) {
|
|
state = RenderState::make(CullFaceAttrib::make_reverse());
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Returns the WindowRenderer with the given name. Creates a new RenderThread
|
|
* if there is no such thread already. The pipeline_stage number specifies
|
|
* the pipeline stage that will be assigned to the thread (unless was
|
|
* previously given a higher stage).
|
|
*
|
|
* You must already be holding the lock before calling this method.
|
|
*/
|
|
GraphicsEngine::WindowRenderer *GraphicsEngine::
|
|
get_window_renderer(const string &name, int pipeline_stage) {
|
|
nassertr(_lock.debug_is_locked(), NULL);
|
|
|
|
if (name.empty()) {
|
|
return &_app;
|
|
}
|
|
|
|
Threads::iterator ti = _threads.find(name);
|
|
if (ti != _threads.end()) {
|
|
return (*ti).second.p();
|
|
}
|
|
|
|
PT(RenderThread) thread = new RenderThread(name, this);
|
|
thread->set_min_pipeline_stage(pipeline_stage);
|
|
_pipeline->set_min_stages(pipeline_stage + 1);
|
|
|
|
bool started = thread->start(TP_normal, true);
|
|
nassertr(started, thread.p());
|
|
_threads[name] = thread;
|
|
|
|
nassertr(thread->get_pipeline_stage() < _pipeline->get_num_stages(), thread.p());
|
|
|
|
return thread.p();
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
GraphicsEngine::WindowRenderer::
|
|
WindowRenderer(const string &name) :
|
|
_wl_lock(string("GraphicsEngine::WindowRenderer::_wl_lock ") + name)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Adds a new GSG to the _gsg list, if it is not already there.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
add_gsg(GraphicsStateGuardian *gsg) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
_gsgs.insert(gsg);
|
|
}
|
|
|
|
/**
|
|
* Adds a new window to the indicated list, which should be a member of the
|
|
* WindowRenderer.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
add_window(Windows &wlist, GraphicsOutput *window) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
wlist.insert(window);
|
|
}
|
|
|
|
/**
|
|
* Immediately removes the indicated window from all lists. If the window is
|
|
* currently open and is already on the _window list, moves it to the
|
|
* _pending_close list for later closure.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
remove_window(GraphicsOutput *window) {
|
|
nassertv(window != NULL);
|
|
LightReMutexHolder holder(_wl_lock);
|
|
PT(GraphicsOutput) ptwin = window;
|
|
|
|
_cull.erase(ptwin);
|
|
_cdraw.erase(ptwin);
|
|
_draw.erase(ptwin);
|
|
|
|
Windows::iterator wi;
|
|
|
|
wi = _window.find(ptwin);
|
|
if (wi != _window.end()) {
|
|
// The window is on our _window list, meaning its openclose operations
|
|
// (among other window ops) are serviced by this thread.
|
|
|
|
// Make sure the window isn't about to request itself open.
|
|
ptwin->request_close();
|
|
|
|
// If the window is already open, move it to the _pending_close list so it
|
|
// can be closed later. We can't close it immediately, because we might
|
|
// not have been called from the subthread.
|
|
if (ptwin->is_valid()) {
|
|
_pending_close.push_back(ptwin);
|
|
}
|
|
|
|
_window.erase(wi);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resorts all the lists of windows, assuming they may have become unsorted.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
resort_windows() {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
|
|
_cull.sort();
|
|
_cdraw.sort();
|
|
_draw.sort();
|
|
_window.sort();
|
|
|
|
if (display_cat.is_debug()) {
|
|
display_cat.debug()
|
|
<< "Windows resorted:";
|
|
Windows::const_iterator wi;
|
|
for (wi = _window.begin(); wi != _window.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
display_cat.debug(false)
|
|
<< " " << win->get_name() << "(" << win->get_sort() << ")";
|
|
}
|
|
display_cat.debug(false)
|
|
<< "\n";
|
|
|
|
for (wi = _draw.begin(); wi != _draw.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
display_cat.debug(false)
|
|
<< " " << win->get_name() << "(" << win->get_sort() << ")";
|
|
}
|
|
display_cat.debug(false)
|
|
<< "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes one stage of the pipeline for the current thread: calls cull on
|
|
* all windows that are on the cull list for this thread, draw on all the
|
|
* windows on the draw list, etc.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
do_frame(GraphicsEngine *engine, Thread *current_thread) {
|
|
PStatTimer timer(engine->_do_frame_pcollector, current_thread);
|
|
LightReMutexHolder holder(_wl_lock);
|
|
|
|
engine->cull_to_bins(_cull, current_thread);
|
|
engine->cull_and_draw_together(_cdraw, current_thread);
|
|
engine->draw_bins(_draw, current_thread);
|
|
engine->process_events(_window, current_thread);
|
|
|
|
// If any GSG's on the list have no more outstanding pointers, clean them
|
|
// up. (We are in the draw thread for all of these GSG's.)
|
|
if (any_done_gsgs()) {
|
|
GSGs new_gsgs;
|
|
GSGs::iterator gi;
|
|
for (gi = _gsgs.begin(); gi != _gsgs.end(); ++gi) {
|
|
GraphicsStateGuardian *gsg = (*gi);
|
|
if (gsg->get_ref_count() == 1) {
|
|
// This one has no outstanding pointers; clean it up.
|
|
GraphicsPipe *pipe = gsg->get_pipe();
|
|
engine->close_gsg(pipe, gsg);
|
|
} else {
|
|
// This one is ok; preserve it.
|
|
new_gsgs.insert(gsg);
|
|
}
|
|
}
|
|
|
|
_gsgs.swap(new_gsgs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts to fully open or close any windows or buffers associated with this
|
|
* thread, but does not otherwise perform any rendering. (Normally, this step
|
|
* is handled during do_frame(); call this method only if you want these
|
|
* things to open immediately.)
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
do_windows(GraphicsEngine *engine, Thread *current_thread) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
|
|
engine->process_events(_window, current_thread);
|
|
|
|
engine->make_contexts(_cdraw, current_thread);
|
|
engine->make_contexts(_draw, current_thread);
|
|
}
|
|
|
|
/**
|
|
* Flips the windows as appropriate for the current thread.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
do_flip(GraphicsEngine *engine, Thread *current_thread) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
engine->flip_windows(_cdraw, current_thread);
|
|
engine->flip_windows(_draw, current_thread);
|
|
}
|
|
|
|
/**
|
|
* Prepares windows for flipping by waiting until all draw calls are finished
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
do_ready_flip(GraphicsEngine *engine, Thread *current_thread) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
engine->ready_flip_windows(_cdraw, current_thread);
|
|
engine->ready_flip_windows(_draw, current_thread);
|
|
}
|
|
|
|
|
|
/**
|
|
* Closes all the windows on the _window list.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
do_close(GraphicsEngine *engine, Thread *current_thread) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
Windows::iterator wi;
|
|
for (wi = _window.begin(); wi != _window.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
win->set_close_now();
|
|
}
|
|
|
|
// Also close all of the GSG's.
|
|
GSGs new_gsgs;
|
|
GSGs::iterator gi;
|
|
for (gi = _gsgs.begin(); gi != _gsgs.end(); ++gi) {
|
|
GraphicsStateGuardian *gsg = (*gi);
|
|
if (gsg->get_ref_count() == 1) {
|
|
// This one has no outstanding pointers; clean it up.
|
|
GraphicsPipe *pipe = gsg->get_pipe();
|
|
engine->close_gsg(pipe, gsg);
|
|
} else {
|
|
// This one is ok; preserve it.
|
|
new_gsgs.insert(gsg);
|
|
}
|
|
}
|
|
|
|
_gsgs.swap(new_gsgs);
|
|
}
|
|
|
|
/**
|
|
* Actually closes any windows that were recently removed from the
|
|
* WindowRenderer.
|
|
*/
|
|
void GraphicsEngine::WindowRenderer::
|
|
do_pending(GraphicsEngine *engine, Thread *current_thread) {
|
|
LightReMutexHolder holder(_wl_lock);
|
|
|
|
if (!_pending_close.empty()) {
|
|
if (display_cat.is_debug()) {
|
|
display_cat.debug()
|
|
<< "_pending_close.size() = " << _pending_close.size() << "\n";
|
|
}
|
|
|
|
// Close any windows that were pending closure. Carefully protect against
|
|
// recursive entry to this function by swapping the vector to a local copy
|
|
// first.
|
|
Windows::iterator wi;
|
|
Windows pending_close;
|
|
_pending_close.swap(pending_close);
|
|
for (wi = pending_close.begin(); wi != pending_close.end(); ++wi) {
|
|
GraphicsOutput *win = (*wi);
|
|
win->set_close_now();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if any of the GSG's on this thread's draw list are done (they
|
|
* have no outstanding pointers other than this one), or false if all of them
|
|
* are still good.
|
|
*/
|
|
bool GraphicsEngine::WindowRenderer::
|
|
any_done_gsgs() const {
|
|
GSGs::const_iterator gi;
|
|
for (gi = _gsgs.begin(); gi != _gsgs.end(); ++gi) {
|
|
if ((*gi)->get_ref_count() == 1) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
GraphicsEngine::RenderThread::
|
|
RenderThread(const string &name, GraphicsEngine *engine) :
|
|
Thread(name, "Main"),
|
|
WindowRenderer(name),
|
|
_engine(engine),
|
|
_cv_mutex(string("GraphicsEngine::RenderThread ") + name),
|
|
_cv_start(_cv_mutex),
|
|
_cv_done(_cv_mutex)
|
|
{
|
|
_thread_state = TS_wait;
|
|
}
|
|
|
|
/**
|
|
* The main loop for a particular render thread. The thread will process
|
|
* whatever cull or draw windows it has assigned to it.
|
|
*/
|
|
void GraphicsEngine::RenderThread::
|
|
thread_main() {
|
|
Thread *current_thread = Thread::get_current_thread();
|
|
|
|
MutexHolder holder(_cv_mutex);
|
|
while (true) {
|
|
nassertv(_cv_mutex.debug_is_locked());
|
|
|
|
switch (_thread_state) {
|
|
case TS_wait:
|
|
break;
|
|
|
|
case TS_do_frame:
|
|
do_pending(_engine, current_thread);
|
|
do_frame(_engine, current_thread);
|
|
break;
|
|
|
|
case TS_do_flip:
|
|
do_flip(_engine, current_thread);
|
|
break;
|
|
|
|
case TS_do_release:
|
|
do_pending(_engine, current_thread);
|
|
break;
|
|
|
|
case TS_do_windows:
|
|
do_windows(_engine, current_thread);
|
|
do_pending(_engine, current_thread);
|
|
break;
|
|
|
|
case TS_terminate:
|
|
do_pending(_engine, current_thread);
|
|
do_close(_engine, current_thread);
|
|
_thread_state = TS_done;
|
|
_cv_done.notify();
|
|
return;
|
|
|
|
case TS_done:
|
|
// Shouldn't be possible to get here.
|
|
nassertv(false);
|
|
return;
|
|
}
|
|
|
|
_thread_state = TS_wait;
|
|
_cv_done.notify();
|
|
|
|
{
|
|
PStatTimer timer(_wait_pcollector, current_thread);
|
|
_cv_start.wait();
|
|
}
|
|
}
|
|
}
|