diff options
| author | Carl Hetherington <cth@carlh.net> | 2025-04-20 23:22:54 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2025-04-28 02:31:15 +0200 |
| commit | c61e1b24a888e8d79b43069e4ed9864ef53bc718 (patch) | |
| tree | a6f939f8a2c607ae42fb57e66fdb580b147a1f3b | |
| parent | 7f25fc20f432649759743848667a5e17b3c43c88 (diff) | |
Extract parts of GLVideoView to GLView.
| -rw-r--r-- | src/wx/gl_util.cc | 28 | ||||
| -rw-r--r-- | src/wx/gl_util.h | 1 | ||||
| -rw-r--r-- | src/wx/gl_video_view.cc | 429 | ||||
| -rw-r--r-- | src/wx/gl_video_view.h | 39 | ||||
| -rw-r--r-- | src/wx/gl_view.cc | 371 | ||||
| -rw-r--r-- | src/wx/gl_view.h | 138 | ||||
| -rw-r--r-- | src/wx/wscript | 1 |
7 files changed, 632 insertions, 375 deletions
diff --git a/src/wx/gl_util.cc b/src/wx/gl_util.cc index f4f079ffa..8116d4448 100644 --- a/src/wx/gl_util.cc +++ b/src/wx/gl_util.cc @@ -31,6 +31,34 @@ using std::string; /* This will only build on an new-enough wxWidgets: see the comment in gl_video_view.h */ #if wxCHECK_VERSION(3,1,0) +GLuint +compile_shader(GLenum type, char const* source) +{ + auto shader = glCreateShader(type); + DCPOMATIC_ASSERT(shader); + GLchar const * src[] = { static_cast<GLchar const *>(source) }; + glShaderSource(shader, 1, src, nullptr); + check_gl_error("glShaderSource"); + glCompileShader(shader); + check_gl_error("glCompileShader"); + GLint ok; + glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); + if (!ok) { + GLint log_length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length); + string log; + if (log_length > 0) { + std::vector<char> log_char(log_length); + glGetShaderInfoLog(shader, log_length, nullptr, log_char.data()); + log = string(log_char.data()); + } + glDeleteShader(shader); + boost::throw_exception(GLError(String::compose("Could not compile shader (%1)", log).c_str(), -1)); + } + return shader; +} + + void check_gl_error(char const * last) { diff --git a/src/wx/gl_util.h b/src/wx/gl_util.h index 83dbf06d4..79b8b2306 100644 --- a/src/wx/gl_util.h +++ b/src/wx/gl_util.h @@ -41,6 +41,7 @@ #include <GL/wglext.h> #endif +extern GLuint compile_shader(GLenum type, char const* source); extern void check_gl_error(char const* last); #endif diff --git a/src/wx/gl_video_view.cc b/src/wx/gl_video_view.cc index 9d85b4c78..424cde3b3 100644 --- a/src/wx/gl_video_view.cc +++ b/src/wx/gl_video_view.cc @@ -67,120 +67,6 @@ using namespace boost::placeholders; #endif -GLVideoView::GLVideoView(FilmViewer* viewer, wxWindow *parent) - : VideoView(viewer) - , _context(nullptr) - , _rec2020(false) - , _vsync_enabled(false) - , _playing(false) - , _one_shot(false) -{ - wxGLAttributes attributes; - /* We don't need a depth buffer, and indeed there is apparently a bug with Windows/Intel HD 630 - * which puts green lines over the OpenGL display if you have a non-zero depth buffer size. - * https://community.intel.com/t5/Graphics/Request-for-details-on-Intel-HD-630-green-lines-in-OpenGL-apps/m-p/1202179 - */ - attributes.PlatformDefaults().MinRGBA(8, 8, 8, 8).DoubleBuffer().Depth(0).EndList(); - _canvas = new wxGLCanvas( - parent, attributes, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE - ); - _canvas->Bind(wxEVT_PAINT, boost::bind(&GLVideoView::update, this)); - _canvas->Bind(wxEVT_SIZE, boost::bind(&GLVideoView::size_changed, this, _1)); - - _canvas->Bind(wxEVT_TIMER, boost::bind(&GLVideoView::check_for_butler_errors, this)); - _timer.reset(new wxTimer(_canvas)); - _timer->Start(2000); -} - - -void -GLVideoView::size_changed(wxSizeEvent const& ev) -{ - auto const scale = _canvas->GetDPIScaleFactor(); - int const width = std::round(ev.GetSize().GetWidth() * scale); - int const height = std::round(ev.GetSize().GetHeight() * scale); - _canvas_size = { width, height }; - LOG_GENERAL("GLVideoView canvas size changed to %1x%2", width, height); - Sized(); -} - - -GLVideoView::~GLVideoView() -{ - boost::this_thread::disable_interruption dis; - - try { - _thread.interrupt(); - _thread.join(); - } catch (...) {} -} - -void -GLVideoView::check_for_butler_errors() -{ - if (!_viewer->butler()) { - return; - } - - try { - _viewer->butler()->rethrow(); - } catch (DecodeError& e) { - error_dialog(get(), std_to_wx(e.what())); - } catch (dcp::ReadError& e) { - error_dialog(get(), wxString::Format(_("Could not read DCP: %s"), std_to_wx(e.what()))); - } - - LOG_DEBUG_PLAYER("Latency %1", _viewer->average_latency()); -} - - -/** Called from the UI thread */ -void -GLVideoView::update() -{ - if (!_canvas->IsShownOnScreen()) { - return; - } - - /* It appears important to do this from the GUI thread; if we do it from the GL thread - * on Linux we get strange failures to create the context for any version of GL higher - * than 3.2. - */ - ensure_context(); - -#ifdef DCPOMATIC_OSX - /* macOS gives errors if we don't do this (and therefore [NSOpenGLContext setView:]) from the main thread */ - if (!_setup_shaders_done) { - setup_shaders(); - _setup_shaders_done = true; - } -#endif - - if (!_thread.joinable()) { - _thread = boost::thread(boost::bind(&GLVideoView::thread, this)); - } - - request_one_shot(); - - rethrow(); -} - - -static constexpr char vertex_source[] = -"#version 330 core\n" -"\n" -"layout (location = 0) in vec3 in_pos;\n" -"layout (location = 1) in vec2 in_tex_coord;\n" -"\n" -"out vec2 TexCoord;\n" -"\n" -"void main()\n" -"{\n" -" gl_Position = vec4(in_pos, 1.0);\n" -" TexCoord = in_tex_coord;\n" -"}\n"; - - /* Bicubic interpolation stolen from https://stackoverflow.com/questions/13501081/efficient-bicubic-filtering-code-in-glsl */ static constexpr char fragment_source[] = "#version 330 core\n" @@ -301,6 +187,47 @@ static constexpr char fragment_source[] = "}\n"; +GLVideoView::GLVideoView(FilmViewer* viewer, wxWindow *parent) + : VideoView(viewer) + , _view( + parent, + boost::bind(&GLVideoView::setup_shaders, this), + boost::bind(&GLVideoView::create_textures, this), + boost::bind(&GLVideoView::play_pre_draw, this), + boost::bind(&GLVideoView::draw, this), + boost::bind(&GLVideoView::play_post_draw, this), + fragment_source + ) + , _rec2020(false) + , _meters(parent, viewer) +{ + _view.Sized.connect(boost::ref(Sized)); + + _view.canvas()->Bind(wxEVT_TIMER, boost::bind(&GLVideoView::check_for_butler_errors, this)); + _timer.reset(new wxTimer(_view.canvas())); + _timer->Start(2000); +} + + +void +GLVideoView::check_for_butler_errors() +{ + if (!_viewer->butler()) { + return; + } + + try { + _viewer->butler()->rethrow(); + } catch (DecodeError& e) { + error_dialog(get(), std_to_wx(e.what())); + } catch (dcp::ReadError& e) { + error_dialog(get(), wxString::Format(_("Could not read DCP: %s"), std_to_wx(e.what()))); + } + + LOG_DEBUG_PLAYER("Latency %1", _viewer->average_latency()); +} + + enum class FragmentType { OUTLINE_CONTENT = 0, @@ -313,20 +240,6 @@ enum class FragmentType }; -void -GLVideoView::ensure_context() -{ - if (!_context) { - wxGLContextAttrs attrs; - attrs.PlatformDefaults().CoreProfile().OGLVersion(4, 1).EndList(); - _context = new wxGLContext(_canvas, nullptr, &attrs); - if (!_context->IsOK()) { - boost::throw_exception(GLError("Making GL context", -1)); - } - } -} - - /* Offset and number of indices for the things in the indices array below */ static constexpr int indices_video_texture_offset = 0; static constexpr int indices_video_texture_number = 6; @@ -362,18 +275,6 @@ static constexpr int array_buffer_crop_guess_offset = array_buffer_outline_conte void GLVideoView::setup_shaders() { - DCPOMATIC_ASSERT(_canvas); - DCPOMATIC_ASSERT(_context); - auto r = _canvas->SetCurrent(*_context); - DCPOMATIC_ASSERT(r); - -#ifdef DCPOMATIC_WINDOWS - r = glewInit(); - if (r != GLEW_OK) { - boost::throw_exception(GLError(reinterpret_cast<char const*>(glewGetErrorString(r)))); - } -#endif - auto get_information = [this](GLenum name) { auto s = glGetString(name); if (s) { @@ -386,109 +287,30 @@ GLVideoView::setup_shaders() get_information(GL_VERSION); get_information(GL_SHADING_LANGUAGE_VERSION); - glGenVertexArrays(1, &_vao); - check_gl_error("glGenVertexArrays"); - GLuint vbo; - glGenBuffers(1, &vbo); - check_gl_error("glGenBuffers"); - GLuint ebo; - glGenBuffers(1, &ebo); - check_gl_error("glGenBuffers"); - - glBindVertexArray(_vao); - check_gl_error("glBindVertexArray"); - - glBindBuffer(GL_ARRAY_BUFFER, vbo); - check_gl_error("glBindBuffer"); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); - check_gl_error("glBindBuffer"); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); check_gl_error("glBufferData"); - /* position attribute to vertex shader (location = 0) */ - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), nullptr); - glEnableVertexAttribArray(0); - /* texture coord attribute to vertex shader (location = 1) */ - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), reinterpret_cast<void*>(3 * sizeof(float))); - glEnableVertexAttribArray(1); - check_gl_error("glEnableVertexAttribArray"); - - auto compile = [](GLenum type, char const* source) -> GLuint { - auto shader = glCreateShader(type); - DCPOMATIC_ASSERT(shader); - GLchar const * src[] = { static_cast<GLchar const *>(source) }; - glShaderSource(shader, 1, src, nullptr); - check_gl_error("glShaderSource"); - glCompileShader(shader); - check_gl_error("glCompileShader"); - GLint ok; - glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); - if (!ok) { - GLint log_length; - glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length); - string log; - if (log_length > 0) { - std::vector<char> log_char(log_length); - glGetShaderInfoLog(shader, log_length, nullptr, log_char.data()); - log = string(log_char.data()); - } - glDeleteShader(shader); - boost::throw_exception(GLError(String::compose("Could not compile shader (%1)", log).c_str(), -1)); - } - return shader; - }; - - auto vertex_shader = compile(GL_VERTEX_SHADER, vertex_source); - auto fragment_shader = compile(GL_FRAGMENT_SHADER, fragment_source); - - auto program = glCreateProgram(); - check_gl_error("glCreateProgram"); - glAttachShader(program, vertex_shader); - check_gl_error("glAttachShader"); - glAttachShader(program, fragment_shader); - check_gl_error("glAttachShader"); - glLinkProgram(program); - check_gl_error("glLinkProgram"); - GLint ok; - glGetProgramiv(program, GL_LINK_STATUS, &ok); - if (!ok) { - GLint log_length; - glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length); - string log; - if (log_length > 0) { - std::vector<char> log_char(log_length); - glGetProgramInfoLog(program, log_length, nullptr, log_char.data()); - log = string(log_char.data()); - } - glDeleteProgram(program); - boost::throw_exception(GLError(String::compose("Could not link shader (%1)", log).c_str(), -1)); - } - glDeleteShader(vertex_shader); - glDeleteShader(fragment_shader); - - glUseProgram(program); - auto texture_0 = glGetUniformLocation(program, "texture_sampler_0"); + auto texture_0 = glGetUniformLocation(_view.program(), "texture_sampler_0"); check_gl_error("glGetUniformLocation"); glUniform1i(texture_0, 0); check_gl_error("glUniform1i"); - auto texture_1 = glGetUniformLocation(program, "texture_sampler_1"); + auto texture_1 = glGetUniformLocation(_view.program(), "texture_sampler_1"); check_gl_error("glGetUniformLocation"); glUniform1i(texture_1, 1); check_gl_error("glUniform1i"); - auto texture_2 = glGetUniformLocation(program, "texture_sampler_2"); + auto texture_2 = glGetUniformLocation(_view.program(), "texture_sampler_2"); check_gl_error("glGetUniformLocation"); glUniform1i(texture_2, 2); check_gl_error("glUniform1i"); - auto texture_3 = glGetUniformLocation(program, "texture_sampler_3"); + auto texture_3 = glGetUniformLocation(_view.program(), "texture_sampler_3"); check_gl_error("glGetUniformLocation"); glUniform1i(texture_3, 3); check_gl_error("glUniform1i"); - _fragment_type = glGetUniformLocation(program, "type"); + _fragment_type = glGetUniformLocation(_view.program(), "type"); check_gl_error("glGetUniformLocation"); - set_outline_content_colour(program); - set_crop_guess_colour(program); + set_outline_content_colour(_view.program()); + set_crop_guess_colour(_view.program()); auto ublas_to_gl = [](boost::numeric::ublas::matrix<double> const& ublas, GLfloat* gl) { gl[0] = static_cast<float>(ublas(0, 0)); @@ -515,7 +337,7 @@ GLVideoView::setup_shaders() GLfloat gl_matrix[16]; ublas_to_gl(matrix, gl_matrix); - auto xyz_rec709_colour_conversion = glGetUniformLocation(program, "xyz_rec709_colour_conversion"); + auto xyz_rec709_colour_conversion = glGetUniformLocation(_view.program(), "xyz_rec709_colour_conversion"); check_gl_error("glGetUniformLocation"); glUniformMatrix4fv(xyz_rec709_colour_conversion, 1, GL_TRUE, gl_matrix); } @@ -528,7 +350,7 @@ GLVideoView::setup_shaders() GLfloat gl_matrix[16]; ublas_to_gl(product, gl_matrix); - auto rec2020_rec709_colour_conversion = glGetUniformLocation(program, "rec2020_rec709_colour_conversion"); + auto rec2020_rec709_colour_conversion = glGetUniformLocation(_view.program(), "rec2020_rec709_colour_conversion"); check_gl_error("glGetUniformLocation"); glUniformMatrix4fv(rec2020_rec709_colour_conversion, 1, GL_TRUE, gl_matrix); } @@ -571,12 +393,17 @@ GLVideoView::set_crop_guess_colour(GLuint program) void GLVideoView::draw() { + auto pv = player_video().first; + if (pv) { + set_image(pv); + } + auto pad = pad_colour(); glClearColor(pad.Red() / 255.0, pad.Green() / 255.0, pad.Blue() / 255.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); check_gl_error("glClear"); - auto const size = _canvas_size.load(); + auto const size = _view.size(); int const width = size.GetWidth(); int const height = size.GetHeight(); @@ -584,11 +411,6 @@ GLVideoView::draw() return; } - glViewport(0, 0, width, height); - check_gl_error("glViewport"); - - glBindVertexArray(_vao); - check_gl_error("glBindVertexArray"); if (_optimisation == Optimisation::MPEG2) { glUniform1i(_fragment_type, static_cast<GLint>(FragmentType::YUV420P_IMAGE)); } else if (_optimisation == Optimisation::JPEG2000) { @@ -618,10 +440,9 @@ GLVideoView::draw() check_gl_error("glDrawElements"); } - glFlush(); - check_gl_error("glFlush"); - - _canvas->SwapBuffers(); + if (pv) { + _viewer->image_changed(pv); + } } @@ -667,7 +488,7 @@ GLVideoView::set_image(shared_ptr<const PlayerVideo> pv) _subtitle_texture->set(text->image); } - auto const canvas_size = _canvas_size.load(); + auto const canvas_size = _view.size(); int const canvas_width = canvas_size.GetWidth(); int const canvas_height = canvas_size.GetHeight(); auto const inter_position = pv->inter_position(); @@ -814,22 +635,20 @@ void GLVideoView::start() { VideoView::start(); - - boost::mutex::scoped_lock lm(_playing_mutex); - _playing = true; - _thread_work_condition.notify_all(); + _view.start(); + _meters.start(); } void GLVideoView::stop() { - boost::mutex::scoped_lock lm(_playing_mutex); - _playing = false; + _view.stop(); + _meters.stop(); } void -GLVideoView::thread_playing() +GLVideoView::play_pre_draw() { if (length() != dcpomatic::DCPTime()) { auto const next = position() + one_video_frame(); @@ -840,117 +659,34 @@ GLVideoView::thread_playing() } get_next_frame(false); - set_image_and_draw(); } +} + +int +GLVideoView::play_post_draw() +{ while (true) { - optional<int> n = time_until_next_frame(); + auto const n = time_until_next_frame(); if (!n || *n > 5) { break; } get_next_frame(true); add_dropped(); } -} - - -void -GLVideoView::set_image_and_draw() -{ - auto pv = player_video().first; - if (pv) { - set_image(pv); - } - - draw(); - if (pv) { - _viewer->image_changed(pv); - } + return time_until_next_frame().get_value_or(0); } void -GLVideoView::thread() -try +GLVideoView::create_textures() { - start_of_thread("GLVideoView"); - -#if defined(DCPOMATIC_OSX) - /* Without this we see errors like - * ../src/osx/cocoa/glcanvas.mm(194): assert ""context"" failed in SwapBuffers(): should have current context [in thread 700006970000] - */ - WXGLSetCurrentContext(_context->GetWXGLContext()); -#else - if (!_setup_shaders_done) { - setup_shaders(); - _setup_shaders_done = true; - } -#endif - -#if defined(DCPOMATIC_LINUX) && defined(DCPOMATIC_HAVE_GLX_SWAP_INTERVAL_EXT) - if (_canvas->IsExtensionSupported("GLX_EXT_swap_control")) { - /* Enable vsync */ - Display* dpy = wxGetX11Display(); - glXSwapIntervalEXT(dpy, DefaultScreen(dpy), 1); - _vsync_enabled = true; - } -#endif - -#ifdef DCPOMATIC_WINDOWS - if (_canvas->IsExtensionSupported("WGL_EXT_swap_control")) { - /* Enable vsync */ - PFNWGLSWAPINTERVALEXTPROC swap = (PFNWGLSWAPINTERVALEXTPROC) wglGetProcAddress("wglSwapIntervalEXT"); - if (swap) { - swap(1); - _vsync_enabled = true; - } - } - -#endif - -#ifdef DCPOMATIC_OSX - /* Enable vsync */ - GLint swapInterval = 1; - CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &swapInterval); - _vsync_enabled = true; -#endif - for (int i = 0; i < 3; ++i) { std::unique_ptr<Texture> texture(new Texture(_optimisation == Optimisation::JPEG2000 ? 2 : 1, i)); _video_textures.push_back(std::move(texture)); } _subtitle_texture.reset(new Texture(1, 3)); - - while (true) { - boost::mutex::scoped_lock lm(_playing_mutex); - while (!_playing && !_one_shot) { - _thread_work_condition.wait(lm); - } - lm.unlock(); - - if (_playing) { - thread_playing(); - } else if (_one_shot) { - _one_shot = false; - set_image_and_draw(); - } - - boost::this_thread::interruption_point(); - dcpomatic_sleep_milliseconds(time_until_next_frame().get_value_or(0)); - } - - /* XXX: leaks _context, but that seems preferable to deleting it here - * without also deleting the wxGLCanvas. - */ -} -catch (boost::thread_interrupted&) -{ - -} -catch (...) -{ - store_current(); } @@ -958,20 +694,11 @@ VideoView::NextFrameResult GLVideoView::display_next_frame(bool non_blocking) { NextFrameResult const r = get_next_frame(non_blocking); - request_one_shot(); + _view.request_draw(); return r; } -void -GLVideoView::request_one_shot() -{ - boost::mutex::scoped_lock lm(_playing_mutex); - _one_shot = true; - _thread_work_condition.notify_all(); -} - - Texture::Texture(GLint unpack_alignment, int unit) : _unpack_alignment(unpack_alignment) , _unit(unit) @@ -1055,4 +782,12 @@ Texture::set(shared_ptr<const Image> image, int component) } } + +void +GLVideoView::update() +{ + _view.request_draw(); +} + + #endif diff --git a/src/wx/gl_video_view.h b/src/wx/gl_video_view.h index dc048e6b0..15805aea1 100644 --- a/src/wx/gl_video_view.h +++ b/src/wx/gl_video_view.h @@ -19,6 +19,7 @@ */ +#include "gl_view.h" #include <dcp/warnings.h> LIBDCP_DISABLE_WARNINGS #include <wx/glcanvas.h> @@ -30,12 +31,7 @@ LIBDCP_ENABLE_WARNINGS #undef Status -/* The OpenGL API in wxWidgets 3.0.x is sufficiently different to make it awkward to support, - * and I think it may even have things missing that we require (e.g. the attributes parameter - * to wxGLContext). Hence we only support the GLVideoView on wxWidgets 3.1.0 and higher - * (which only excludes the old macOS builds, since wxWidgets 3.1.x does not support macOS - * 10.9 or earlier). - */ +/* This will only build on an new-enough wxWidgets: see the comment in gl_view.h */ #if wxCHECK_VERSION(3,1,0) @@ -72,10 +68,9 @@ class GLVideoView : public VideoView { public: GLVideoView(FilmViewer* viewer, wxWindow* parent); - ~GLVideoView(); wxWindow* get() const override { - return _canvas; + return _view.canvas(); } void update() override; void start() override; @@ -84,7 +79,7 @@ public: NextFrameResult display_next_frame(bool) override; bool vsync_enabled() const { - return _vsync_enabled; + return _view.vsync_enabled(); } std::map<GLenum, std::string> information() const { @@ -93,20 +88,18 @@ public: private: void set_image(std::shared_ptr<const PlayerVideo> pv); - void set_image_and_draw(); - void draw(); - void thread(); - void thread_playing(); - void request_one_shot(); void check_for_butler_errors(); void ensure_context(); - void size_changed(wxSizeEvent const &); - void setup_shaders(); void set_outline_content_colour(GLuint program); void set_crop_guess_colour(GLuint program); - wxGLCanvas* _canvas; - wxGLContext* _context; + void setup_shaders(); + void create_textures(); + void play_pre_draw(); + void draw(); + int play_post_draw(); + + GLView _view; template <class T> class Last @@ -136,22 +129,12 @@ private: Last<dcp::Size> _last_out_size; Last<boost::optional<dcpomatic::Rect<float>>> _last_crop_guess; - boost::atomic<wxSize> _canvas_size; boost::atomic<bool> _rec2020; std::vector<std::unique_ptr<Texture>> _video_textures; std::unique_ptr<Texture> _subtitle_texture; bool _have_subtitle_to_render = false; - bool _vsync_enabled; - boost::thread _thread; - - boost::mutex _playing_mutex; - boost::condition _thread_work_condition; - boost::atomic<bool> _playing; - boost::atomic<bool> _one_shot; - GLuint _vao; GLint _fragment_type; - bool _setup_shaders_done = false; std::shared_ptr<wxTimer> _timer; diff --git a/src/wx/gl_view.cc b/src/wx/gl_view.cc new file mode 100644 index 000000000..245c69315 --- /dev/null +++ b/src/wx/gl_view.cc @@ -0,0 +1,371 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "gl_util.h" +#include "gl_view.h" +#include "lib/dcpomatic_log.h" +#include "lib/exceptions.h" +#include "lib/util.h" +LIBDCP_DISABLE_WARNINGS +#include <wx/glcanvas.h> +#include <wx/wx.h> +LIBDCP_ENABLE_WARNINGS + + +using std::string; + + +/* This will only build on an new-enough wxWidgets: see the comment in gl_view.h */ +#if wxCHECK_VERSION(3,1,0) + + +using std::function; +#if BOOST_VERSION >= 106100 +using namespace boost::placeholders; +#endif + + +GLView::GLView( + wxWindow* parent, + function<void ()> setup_shaders, + function<void ()> create_textures, + function<void ()> play_pre_draw, + function<void ()> draw, + function<int ()> play_post_draw, + string fragment_source + ) + : _vsync_enabled(false) + , _playing(false) + , _draw_requested(false) + , _setup_shaders(setup_shaders) + , _create_textures(create_textures) + , _play_pre_draw(play_pre_draw) + , _draw(draw) + , _play_post_draw(play_post_draw) + , _fragment_source(std::move(fragment_source)) +{ + wxGLAttributes attributes; + /* We don't need a depth buffer, and indeed there is apparently a bug with Windows/Intel HD 630 + * which puts green lines over the OpenGL display if you have a non-zero depth buffer size. + * https://community.intel.com/t5/Graphics/Request-for-details-on-Intel-HD-630-green-lines-in-OpenGL-apps/m-p/1202179 + */ + attributes.PlatformDefaults().MinRGBA(8, 8, 8, 8).DoubleBuffer().Depth(0).EndList(); + _canvas = new wxGLCanvas( + parent, attributes, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE + ); + + _canvas->Bind(wxEVT_SIZE, boost::bind(&GLView::size_changed, this, _1)); + _canvas->Bind(wxEVT_PAINT, boost::bind(&GLView::request_draw, this)); +} + + +GLView::~GLView() +{ + boost::this_thread::disable_interruption dis; + + try { + _thread.interrupt(); + _thread.join(); + } catch (...) {} +} + + +void +GLView::size_changed(wxSizeEvent const& ev) +{ + auto const scale = _canvas->GetDPIScaleFactor(); + int const width = std::round(ev.GetSize().GetWidth() * scale); + int const height = std::round(ev.GetSize().GetHeight() * scale); + _canvas_size = { width, height }; + LOG_GENERAL("GLView canvas size changed to %1x%2", width, height); + Sized(); +} + + +wxSize +GLView::size() const +{ + return _canvas_size.load(); +} + + +static constexpr char vertex_source[] = +"#version 330 core\n" +"\n" +"layout (location = 0) in vec3 in_pos;\n" +"layout (location = 1) in vec2 in_tex_coord;\n" +"\n" +"out vec2 TexCoord;\n" +"\n" +"void main()\n" +"{\n" +" gl_Position = vec4(in_pos, 1.0);\n" +" TexCoord = in_tex_coord;\n" +"}\n"; + + +void +GLView::setup_shaders() +{ + if (!_setup_shaders_done) { + DCPOMATIC_ASSERT(_canvas); + DCPOMATIC_ASSERT(_context); + auto r = _canvas->SetCurrent(*_context); + DCPOMATIC_ASSERT(r); + +#ifdef DCPOMATIC_WINDOWS + r = glewInit(); + if (r != GLEW_OK) { + boost::throw_exception(GLError(reinterpret_cast<char const*>(glewGetErrorString(r)))); + } +#endif + + glGenVertexArrays(1, &_vao); + check_gl_error("glGenVertexArrays"); + GLuint vbo; + glGenBuffers(1, &vbo); + check_gl_error("glGenBuffers"); + GLuint ebo; + glGenBuffers(1, &ebo); + check_gl_error("glGenBuffers"); + + glBindVertexArray(_vao); + check_gl_error("glBindVertexArray"); + + glBindBuffer(GL_ARRAY_BUFFER, vbo); + check_gl_error("glBindBuffer"); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + check_gl_error("glBindBuffer"); + + /* position attribute to vertex shader (location = 0) */ + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + /* texture coord attribute to vertex shader (location = 1) */ + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), reinterpret_cast<void*>(3 * sizeof(float))); + glEnableVertexAttribArray(1); + check_gl_error("glEnableVertexAttribArray"); + + auto vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_source); + auto fragment_shader = compile_shader(GL_FRAGMENT_SHADER, _fragment_source.c_str()); + + _program = glCreateProgram(); + check_gl_error("glCreateProgram"); + glAttachShader(_program, vertex_shader); + check_gl_error("glAttachShader"); + glAttachShader(_program, fragment_shader); + check_gl_error("glAttachShader"); + glLinkProgram(_program); + check_gl_error("glLinkProgram"); + GLint ok; + glGetProgramiv(_program, GL_LINK_STATUS, &ok); + if (!ok) { + GLint log_length; + glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &log_length); + string log; + if (log_length > 0) { + std::vector<char> log_char(log_length); + glGetProgramInfoLog(_program, log_length, nullptr, log_char.data()); + log = string(log_char.data()); + } + glDeleteProgram(_program); + boost::throw_exception(GLError(String::compose("Could not link shader (%1)", log).c_str(), -1)); + } + glDeleteShader(vertex_shader); + glDeleteShader(fragment_shader); + + glUseProgram(_program); + + _setup_shaders(); + _setup_shaders_done = true; + } +} + + +void +GLView::request_draw() +{ + if (!_canvas->IsShownOnScreen()) { + return; + } + + /* It appears important to do this from the UI thread; if we do it from the GL thread + * on Linux we get strange failures to create the context for any version of GL higher + * than 3.2. + */ + ensure_context(); + +#ifdef DCPOMATIC_OSX + /* macOS gives errors if we don't do this (and therefore [NSOpenGLContext setView:]) from the main thread */ + setup_shaders(); +#endif + + if (!_thread.joinable()) { + _thread = boost::thread(boost::bind(&GLView::thread, this)); + } + + { + boost::mutex::scoped_lock lm(_playing_mutex); + _draw_requested = true; + _thread_work_condition.notify_all(); + } + + rethrow(); +} + + +void +GLView::thread() +try +{ + start_of_thread("GLView"); + +#if defined(DCPOMATIC_OSX) + /* Without this we see errors like + * ../src/osx/cocoa/glcanvas.mm(194): assert ""context"" failed in SwapBuffers(): should have current context [in thread 700006970000] + */ + WXGLSetCurrentContext(_context->GetWXGLContext()); +#else + setup_shaders(); +#endif + +#if defined(DCPOMATIC_LINUX) && defined(DCPOMATIC_HAVE_GLX_SWAP_INTERVAL_EXT) + if (_canvas->IsExtensionSupported("GLX_EXT_swap_control")) { + /* Enable vsync */ + auto dpy = wxGetX11Display(); + glXSwapIntervalEXT(dpy, DefaultScreen(dpy), 1); + _vsync_enabled = true; + } +#endif + +#ifdef DCPOMATIC_WINDOWS + if (_canvas->IsExtensionSupported("WGL_EXT_swap_control")) { + /* Enable vsync */ + PFNWGLSWAPINTERVALEXTPROC swap = (PFNWGLSWAPINTERVALEXTPROC) wglGetProcAddress("wglSwapIntervalEXT"); + if (swap) { + swap(1); + _vsync_enabled = true; + } + } + +#endif + +#ifdef DCPOMATIC_OSX + /* Enable vsync */ + GLint swapInterval = 1; + CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &swapInterval); + _vsync_enabled = true; +#endif + + _create_textures(); + + while (true) { + boost::mutex::scoped_lock lm(_playing_mutex); + while (!_playing && !_draw_requested) { + _thread_work_condition.wait(lm); + } + lm.unlock(); + + if (_playing) { + _play_pre_draw(); + draw(); + _canvas->SwapBuffers(); + auto const sleep_ms = _play_post_draw(); + boost::this_thread::sleep(boost::posix_time::milliseconds(sleep_ms)); + } else if (_draw_requested) { + _draw_requested = false; + draw(); + _canvas->SwapBuffers(); + } + } +} +catch (boost::thread_interrupted&) +{ + +} +catch (...) +{ + store_current(); +} + + +void +GLView::draw() +{ + auto const s = size(); + int const width = s.GetWidth(); + int const height = s.GetHeight(); + + if (width < 64 || height < 0) { + return; + } + + glViewport(0, 0, width, height); + check_gl_error("glViewport"); + + glBindVertexArray(_vao); + check_gl_error("glBindVertexArray"); + + _draw(); + + glFlush(); + check_gl_error("glFlush"); +} + + + +void +GLView::ensure_context() +{ + if (!_context) { + wxGLContextAttrs attrs; + attrs.PlatformDefaults().CoreProfile().OGLVersion(4, 1).EndList(); + _context = new wxGLContext(_canvas, nullptr, &attrs); + if (!_context->IsOK()) { + boost::throw_exception(GLError("Making GL context", -1)); + } + } +} + + +void +GLView::start() +{ + { + boost::mutex::scoped_lock lm(_playing_mutex); + _playing = true; + _thread_work_condition.notify_all(); + } + + request_draw(); +} + + +void +GLView::stop() +{ + boost::mutex::scoped_lock lm(_playing_mutex); + _playing = false; +} + + +#endif + diff --git a/src/wx/gl_view.h b/src/wx/gl_view.h new file mode 100644 index 000000000..a857188ec --- /dev/null +++ b/src/wx/gl_view.h @@ -0,0 +1,138 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_GL_VIEW_H +#define DCPOMATIC_GL_VIEW_H + + +#include "lib/exception_store.h" +#include <wx/wx.h> +#include <boost/atomic.hpp> +#include <boost/signals2.hpp> +#include <boost/thread.hpp> +#include <boost/thread/condition.hpp> + + +/* The OpenGL API in wxWidgets 3.0.x is sufficiently different to make it awkward to support, + * and I think it may even have things missing that we require (e.g. the attributes parameter + * to wxGLContext). Hence we only support the GLVideoView on wxWidgets 3.1.0 and higher + * (which only excludes the old macOS builds, since wxWidgets 3.1.x does not support macOS + * 10.9 or earlier). + */ +#if wxCHECK_VERSION(3,1,0) + + +class wxGLCanvas; +class wxGLContext; + + +/** @class GLView + * + * @brief An OpenGL view for things that can be "played". + */ +class GLView : public ExceptionStore +{ +public: + /** @param parent Parent window for the wxGLCanvas. + * @param setup_shaders Function to set up any GL shaders that are required. + * May be called from any thread. + * @param create_textures Function to create any GL textures that are required. + * Will be called from the GLView's internal thread. + * @param play_pre_draw Function called when the view is playing, before draw is called + * to draw a frame. Should prepare things so that draw renders a new frame. + * @param draw function to render the current frame. + * @param play_pre_draw Function called after draw when playing. Should return the number + * of ms to sleep before trying to draw the next frame. + */ + GLView( + wxWindow* parent, + std::function<void ()> setup_shaders, + std::function<void ()> create_textures, + std::function<void ()> play_pre_draw, + std::function<void ()> draw, + std::function<int ()> play_post_draw, + std::string fragment_source + ); + + ~GLView(); + + wxGLCanvas* canvas() const { + return _canvas; + } + + /** Can be called from any thread */ + wxSize size() const; + + /** Can be called from any thread */ + bool vsync_enabled() const { + return _vsync_enabled; + } + + /** Arrange for `draw' to be called soon to update the view. + * Must be called from the UI thread. + */ + void request_draw(); + + void start(); + void stop(); + + unsigned int program() const { + return _program; + } + + /** Emitted from the UI thread when the view changes in size */ + boost::signals2::signal<void()> Sized; + +private: + void size_changed(wxSizeEvent const& ev); + void ensure_context(); + void thread(); + void setup_shaders(); + void draw(); + + wxGLCanvas* _canvas; + wxGLContext* _context = nullptr; + + boost::atomic<wxSize> _canvas_size; + bool _setup_shaders_done = false; + boost::atomic<bool> _vsync_enabled; + + unsigned int _vao; + unsigned int _program; + + boost::thread _thread; + boost::mutex _playing_mutex; + boost::condition _thread_work_condition; + boost::atomic<bool> _playing; + boost::atomic<bool> _draw_requested; + + std::function<void ()> _setup_shaders; + std::function<void ()> _create_textures; + std::function<void ()> _play_pre_draw; + std::function<void ()> _draw; + std::function<int ()> _play_post_draw; + std::string _fragment_source; +}; + + +#endif + +#endif diff --git a/src/wx/wscript b/src/wx/wscript index c9229416c..f1066dabe 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -96,6 +96,7 @@ sources = """ gain_calculator_dialog.cc gdc_certificate_panel.cc general_preferences_page.cc + gl_view.cc gl_video_view.cc gl_util.cc hints_dialog.cc |
