From cfd18bb16f5dfbb8c9c6a447c40000355213dded Mon Sep 17 00:00:00 2001 From: rdb Date: Mon, 26 Dec 2022 19:25:53 +0100 Subject: [PATCH] display: Add support for asynchronous screenshot download --- direct/src/showbase/ShowBase.py | 15 +- panda/src/display/CMakeLists.txt | 2 + panda/src/display/config_display.cxx | 2 + panda/src/display/graphicsEngine.cxx | 5 + panda/src/display/graphicsOutput.cxx | 90 +++++- panda/src/display/graphicsOutput.h | 7 + panda/src/display/graphicsStateGuardian.cxx | 6 +- panda/src/display/graphicsStateGuardian.h | 3 +- panda/src/display/p3display_composite2.cxx | 1 + panda/src/display/screenshotRequest.I | 39 +++ panda/src/display/screenshotRequest.cxx | 104 +++++++ panda/src/display/screenshotRequest.h | 71 +++++ panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx | 13 +- panda/src/dxgsg9/dxGraphicsStateGuardian9.h | 3 +- panda/src/gles2gsg/gles2gsg.h | 7 + .../glstuff/glGraphicsStateGuardian_src.cxx | 293 +++++++++++++++++- .../src/glstuff/glGraphicsStateGuardian_src.h | 55 +++- panda/src/gsgbase/graphicsStateGuardianBase.h | 4 +- .../tinydisplay/tinyGraphicsStateGuardian.cxx | 7 +- .../tinydisplay/tinyGraphicsStateGuardian.h | 3 +- 20 files changed, 702 insertions(+), 28 deletions(-) create mode 100644 panda/src/display/screenshotRequest.I create mode 100644 panda/src/display/screenshotRequest.cxx create mode 100644 panda/src/display/screenshotRequest.h diff --git a/direct/src/showbase/ShowBase.py b/direct/src/showbase/ShowBase.py index d33e580ba7..ce02cfb78d 100644 --- a/direct/src/showbase/ShowBase.py +++ b/direct/src/showbase/ShowBase.py @@ -2713,7 +2713,7 @@ class ShowBase(DirectObject.DirectObject): def screenshot(self, namePrefix = 'screenshot', defaultFilename = 1, source = None, - imageComment=""): + imageComment="", blocking=True): """ Captures a screenshot from the main window or from the specified window or Texture and writes it to a filename in the current directory (or to a specified directory). @@ -2735,6 +2735,13 @@ class ShowBase(DirectObject.DirectObject): generated by makeCubeMap(), namePrefix should contain the hash mark ('#') character. + Normally, this call will block until the screenshot is fully + written. To write the screenshot in a background thread + instead, pass blocking = False. In this case, the return value + is a future that can be awaited. + + A "screenshot" event will be sent once the screenshot is saved. + :returns: The filename if successful, or None if there is a problem. """ @@ -2751,8 +2758,12 @@ class ShowBase(DirectObject.DirectObject): saved = source.write(filename, 0, 0, 1, 0) else: saved = source.write(filename) - else: + elif blocking: saved = source.saveScreenshot(filename, imageComment) + else: + request = source.saveAsyncScreenshot(filename, imageComment) + request.addDoneCallback(lambda fut, filename=filename: messenger.send('screenshot', [filename])) + return request if saved: # Announce to anybody that a screenshot has been taken diff --git a/panda/src/display/CMakeLists.txt b/panda/src/display/CMakeLists.txt index 56e9552d9b..e7662b9e98 100644 --- a/panda/src/display/CMakeLists.txt +++ b/panda/src/display/CMakeLists.txt @@ -27,6 +27,7 @@ set(P3DISPLAY_HEADERS windowHandle.I windowHandle.h windowProperties.I windowProperties.h renderBuffer.h + screenshotRequest.I screenshotRequest.h stereoDisplayRegion.I stereoDisplayRegion.h displaySearchParameters.h displayInformation.h @@ -61,6 +62,7 @@ set(P3DISPLAY_SOURCES parasiteBuffer.cxx windowHandle.cxx windowProperties.cxx + screenshotRequest.cxx stereoDisplayRegion.cxx subprocessWindow.cxx touchInfo.cxx diff --git a/panda/src/display/config_display.cxx b/panda/src/display/config_display.cxx index 1a5f975215..2c7ea8fd2e 100644 --- a/panda/src/display/config_display.cxx +++ b/panda/src/display/config_display.cxx @@ -29,6 +29,7 @@ #include "nativeWindowHandle.h" #include "parasiteBuffer.h" #include "pandaSystem.h" +#include "screenshotRequest.h" #include "stereoDisplayRegion.h" #include "subprocessWindow.h" #include "windowHandle.h" @@ -534,6 +535,7 @@ init_libdisplay() { MouseAndKeyboard::init_type(); NativeWindowHandle::init_type(); ParasiteBuffer::init_type(); + ScreenshotRequest::init_type(); StandardMunger::init_type(); StereoDisplayRegion::init_type(); #ifdef SUPPORT_SUBPROCESS_WINDOW diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx index af95350f82..061375611d 100644 --- a/panda/src/display/graphicsEngine.cxx +++ b/panda/src/display/graphicsEngine.cxx @@ -1419,6 +1419,8 @@ cull_and_draw_together(GraphicsEngine::Windows wlist, } if (win->begin_frame(GraphicsOutput::FM_render, current_thread)) { + win->copy_async_screenshot(); + if (win->is_any_clear_active()) { GraphicsStateGuardian *gsg = win->get_gsg(); PStatGPUTimer timer(gsg, win->get_clear_window_pcollector(), current_thread); @@ -1720,6 +1722,9 @@ draw_bins(const GraphicsEngine::Windows &wlist, Thread *current_thread) { // a current context for PStatGPUTimer to work. { PStatGPUTimer timer(gsg, win->get_draw_window_pcollector(), current_thread); + + win->copy_async_screenshot(); + if (win->is_any_clear_active()) { PStatGPUTimer timer(gsg, win->get_clear_window_pcollector(), current_thread); win->get_gsg()->push_group_marker("Clear"); diff --git a/panda/src/display/graphicsOutput.cxx b/panda/src/display/graphicsOutput.cxx index 462a298cc3..6d4e19981e 100644 --- a/panda/src/display/graphicsOutput.cxx +++ b/panda/src/display/graphicsOutput.cxx @@ -976,6 +976,43 @@ make_cube_map(const string &name, int size, NodePath &camera_rig, return buffer; } +/** + * Like save_screenshot, but performs both the texture transfer and the saving + * to disk in the background. Returns a future that can be awaited. + * + * This captures the frame that was last submitted by the App stage to the + * render_frame() call. This may not be the latest frame shown on the screen + * if the multi-threaded pipeline is used, in which case the request may take + * several frames extra to complete. + */ +PT(ScreenshotRequest) GraphicsOutput:: +save_async_screenshot(const Filename &filename, const std::string &image_comment) { + PT(ScreenshotRequest) request = get_async_screenshot(); + request->add_output_file(filename, image_comment); + return request; +} + +/** + * Used to obtain a new Texture object containing the previously rendered frame. + * Unlike get_screenshot, this works asynchronously, meaning that the contents + * are transferred in the background. Returns a future that can be awaited. + * + * This captures the frame that was last submitted by the App stage to the + * render_frame() call. This may not be the latest frame shown on the screen + * if the multi-threaded pipeline is used, in which case the request may take + * several frames extra to complete. + */ +PT(ScreenshotRequest) GraphicsOutput:: +get_async_screenshot() { + Thread *current_thread = Thread::get_current_thread(); + CDWriter cdata(_cycler, current_thread); + if (cdata->_screenshot_request == nullptr) { + PT(Texture) texture = new Texture("screenshot of " + get_name()); + cdata->_screenshot_request = new ScreenshotRequest(texture); + } + return cdata->_screenshot_request; +} + /** * Returns a PandaNode containing a square polygon. The dimensions are * (-1,0,-1) to (1,0,1). The texture coordinates are such that the texture of @@ -1468,6 +1505,56 @@ copy_to_textures() { return okflag; } +/** + * Do the necessary copies for the get_async_screenshot request. + */ +void GraphicsOutput:: +copy_async_screenshot() { + Thread *current_thread = Thread::get_current_thread(); + PT(ScreenshotRequest) request; + { + CDWriter cdata(_cycler, current_thread); + if (cdata->_screenshot_request == nullptr) { + return; + } + request = std::move(cdata->_screenshot_request); + cdata->_screenshot_request.clear(); + } + + // Make sure it is cleared from upstream stages as well. + OPEN_ITERATE_UPSTREAM_ONLY(_cycler, current_thread) { + CDStageWriter cdata(_cycler, pipeline_stage, current_thread); + if (cdata->_screenshot_request == request) { + cdata->_screenshot_request.clear(); + } + } + CLOSE_ITERATE_UPSTREAM_ONLY(_cycler); + + PStatTimer timer(_copy_texture_pcollector); + + RenderBuffer buffer = _gsg->get_render_buffer(get_draw_buffer_type(), + get_fb_properties()); + DisplayRegion *dr = _overlay_display_region; + + Texture *texture = request->get_result(); + + if (_fb_properties.is_stereo()) { + // We've got two texture views to copy. + texture->set_num_views(2); + + RenderBuffer left(_gsg, buffer._buffer_type & ~RenderBuffer::T_right); + RenderBuffer right(_gsg, buffer._buffer_type & ~RenderBuffer::T_left); + + _gsg->framebuffer_copy_to_ram(texture, 0, _target_tex_page, + dr, left, request); + _gsg->framebuffer_copy_to_ram(texture, 1, _target_tex_page, + dr, right, request); + } else { + _gsg->framebuffer_copy_to_ram(texture, 0, _target_tex_page, + dr, buffer, request); + } +} + /** * Generates a GeomVertexData for a texture card. */ @@ -1653,7 +1740,8 @@ CData(const GraphicsOutput::CData ©) : _active(copy._active), _one_shot_frame(copy._one_shot_frame), _active_display_regions(copy._active_display_regions), - _active_display_regions_stale(copy._active_display_regions_stale) + _active_display_regions_stale(copy._active_display_regions_stale), + _screenshot_request(copy._screenshot_request) { } diff --git a/panda/src/display/graphicsOutput.h b/panda/src/display/graphicsOutput.h index 7a210cb188..87d094e9aa 100644 --- a/panda/src/display/graphicsOutput.h +++ b/panda/src/display/graphicsOutput.h @@ -41,6 +41,7 @@ #include "pipelineCycler.h" #include "updateSeq.h" #include "asyncFuture.h" +#include "screenshotRequest.h" class PNMImage; class GraphicsEngine; @@ -239,6 +240,9 @@ PUBLISHED: const Filename &filename, const std::string &image_comment = ""); INLINE bool get_screenshot(PNMImage &image); INLINE PT(Texture) get_screenshot(); + PT(ScreenshotRequest) save_async_screenshot(const Filename &filename, + const std::string &image_comment = ""); + PT(ScreenshotRequest) get_async_screenshot(); NodePath get_texture_card(); @@ -298,6 +302,7 @@ protected: void prepare_for_deletion(); void promote_to_copy_texture(); bool copy_to_textures(); + void copy_async_screenshot(); INLINE void begin_frame_spam(FrameMode mode); INLINE void end_frame_spam(FrameMode mode); @@ -392,6 +397,8 @@ protected: int _one_shot_frame; ActiveDisplayRegions _active_display_regions; bool _active_display_regions_stale; + + PT(ScreenshotRequest) _screenshot_request; }; PipelineCycler _cycler; typedef CycleDataLockedReader CDLockedReader; diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx index 6f23741a15..34e49c42de 100644 --- a/panda/src/display/graphicsStateGuardian.cxx +++ b/panda/src/display/graphicsStateGuardian.cxx @@ -3037,11 +3037,15 @@ framebuffer_copy_to_texture(Texture *, int, int, const DisplayRegion *, * into system memory, not texture memory. Returns true on success, false on * failure. * + * If a future is given, the operation may be scheduled to occur in the + * background, in which case the texture will be passed as the result of the + * future when the operation is complete. + * * This completely redefines the ram image of the indicated texture. */ bool GraphicsStateGuardian:: framebuffer_copy_to_ram(Texture *, int, int, const DisplayRegion *, - const RenderBuffer &) { + const RenderBuffer &, ScreenshotRequest *) { return false; } diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h index bc6e175f8e..ee08075393 100644 --- a/panda/src/display/graphicsStateGuardian.h +++ b/panda/src/display/graphicsStateGuardian.h @@ -426,7 +426,8 @@ public: virtual bool framebuffer_copy_to_texture (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); virtual bool framebuffer_copy_to_ram - (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); + (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request = nullptr); virtual void bind_light(PointLight *light_obj, const NodePath &light, int light_id); diff --git a/panda/src/display/p3display_composite2.cxx b/panda/src/display/p3display_composite2.cxx index ef13357435..8f49915f30 100644 --- a/panda/src/display/p3display_composite2.cxx +++ b/panda/src/display/p3display_composite2.cxx @@ -9,6 +9,7 @@ #include "parasiteBuffer.cxx" #include "standardMunger.cxx" #include "touchInfo.cxx" +#include "screenshotRequest.cxx" #include "stereoDisplayRegion.cxx" #include "subprocessWindow.cxx" #ifdef IS_OSX diff --git a/panda/src/display/screenshotRequest.I b/panda/src/display/screenshotRequest.I new file mode 100644 index 0000000000..27c8d0affe --- /dev/null +++ b/panda/src/display/screenshotRequest.I @@ -0,0 +1,39 @@ +/** + * 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 screenshotRequest.I + * @author rdb + * @date 2022-12-26 + */ + +/** + * + */ +INLINE ScreenshotRequest:: +ScreenshotRequest(Texture *tex) : + _frame_number(ClockObject::get_global_clock()->get_frame_count()) { + _result = tex; + _result_ref = tex; +} + + +/** + * Returns the frame number in which the request originated. + */ +INLINE int ScreenshotRequest:: +get_frame_number() const { + return _frame_number; +} + +/** + * Returns the resulting texture. Can always be called. + */ +INLINE Texture *ScreenshotRequest:: +get_result() const { + return (Texture *)_result; +} diff --git a/panda/src/display/screenshotRequest.cxx b/panda/src/display/screenshotRequest.cxx new file mode 100644 index 0000000000..bb05b448c6 --- /dev/null +++ b/panda/src/display/screenshotRequest.cxx @@ -0,0 +1,104 @@ +/** + * 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 screenshotRequest.cxx + * @author rdb + * @date 2022-12-26 + */ + +#include "screenshotRequest.h" +#include "lightMutexHolder.h" +#include "pnmImage.h" +#include "texture.h" + +TypeHandle ScreenshotRequest::_type_handle; + +/** + * + */ +void ScreenshotRequest:: +set_view_data(int view, const void *ptr) { + const int z = 0; + + Texture *tex = get_result(); + PTA_uchar new_image = tex->modify_ram_image(); + unsigned char *image_ptr = new_image.p(); + size_t image_size = tex->get_ram_image_size(); + if (z >= 0 || view > 0) { + image_size = tex->get_expected_ram_page_size(); + if (z >= 0) { + image_ptr += z * image_size; + } + if (view > 0) { + image_ptr += (view * tex->get_z_size()) * image_size; + nassertd(view < tex->get_num_views()) { + if (set_future_state(FS_cancelled)) { + notify_done(false); + } + return; + } + } + } + memcpy(image_ptr, ptr, image_size); +} + +/** + * + */ +void ScreenshotRequest:: +finish() { + Texture *tex = get_result(); + + ++_got_num_views; + if (_got_num_views < tex->get_num_views()) { + return; + } + + { + LightMutexHolder holder(_lock); + if (!_output_files.empty()) { + PNMImage image; + tex->store(image); + + for (const auto &item : _output_files) { + image.set_comment(item.second); + image.write(item.first); + } + } + + AsyncFuture::set_result(tex); + _output_files.clear(); + + if (!set_future_state(FS_finished)) { + return; + } + } + + notify_done(true); +} + +/** + * Adds a filename to write the screenshot to when it is available. If the + * request is already done, performs the write synchronously. + */ +void ScreenshotRequest:: +add_output_file(const Filename &filename, const std::string &image_comment) { + if (!done()) { + LightMutexHolder holder(_lock); + if (!done()) { + _output_files[filename] = image_comment; + return; + } + } + // Was already done, write it right away. + Texture *tex = get_result(); + PNMImage image; + tex->store(image); + image.set_comment(image_comment); + image.write(filename); +} diff --git a/panda/src/display/screenshotRequest.h b/panda/src/display/screenshotRequest.h new file mode 100644 index 0000000000..4d56cf73bf --- /dev/null +++ b/panda/src/display/screenshotRequest.h @@ -0,0 +1,71 @@ +/** + * 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 screenshotRequest.h + * @author rdb + * @date 2022-12-26 + */ + +#ifndef SCREENSHOTREQUEST_H +#define SCREENSHOTREQUEST_H + +#include "pandabase.h" + +#include "asyncFuture.h" +#include "filename.h" +#include "lightMutex.h" +#include "pmap.h" + +/** + * A class representing an asynchronous request to save a screenshot. + */ +class EXPCL_PANDA_PGRAPH ScreenshotRequest : public AsyncFuture { +public: + INLINE ScreenshotRequest(Texture *tex); + + INLINE int get_frame_number() const; + INLINE Texture *get_result() const; + + void set_view_data(int view, const void *ptr); + void finish(); + +PUBLISHED: + void add_output_file(const Filename &filename, + const std::string &image_comment = ""); + +private: + // It's possible to call save_screenshot multiple times in the same frame, so + // rather than have to store a vector of request objects, we just allow + // storing multiple filenames to handle this corner case. + LightMutex _lock; + pmap _output_files; + int _got_num_views = 0; + + int _frame_number = 0; + +public: + static TypeHandle get_class_type() { + return _type_handle; + } + static void init_type() { + AsyncFuture::init_type(); + register_type(_type_handle, "ScreenshotRequest", + AsyncFuture::get_class_type()); + } + virtual TypeHandle get_type() const { + return get_class_type(); + } + virtual TypeHandle force_init_type() {init_type(); return get_class_type();} + +private: + static TypeHandle _type_handle; +}; + +#include "screenshotRequest.I" + +#endif diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx index 1452c59252..be3cfb14d6 100644 --- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx +++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx @@ -1988,8 +1988,17 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, */ bool DXGraphicsStateGuardian9:: framebuffer_copy_to_ram(Texture *tex, int view, int z, - const DisplayRegion *dr, const RenderBuffer &rb) { - return do_framebuffer_copy_to_ram(tex, view, z, dr, rb, false); + const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request) { + bool success = do_framebuffer_copy_to_ram(tex, view, z, dr, rb, false); + if (request != nullptr) { + if (success) { + request->finish(); + } else { + request->cancel(); + } + } + return success; } /** diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.h b/panda/src/dxgsg9/dxGraphicsStateGuardian9.h index eda2a5c233..01fa8cf981 100644 --- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.h +++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.h @@ -127,7 +127,8 @@ public: const RenderBuffer &rb); virtual bool framebuffer_copy_to_ram(Texture *tex, int view, int z, const DisplayRegion *dr, - const RenderBuffer &rb); + const RenderBuffer &rb, + ScreenshotRequest *request); bool do_framebuffer_copy_to_ram(Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb, diff --git a/panda/src/gles2gsg/gles2gsg.h b/panda/src/gles2gsg/gles2gsg.h index 044ac72991..d5c77bf6b9 100644 --- a/panda/src/gles2gsg/gles2gsg.h +++ b/panda/src/gles2gsg/gles2gsg.h @@ -177,6 +177,7 @@ typedef char GLchar; #define GL_READ_ONLY 0x88B8 #define GL_WRITE_ONLY 0x88B9 #define GL_READ_WRITE 0x88BA +#define GL_PIXEL_PACK_BUFFER 0x88EB #define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF #define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35 #define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36 @@ -253,6 +254,12 @@ typedef char GLchar; #define GL_UNSIGNED_INT_IMAGE_3D 0x9064 #define GL_UNSIGNED_INT_IMAGE_CUBE 0x9066 #define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069 +#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 +#define GL_UNSIGNALED 0x9118 +#define GL_SIGNALED 0x9119 +#define GL_ALREADY_SIGNALED 0x911A +#define GL_TIMEOUT_EXPIRED 0x911B +#define GL_CONDITION_SATISFIED 0x911C #define GL_COMPUTE_SHADER 0x91B9 #define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310 #define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311 diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx index 95078a71e2..702da1ce2c 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx @@ -93,6 +93,8 @@ PStatCollector CLP(GraphicsStateGuardian)::_texture_update_pcollector("Draw:Upda PStatCollector CLP(GraphicsStateGuardian)::_fbo_bind_pcollector("Draw:Bind FBO"); PStatCollector CLP(GraphicsStateGuardian)::_check_error_pcollector("Draw:Check errors"); PStatCollector CLP(GraphicsStateGuardian)::_check_residency_pcollector("*:PStats:Check residency"); +PStatCollector CLP(GraphicsStateGuardian)::_wait_fence_pcollector("Wait:Fence"); +PStatCollector CLP(GraphicsStateGuardian)::_copy_texture_finish_pcollector("Draw:Copy texture:Finish"); #if defined(HAVE_CG) && !defined(OPENGLES) AtomicAdjust::Integer CLP(GraphicsStateGuardian)::_num_gsgs_with_cg_contexts = 0; @@ -164,6 +166,10 @@ null_glPolygonOffsetClamp(GLfloat factor, GLfloat units, GLfloat clamp) { } #endif +static void APIENTRY +null_glMemoryBarrier(GLbitfield barriers) { +} + #ifndef OPENGLES_1 // We have a default shader that will be applied when there isn't any shader // applied (e.g. if it failed to compile). We need this because OpenGL ES @@ -507,7 +513,9 @@ int CLP(GraphicsStateGuardian)::get_driver_shader_version_minor() { return _gl_s CLP(GraphicsStateGuardian):: CLP(GraphicsStateGuardian)(GraphicsEngine *engine, GraphicsPipe *pipe) : GraphicsStateGuardian(gl_coordinate_system, engine, pipe), - _renderbuffer_residency(get_prepared_objects()->get_name(), "renderbuffer") + _renderbuffer_residency(get_prepared_objects()->get_name(), "renderbuffer"), + _active_ppbuffer_memory_pcollector("Graphics memory:" + get_prepared_objects()->get_name() + ":Active:ppbuffer"), + _inactive_ppbuffer_memory_pcollector("Graphics memory:" + get_prepared_objects()->get_name() + ":Inactive:ppbuffer") { _error_count = 0; _last_error_check = -1.0; @@ -1685,13 +1693,18 @@ reset() { if (is_at_least_gles_version(3, 0)) { _glMapBufferRange = (PFNGLMAPBUFFERRANGEEXTPROC) get_extension_func("glMapBufferRange"); - - } else if (has_extension("GL_EXT_map_buffer_range")) { + _glUnmapBuffer = (PFNGLUNMAPBUFFERPROC) + get_extension_func("glUnmapBuffer"); + } + else if (has_extension("GL_EXT_map_buffer_range")) { _glMapBufferRange = (PFNGLMAPBUFFERRANGEEXTPROC) get_extension_func("glMapBufferRangeEXT"); - - } else { + _glUnmapBuffer = (PFNGLUNMAPBUFFERPROC) + get_extension_func("glUnmapBufferOES"); + } + else { _glMapBufferRange = nullptr; + _glUnmapBuffer = nullptr; } #else // Check for various advanced buffer management features. @@ -2891,6 +2904,28 @@ reset() { is_at_least_gl_version(3, 3) || has_extension("GL_ARB_blend_func_extended"); #endif +#ifndef OPENGLES + if (is_at_least_gl_version(3, 2) || has_extension("GL_ARB_sync")) { + _glFenceSync = (PFNGLFENCESYNCPROC)get_extension_func("glFenceSync"); + _glDeleteSync = (PFNGLDELETESYNCPROC)get_extension_func("glDeleteSync"); + _glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC)get_extension_func("glClientWaitSync"); + _glGetSynciv = (PFNGLGETSYNCIVPROC)get_extension_func("glGetSynciv"); + } +#elif !defined(OPENGLES_1) + if (is_at_least_gles_version(3, 0)) { + _glFenceSync = (PFNGLFENCESYNCPROC)get_extension_func("glFenceSync"); + _glDeleteSync = (PFNGLDELETESYNCPROC)get_extension_func("glDeleteSync"); + _glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC)get_extension_func("glClientWaitSync"); + _glGetSynciv = (PFNGLGETSYNCIVPROC)get_extension_func("glGetSynciv"); + } + else if (has_extension("GL_APPLE_sync")) { + _glFenceSync = (PFNGLFENCESYNCPROC)get_extension_func("glFenceSyncAPPLE"); + _glDeleteSync = (PFNGLDELETESYNCPROC)get_extension_func("glDeleteSyncAPPLE"); + _glClientWaitSync = (PFNGLCLIENTWAITSYNCPROC)get_extension_func("glClientWaitSyncAPPLE"); + _glGetSynciv = (PFNGLGETSYNCIVPROC)get_extension_func("glGetSyncivAPPLE"); + } +#endif + #ifdef OPENGLES _edge_clamp = GL_CLAMP_TO_EDGE; #else @@ -3137,7 +3172,7 @@ reset() { } else { _glBindImageTexture = nullptr; - _glMemoryBarrier = nullptr; + _glMemoryBarrier = null_glMemoryBarrier; } #endif // !OPENGLES_1 @@ -4162,6 +4197,10 @@ begin_frame(Thread *current_thread) { _primitive_batches_display_list_pcollector.clear_level(); #endif + if (!_async_ram_copies.empty()) { + finish_async_framebuffer_ram_copies(); + } + #if defined(DO_PSTATS) && !defined(OPENGLES) int frame_number = ClockObject::get_global_clock()->get_frame_count(current_thread); if (_current_frame_timing == nullptr || @@ -4350,6 +4389,38 @@ end_frame(Thread *current_thread) { } #endif // OPENGLES +#ifndef OPENGLES_1 + if (!_deleted_buffers.empty()) { + GLuint *indices = (GLuint *)alloca(sizeof(GLuint *) * _deleted_buffers.size()); + size_t num_indices = 0; + DeletedBuffers::iterator it = _deleted_buffers.begin(); + while (it != _deleted_buffers.end()) { + DeletedBuffer &buffer = *it; + if (!_supports_buffer_storage && buffer._mapped_pointer != nullptr) { + _glBindBuffer(GL_PIXEL_PACK_BUFFER, buffer._index); + _glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + buffer._mapped_pointer = nullptr; + } + if (++buffer._age > 2) { + indices[num_indices++] = buffer._index; + it = _deleted_buffers.erase(it); + _inactive_ppbuffer_memory_pcollector.sub_level(buffer._size); + } else { + ++it; + } + } + if (!_supports_buffer_storage) { + _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + } + if (num_indices > 0) { + _glDeleteBuffers(num_indices, indices); + } + } + + _active_ppbuffer_memory_pcollector.flush_level(); + _inactive_ppbuffer_memory_pcollector.flush_level(); +#endif + #ifndef NDEBUG if (_check_errors || (_supports_debug && gl_debug)) { report_my_gl_errors(); @@ -6573,6 +6644,72 @@ record_deleted_display_list(GLuint index) { _deleted_display_lists.push_back(index); } +#ifndef OPENGLES_1 +/** + * Creates a new buffer for client access. It is bound when this returns. + * If persistent mapping is possible, mapped_ptr will be filled in with a + * pointer to the mapped data. + */ +void CLP(GraphicsStateGuardian):: +bind_new_client_buffer(GLuint &index, void *&mapped_ptr, GLenum target, size_t size) { + _active_ppbuffer_memory_pcollector.add_level(size); + + { + // Start at the end, because removing near the end is cheaper. + LightMutexHolder holder(_lock); + size_t i = _deleted_buffers.size(); + while (i > 1) { + --i; + DeletedBuffer &buffer = _deleted_buffers[i]; + if (buffer._size == size) { + index = buffer._index; + mapped_ptr = buffer._mapped_pointer; + _glBindBuffer(target, buffer._index); + if (!_supports_buffer_storage && mapped_ptr != nullptr) { + // Need to unmap it before we can use it. + _glUnmapBuffer(target); + mapped_ptr = nullptr; + } + _deleted_buffers.erase(_deleted_buffers.begin() + i); + _inactive_ppbuffer_memory_pcollector.sub_level(size); + return; + } + } + } + + _glGenBuffers(1, &index); + _glBindBuffer(target, index); +#ifndef OPENGLES + if (_supports_buffer_storage) { + // Map persistently, we already use fences to synchronize access anyway. + _glBufferStorage(target, size, nullptr, GL_MAP_READ_BIT | + GL_CLIENT_STORAGE_BIT | GL_MAP_PERSISTENT_BIT); + mapped_ptr = _glMapBufferRange(target, 0, size, + GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT); + } else +#endif + { + //XXX does it matter what usage hint we pass here? None seem to fit well. + _glBufferData(target, size, nullptr, GL_DYNAMIC_DRAW); + mapped_ptr = nullptr; + } +} + +/** + * Called when the given buffer, as returned by bind_new_client_buffer, is no + * longer needed. + */ +void CLP(GraphicsStateGuardian):: +release_client_buffer(GLuint index, void *mapped_ptr, size_t size) { + // This may be called from any thread, so we can't make OpenGL calls here + // (like unmapping the buffer). + LightMutexHolder holder(_lock); + _deleted_buffers.push_back({index, 0, mapped_ptr, size}); + _active_ppbuffer_memory_pcollector.sub_level(size); + _inactive_ppbuffer_memory_pcollector.add_level(size); +} +#endif // !OPENGLES_1 + /** * Creates a new retained-mode representation of the given data, and returns a * newly-allocated VertexBufferContext pointer to reference it. It is the @@ -7567,7 +7704,6 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, return true; } - /** * Copy the pixels within the indicated display region from the framebuffer * into system memory, not texture memory. Returns true on success, false on @@ -7577,7 +7713,8 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, */ bool CLP(GraphicsStateGuardian):: framebuffer_copy_to_ram(Texture *tex, int view, int z, - const DisplayRegion *dr, const RenderBuffer &rb) { + const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request) { nassertr(tex != nullptr && dr != nullptr, false); set_read_buffer(rb._buffer_type); glPixelStorei(GL_PACK_ALIGNMENT, 1); @@ -7868,12 +8005,23 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, } #endif // NDEBUG - unsigned char *image_ptr = tex->modify_ram_image(); - size_t image_size = tex->get_ram_image_size(); - if (z >= 0 || view > 0) { - image_size = tex->get_expected_ram_page_size(); + size_t image_size = tex->get_expected_ram_page_size(); + unsigned char *image_ptr = nullptr; +#ifndef OPENGLES_1 + GLuint pbo = 0; + void *mapped_ptr = nullptr; + if (request != nullptr) { + nassertr(z <= 0, false); + image_size *= tex->get_z_size(); + bind_new_client_buffer(pbo, mapped_ptr, GL_PIXEL_PACK_BUFFER, image_size); + } else +#endif + { + image_ptr = tex->modify_ram_image(); if (z >= 0) { image_ptr += z * image_size; + } else { + image_size = tex->get_ram_image_size(); } if (view > 0) { image_ptr += (view * tex->get_z_size()) * image_size; @@ -7884,9 +8032,22 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, glReadPixels(xo, yo, w, h, external_format, get_component_type(component_type), image_ptr); - // We may have to reverse the byte ordering of the image if GL didn't do it - // for us. +#ifndef OPENGLES_1 + if (request != nullptr) { + _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); +#ifndef OPENGLES + if (_supports_buffer_storage) { + _glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT); + } +#endif + GLsync fence = _glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + _async_ram_copies.push_back({request, pbo, fence, external_format, + view, mapped_ptr, image_size}); + } else +#endif if (external_format == GL_RGBA || external_format == GL_RGB) { + // We may have to reverse the byte ordering of the image if GL didn't do it + // for us. PTA_uchar new_image; const unsigned char *result = fix_component_ordering(new_image, image_ptr, image_size, @@ -7896,10 +8057,114 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, } } +#ifdef OPENGLES_1 + if (request != nullptr) { + request->finish(); + } +#endif + report_my_gl_errors(); return true; } +/** + * Finishes all asynchronous framebuffer-copy-to-ram operations. + */ +void CLP(GraphicsStateGuardian):: +finish_async_framebuffer_ram_copies(bool force) { +#ifndef OPENGLES_1 + if (_async_ram_copies.empty()) { + return; + } + + //XXX having a fixed number of threads is not a great idea. We ought to have + // a common thread pool that is sized based on the available number of CPUs. +#ifdef HAVE_THREADS + AsyncTaskManager *task_mgr = AsyncTaskManager::get_global_ptr(); + static AsyncTaskChain *chain = task_mgr->make_task_chain("texture_download", 2, TP_low); +#endif + + PStatTimer timer(_copy_texture_finish_pcollector); + + if (force) { + // Just wait for the last fence, the rest must be complete too then. + PStatTimer timer(_wait_fence_pcollector); + GLsync fence = _async_ram_copies.back()._fence; + _glClientWaitSync(fence, 0, (GLuint64)-1); + } + + while (!_async_ram_copies.empty()) { + AsyncRamCopy © = _async_ram_copies.front(); + if (!force) { + GLenum result = _glClientWaitSync(copy._fence, 0, 0); + if (result != GL_ALREADY_SIGNALED && result != GL_CONDITION_SATISFIED) { + // Not yet done. The rest must not yet be done then, either. + break; + } + } + _glDeleteSync(copy._fence); + + GLuint pbo = copy._pbo; + int view = copy._view; + PT(ScreenshotRequest) request = std::move(copy._request); + GLuint external_format = copy._external_format; + void *mapped_ptr = copy._mapped_pointer; + size_t size = copy._size; + + if (mapped_ptr == nullptr) { + _glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo); +#ifdef OPENGLES + // There is neither glMapBuffer nor persistent mapping in OpenGL ES + mapped_ptr = _glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, size, GL_MAP_READ_BIT); +#else + // If we get here in desktop GL, we must not have persistent mapping + nassertv(!_supports_buffer_storage); + mapped_ptr = _glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); +#endif + } + + // Do the memcpy in the background, since it can be slow. + auto func = [=](AsyncTask *task) { + const unsigned char *result = (unsigned char *)mapped_ptr; + PTA_uchar new_image; + if (external_format == GL_RGBA || external_format == GL_RGB) { + // We may have to reverse the byte ordering of the image if GL didn't do + // it for us. + result = fix_component_ordering(new_image, result, size, + external_format, request->get_result()); + } + request->set_view_data(view, result); + + // Finishing can take a long time, release the client buffer first so it + // can be reused for the next screenshot. + this->release_client_buffer(pbo, mapped_ptr, size); + request->finish(); + return AsyncTask::DS_done; + }; +#ifdef HAVE_THREADS + // We assign a sort value based on the originating frame number, so that + // earlier frames will be processed before subsequent frames, but we don't + // make it unique for every frame, which would kill concurrency. + int frame_number = request->get_frame_number(); + chain->add(std::move(func), "screenshot", frame_number >> 3, -(frame_number & ((1 << 3) - 1))); +#else + func(nullptr); +#endif + + _async_ram_copies.pop_front(); + + // If there is 1 remaining, save it for next frame. This helps prevent an + // inconsistent frame rate when the number of fetched frames alternates + // between 0 and 2, which can settle into a stable feedback loop. + if (!force && _async_ram_copies.size() == 1) { + break; + } + } + + _glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); +#endif +} + #ifdef SUPPORT_FIXED_FUNCTION /** * diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h index 911b182c31..f12803b12c 100644 --- a/panda/src/glstuff/glGraphicsStateGuardian_src.h +++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h @@ -142,6 +142,7 @@ typedef void (APIENTRYP PFNGLDELETEVERTEXARRAYSPROC) (GLsizei n, const GLuint *a typedef void (APIENTRYP PFNGLGENVERTEXARRAYSPROC) (GLsizei n, GLuint *arrays); typedef void (APIENTRYP PFNGLBLENDEQUATIONSEPARATEPROC) (GLenum modeRGB, GLenum modeAlpha); typedef void (APIENTRYP PFNGLBLENDFUNCSEPARATEPROC) (GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha); +typedef GLboolean (APIENTRYP PFNGLUNMAPBUFFERPROC) (GLenum target); #ifndef OPENGLES_1 // GLSL shader functions @@ -231,6 +232,13 @@ typedef void (APIENTRYP PFNGLBUFFERSTORAGEPROC) (GLenum target, GLsizeiptr size, typedef void (APIENTRYP PFNGLBINDIMAGETEXTUREPROC) (GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); typedef void (APIENTRYP PFNGLCLEARTEXIMAGEPROC) (GLuint texture, GLint level, GLenum format, GLenum type, const void *data); typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC) (GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data); +typedef GLsync (APIENTRYP PFNGLFENCESYNCPROC) (GLenum condition, GLbitfield flags); +typedef GLboolean (APIENTRYP PFNGLISSYNCPROC) (GLsync sync); +typedef void (APIENTRYP PFNGLDELETESYNCPROC) (GLsync sync); +typedef GLenum (APIENTRYP PFNGLCLIENTWAITSYNCPROC) (GLsync sync, GLbitfield flags, GLuint64 timeout); +typedef void (APIENTRYP PFNGLWAITSYNCPROC) (GLsync sync, GLbitfield flags, GLuint64 timeout); +typedef void (APIENTRYP PFNGLGETINTEGER64VPROC) (GLenum pname, GLint64 *data); +typedef void (APIENTRYP PFNGLGETSYNCIVPROC) (GLsync sync, GLenum pname, GLsizei bufSize, GLsizei *length, GLint *values); #endif // OPENGLES_1 #ifndef OPENGLES typedef void (APIENTRYP PFNGLBINDTEXTURESPROC) (GLuint first, GLsizei count, const GLuint *textures); @@ -253,7 +261,6 @@ typedef void (APIENTRYP PFNGLVERTEXATTRIBL1UI64PROC) (GLuint index, GLuint64EXT typedef void (APIENTRYP PFNGLVERTEXATTRIBL1UI64VPROC) (GLuint index, const GLuint64EXT *v); typedef void (APIENTRYP PFNGLGETVERTEXATTRIBLUI64VPROC) (GLuint index, GLenum pname, GLuint64EXT *params); typedef void *(APIENTRYP PFNGLMAPBUFFERPROC) (GLenum target, GLenum access); -typedef GLboolean (APIENTRYP PFNGLUNMAPBUFFERPROC) (GLenum target); typedef void (APIENTRYP PFNGLGETBUFFERSUBDATAPROC) (GLenum target, GLintptr offset, GLsizeiptr size, void *data); #endif // OPENGLES #endif // __EDG__ @@ -353,6 +360,11 @@ public: virtual ShaderContext *prepare_shader(Shader *shader); virtual void release_shader(ShaderContext *sc); +#ifndef OPENGLES_1 + void bind_new_client_buffer(GLuint &index, void *&mapped_ptr, GLenum target, size_t size); + void release_client_buffer(GLuint index, void *mapped_ptr, size_t size); +#endif + void record_deleted_display_list(GLuint index); virtual VertexBufferContext *prepare_vertex_buffer(GeomVertexArrayData *data); @@ -403,7 +415,9 @@ public: virtual bool framebuffer_copy_to_texture (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); virtual bool framebuffer_copy_to_ram - (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); + (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request); + void finish_async_framebuffer_ram_copies(bool force = false); #ifdef SUPPORT_FIXED_FUNCTION void apply_fog(Fog *fog); @@ -880,6 +894,7 @@ public: #ifdef OPENGLES PFNGLMAPBUFFERRANGEEXTPROC _glMapBufferRange; + PFNGLUNMAPBUFFEROESPROC _glUnmapBuffer; #else PFNGLMAPBUFFERRANGEPROC _glMapBufferRange; #endif @@ -891,6 +906,8 @@ public: bool _supports_buffer_storage; PFNGLBUFFERSTORAGEPROC _glBufferStorage; +#else + static const bool _supports_buffer_storage = false; #endif bool _supports_blend_equation_separate; @@ -1088,6 +1105,13 @@ public: PFNGLSHADERSTORAGEBLOCKBINDINGPROC _glShaderStorageBlockBinding; #endif // !OPENGLES +#ifndef OPENGLES_1 + PFNGLFENCESYNCPROC _glFenceSync; + PFNGLDELETESYNCPROC _glDeleteSync; + PFNGLCLIENTWAITSYNCPROC _glClientWaitSync; + PFNGLGETSYNCIVPROC _glGetSynciv; +#endif + GLenum _edge_clamp; GLenum _border_clamp; GLenum _mirror_repeat; @@ -1109,6 +1133,17 @@ public: DeletedNames _deleted_display_lists; DeletedNames _deleted_queries; +#ifndef OPENGLES_1 + struct DeletedBuffer { + GLuint _index; + int _age; + void *_mapped_pointer; + size_t _size; + }; + typedef pvector DeletedBuffers; + DeletedBuffers _deleted_buffers; +#endif + #ifndef OPENGLES_1 // Stores textures for which memory bariers should be issued. typedef pset TextureSet; @@ -1165,8 +1200,22 @@ public: FrameTiming *_current_frame_timing = nullptr; #endif + struct AsyncRamCopy { + PT(ScreenshotRequest) _request; + GLuint _pbo; + GLsync _fence; + GLuint _external_format; + int _view; + void *_mapped_pointer; + size_t _size; + }; + pdeque _async_ram_copies; + BufferResidencyTracker _renderbuffer_residency; + PStatCollector _active_ppbuffer_memory_pcollector; + PStatCollector _inactive_ppbuffer_memory_pcollector; + static PStatCollector _load_display_list_pcollector; static PStatCollector _primitive_batches_display_list_pcollector; static PStatCollector _vertices_display_list_pcollector; @@ -1177,6 +1226,8 @@ public: static PStatCollector _fbo_bind_pcollector; static PStatCollector _check_error_pcollector; static PStatCollector _check_residency_pcollector; + static PStatCollector _wait_fence_pcollector; + static PStatCollector _copy_texture_finish_pcollector; public: virtual TypeHandle get_type() const { diff --git a/panda/src/gsgbase/graphicsStateGuardianBase.h b/panda/src/gsgbase/graphicsStateGuardianBase.h index 19c7140024..dd144cb859 100644 --- a/panda/src/gsgbase/graphicsStateGuardianBase.h +++ b/panda/src/gsgbase/graphicsStateGuardianBase.h @@ -28,6 +28,7 @@ class RenderBuffer; class GraphicsWindow; class NodePath; class GraphicsOutputBase; +class ScreenshotRequest; class VertexBufferContext; class IndexBufferContext; @@ -220,7 +221,8 @@ public: virtual bool framebuffer_copy_to_texture (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb)=0; virtual bool framebuffer_copy_to_ram - (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb)=0; + (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request = nullptr)=0; virtual CoordinateSystem get_internal_coordinate_system() const=0; diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx index 0e2af03bdd..17b7ac24d4 100644 --- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx +++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx @@ -1393,8 +1393,8 @@ framebuffer_copy_to_texture(Texture *tex, int view, int z, */ bool TinyGraphicsStateGuardian:: framebuffer_copy_to_ram(Texture *tex, int view, int z, - const DisplayRegion *dr, - const RenderBuffer &rb) { + const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request) { nassertr(tex != nullptr && dr != nullptr, false); int xo, yo, w, h; @@ -1465,6 +1465,9 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, fo += _c->zb->linesize / PSZB; } + if (request != nullptr) { + request->finish(); + } return true; } diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.h b/panda/src/tinydisplay/tinyGraphicsStateGuardian.h index c4ee0348ad..5f569fc1c5 100644 --- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.h +++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.h @@ -78,7 +78,8 @@ public: virtual bool framebuffer_copy_to_texture (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); virtual bool framebuffer_copy_to_ram - (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); + (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb, + ScreenshotRequest *request); virtual void set_state_and_transform(const RenderState *state, const TransformState *transform);