summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-04-20 23:22:54 +0200
committerCarl Hetherington <cth@carlh.net>2025-04-28 02:31:15 +0200
commitc61e1b24a888e8d79b43069e4ed9864ef53bc718 (patch)
treea6f939f8a2c607ae42fb57e66fdb580b147a1f3b
parent7f25fc20f432649759743848667a5e17b3c43c88 (diff)
Extract parts of GLVideoView to GLView.
-rw-r--r--src/wx/gl_util.cc28
-rw-r--r--src/wx/gl_util.h1
-rw-r--r--src/wx/gl_video_view.cc429
-rw-r--r--src/wx/gl_video_view.h39
-rw-r--r--src/wx/gl_view.cc371
-rw-r--r--src/wx/gl_view.h138
-rw-r--r--src/wx/wscript1
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