C++11 tidying.
[dcpomatic.git] / src / wx / gl_video_view.cc
1 /*
2     Copyright (C) 2018-2019 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "gl_video_view.h"
22 #include "film_viewer.h"
23 #include "wx_util.h"
24 #include "lib/image.h"
25 #include "lib/dcpomatic_assert.h"
26 #include "lib/exceptions.h"
27 #include "lib/cross.h"
28 #include "lib/player_video.h"
29 #include "lib/butler.h"
30 #include <boost/bind/bind.hpp>
31 #include <iostream>
32
33 #ifdef DCPOMATIC_OSX
34 #include <OpenGL/glu.h>
35 #include <OpenGL/glext.h>
36 #include <OpenGL/CGLTypes.h>
37 #include <OpenGL/OpenGL.h>
38 #endif
39
40 #ifdef DCPOMATIC_LINUX
41 #include <GL/glu.h>
42 #include <GL/glext.h>
43 #endif
44
45 #ifdef DCPOMATIC_WINDOWS
46 #include <GL/glu.h>
47 #include <GL/glext.h>
48 #include <GL/wglext.h>
49 #endif
50
51 using std::cout;
52 using std::shared_ptr;
53 using boost::optional;
54 #if BOOST_VERSION >= 106100
55 using namespace boost::placeholders;
56 #endif
57
58
59 static void
60 check_gl_error (char const * last)
61 {
62         GLenum const e = glGetError ();
63         if (e != GL_NO_ERROR) {
64                 throw GLError (last, e);
65         }
66 }
67
68
69 GLVideoView::GLVideoView (FilmViewer* viewer, wxWindow *parent)
70         : VideoView (viewer)
71         , _context (nullptr)
72         , _have_storage (false)
73         , _vsync_enabled (false)
74         , _playing (false)
75         , _one_shot (false)
76 {
77         _canvas = new wxGLCanvas (parent, wxID_ANY, 0, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE);
78         _canvas->Bind (wxEVT_PAINT, boost::bind(&GLVideoView::update, this));
79         _canvas->Bind (wxEVT_SIZE, boost::bind(&GLVideoView::size_changed, this, _1));
80
81         _canvas->Bind (wxEVT_TIMER, boost::bind(&GLVideoView::check_for_butler_errors, this));
82         _timer.reset (new wxTimer(_canvas));
83         _timer->Start (2000);
84 }
85
86
87 void
88 GLVideoView::size_changed (wxSizeEvent const& ev)
89 {
90         _canvas_size = ev.GetSize ();
91         Sized ();
92 }
93
94
95
96 GLVideoView::~GLVideoView ()
97 {
98         boost::this_thread::disable_interruption dis;
99
100         try {
101                 _thread.interrupt ();
102                 _thread.join ();
103         } catch (...) {}
104
105         glDeleteTextures (1, &_id);
106 }
107
108 void
109 GLVideoView::check_for_butler_errors ()
110 {
111         if (!_viewer->butler()) {
112                 return;
113         }
114
115         try {
116                 _viewer->butler()->rethrow ();
117         } catch (DecodeError& e) {
118                 error_dialog (get(), e.what());
119         } catch (dcp::ReadError& e) {
120                 error_dialog (get(), wxString::Format(_("Could not read DCP: %s"), std_to_wx(e.what())));
121         }
122 }
123
124
125 /** Called from the UI thread */
126 void
127 GLVideoView::update ()
128 {
129         if (!_canvas->IsShownOnScreen()) {
130                 return;
131         }
132
133 #ifdef DCPOMATIC_OSX
134         /* macOS gives errors if we don't do this (and therefore [NSOpenGLContext setView:]) from the main thread */
135         ensure_context ();
136 #endif
137
138         if (!_thread.joinable()) {
139                 _thread = boost::thread (boost::bind(&GLVideoView::thread, this));
140         }
141
142         request_one_shot ();
143 }
144
145
146 void
147 GLVideoView::ensure_context ()
148 {
149         if (!_context) {
150                 _context = new wxGLContext (_canvas);
151                 _canvas->SetCurrent (*_context);
152         }
153 }
154
155 void
156 GLVideoView::draw (Position<int> inter_position, dcp::Size inter_size)
157 {
158         glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
159         check_gl_error ("glClear");
160
161         glClearColor (0.0f, 0.0f, 0.0f, 1.0f);
162         check_gl_error ("glClearColor");
163         glEnable (GL_TEXTURE_2D);
164         check_gl_error ("glEnable GL_TEXTURE_2D");
165         glEnable (GL_BLEND);
166         check_gl_error ("glEnable GL_BLEND");
167         glDisable (GL_DEPTH_TEST);
168         check_gl_error ("glDisable GL_DEPTH_TEST");
169         glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
170
171         auto const size = _canvas_size.load();
172         int const width = size.GetWidth();
173         int const height = size.GetHeight();
174
175         if (width < 64 || height < 0) {
176                 return;
177         }
178
179         glViewport (0, 0, width, height);
180         check_gl_error ("glViewport");
181         glMatrixMode (GL_PROJECTION);
182         glLoadIdentity ();
183
184 DCPOMATIC_DISABLE_WARNINGS
185         gluOrtho2D (0, width, height, 0);
186 DCPOMATIC_ENABLE_WARNINGS
187         check_gl_error ("gluOrtho2d");
188         glMatrixMode (GL_MODELVIEW);
189         glLoadIdentity ();
190
191         glTranslatef (0, 0, 0);
192
193         dcp::Size const out_size = _viewer->out_size ();
194
195         if (_size) {
196                 /* Render our image (texture) */
197                 glBegin (GL_QUADS);
198                 glTexCoord2f (0, 1);
199                 glVertex2f (0, _size->height);
200                 glTexCoord2f (1, 1);
201                 glVertex2f (_size->width, _size->height);
202                 glTexCoord2f (1, 0);
203                 glVertex2f (_size->width, 0);
204                 glTexCoord2f (0, 0);
205                 glVertex2f (0, 0);
206                 glEnd ();
207         } else {
208                 /* No image, so just fill with black */
209                 glBegin (GL_QUADS);
210                 glColor3ub (0, 0, 0);
211                 glVertex2f (0, 0);
212                 glVertex2f (out_size.width, 0);
213                 glVertex2f (out_size.width, out_size.height);
214                 glVertex2f (0, out_size.height);
215                 glVertex2f (0, 0);
216                 glEnd ();
217         }
218
219         if (!_viewer->pad_black() && out_size.width < width) {
220                 glBegin (GL_QUADS);
221                 /* XXX: these colours are right for GNOME; may need adjusting for other OS */
222                 glColor3ub (240, 240, 240);
223                 glVertex2f (out_size.width, 0);
224                 glVertex2f (width, 0);
225                 glVertex2f (width, height);
226                 glVertex2f (out_size.width, height);
227                 glEnd ();
228                 glColor3ub (255, 255, 255);
229         }
230
231         if (!_viewer->pad_black() && out_size.height < height) {
232                 glColor3ub (240, 240, 240);
233                 int const gap = (height - out_size.height) / 2;
234                 glBegin (GL_QUADS);
235                 glVertex2f (0, 0);
236                 glVertex2f (width, 0);
237                 glVertex2f (width, gap);
238                 glVertex2f (0, gap);
239                 glEnd ();
240                 glBegin (GL_QUADS);
241                 glVertex2f (0, gap + out_size.height + 1);
242                 glVertex2f (width, gap + out_size.height + 1);
243                 glVertex2f (width, 2 * gap + out_size.height + 2);
244                 glVertex2f (0, 2 * gap + out_size.height + 2);
245                 glEnd ();
246                 glColor3ub (255, 255, 255);
247         }
248
249         if (_viewer->outline_content()) {
250                 glColor3ub (255, 0, 0);
251                 glBegin (GL_LINE_LOOP);
252                 glVertex2f (inter_position.x, inter_position.y + (height - out_size.height) / 2);
253                 glVertex2f (inter_position.x + inter_size.width, inter_position.y + (height - out_size.height) / 2);
254                 glVertex2f (inter_position.x + inter_size.width, inter_position.y + (height - out_size.height) / 2 + inter_size.height);
255                 glVertex2f (inter_position.x, inter_position.y + (height - out_size.height) / 2 + inter_size.height);
256                 glEnd ();
257                 glColor3ub (255, 255, 255);
258         }
259
260         glFlush();
261         check_gl_error ("glFlush");
262
263         _canvas->SwapBuffers();
264 }
265
266 void
267 GLVideoView::set_image (shared_ptr<const Image> image)
268 {
269         if (!image) {
270                 _size = optional<dcp::Size>();
271                 return;
272         }
273
274         DCPOMATIC_ASSERT (image->pixel_format() == AV_PIX_FMT_RGB24);
275         DCPOMATIC_ASSERT (!image->aligned());
276
277         if (image->size() != _size) {
278                 _have_storage = false;
279         }
280
281         _size = image->size ();
282         glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
283         check_gl_error ("glPixelStorei");
284         if (_have_storage) {
285                 glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, _size->width, _size->height, GL_RGB, GL_UNSIGNED_BYTE, image->data()[0]);
286                 check_gl_error ("glTexSubImage2D");
287         } else {
288                 glTexImage2D (GL_TEXTURE_2D, 0, GL_RGB8, _size->width, _size->height, 0, GL_RGB, GL_UNSIGNED_BYTE, image->data()[0]);
289                 _have_storage = true;
290                 check_gl_error ("glTexImage2D");
291         }
292
293         glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
294         glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
295         check_gl_error ("glTexParameteri");
296
297         glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
298         glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
299         check_gl_error ("glTexParameterf");
300 }
301
302 void
303 GLVideoView::start ()
304 {
305         VideoView::start ();
306
307         boost::mutex::scoped_lock lm (_playing_mutex);
308         _playing = true;
309         _thread_work_condition.notify_all ();
310 }
311
312 void
313 GLVideoView::stop ()
314 {
315         boost::mutex::scoped_lock lm (_playing_mutex);
316         _playing = false;
317 }
318
319
320 void
321 GLVideoView::thread_playing ()
322 {
323         if (length() != dcpomatic::DCPTime()) {
324                 dcpomatic::DCPTime const next = position() + one_video_frame();
325
326                 if (next >= length()) {
327                         _viewer->finished ();
328                         return;
329                 }
330
331                 get_next_frame (false);
332                 set_image_and_draw ();
333         }
334
335         while (true) {
336                 optional<int> n = time_until_next_frame();
337                 if (!n || *n > 5) {
338                         break;
339                 }
340                 get_next_frame (true);
341                 add_dropped ();
342         }
343 }
344
345
346 void
347 GLVideoView::set_image_and_draw ()
348 {
349         shared_ptr<PlayerVideo> pv = player_video().first;
350         if (pv) {
351                 set_image (pv->image(bind(&PlayerVideo::force, _1, AV_PIX_FMT_RGB24), VideoRange::FULL, false, true));
352                 draw (pv->inter_position(), pv->inter_size());
353                 _viewer->image_changed (pv);
354         }
355 }
356
357
358 void
359 GLVideoView::thread ()
360 try
361 {
362         start_of_thread ("GLVideoView");
363
364 #if defined(DCPOMATIC_OSX)
365         /* Without this we see errors like
366          * ../src/osx/cocoa/glcanvas.mm(194): assert ""context"" failed in SwapBuffers(): should have current context [in thread 700006970000]
367          */
368         WXGLSetCurrentContext (_context->GetWXGLContext());
369 #else
370         /* We must call this here on Linux otherwise we get no image (for reasons
371          * that aren't clear).  However, doing ensure_context() from this thread
372          * on macOS gives
373          * "[NSOpenGLContext setView:] must be called from the main thread".
374          */
375         ensure_context ();
376 #endif
377
378 #if defined(DCPOMATIC_LINUX) && defined(DCPOMATIC_HAVE_GLX_SWAP_INTERVAL_EXT)
379         if (_canvas->IsExtensionSupported("GLX_EXT_swap_control")) {
380                 /* Enable vsync */
381                 Display* dpy = wxGetX11Display();
382                 glXSwapIntervalEXT (dpy, DefaultScreen(dpy), 1);
383                 _vsync_enabled = true;
384         }
385 #endif
386
387 #ifdef DCPOMATIC_WINDOWS
388         if (_canvas->IsExtensionSupported("WGL_EXT_swap_control")) {
389                 /* Enable vsync */
390                 PFNWGLSWAPINTERVALEXTPROC swap = (PFNWGLSWAPINTERVALEXTPROC) wglGetProcAddress("wglSwapIntervalEXT");
391                 if (swap) {
392                         swap (1);
393                         _vsync_enabled = true;
394                 }
395         }
396
397 #endif
398
399 #ifdef DCPOMATIC_OSX
400         /* Enable vsync */
401         GLint swapInterval = 1;
402         CGLSetParameter (CGLGetCurrentContext(), kCGLCPSwapInterval, &swapInterval);
403         _vsync_enabled = true;
404 #endif
405
406         glGenTextures (1, &_id);
407         check_gl_error ("glGenTextures");
408         glBindTexture (GL_TEXTURE_2D, _id);
409         check_gl_error ("glBindTexture");
410         glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
411         check_gl_error ("glPixelStorei");
412
413         while (true) {
414                 boost::mutex::scoped_lock lm (_playing_mutex);
415                 while (!_playing && !_one_shot) {
416                         _thread_work_condition.wait (lm);
417                 }
418                 lm.unlock ();
419
420                 if (_playing) {
421                         thread_playing ();
422                 } else if (_one_shot) {
423                         _one_shot = false;
424                         set_image_and_draw ();
425                 }
426
427                 boost::this_thread::interruption_point ();
428                 dcpomatic_sleep_milliseconds (time_until_next_frame().get_value_or(0));
429         }
430
431         /* XXX: leaks _context, but that seems preferable to deleting it here
432          * without also deleting the wxGLCanvas.
433          */
434 }
435 catch (boost::thread_interrupted& e)
436 {
437         store_current ();
438 }
439
440
441 VideoView::NextFrameResult
442 GLVideoView::display_next_frame (bool non_blocking)
443 {
444         NextFrameResult const r = get_next_frame (non_blocking);
445         request_one_shot ();
446         return r;
447 }
448
449
450 void
451 GLVideoView::request_one_shot ()
452 {
453         boost::mutex::scoped_lock lm (_playing_mutex);
454         _one_shot = true;
455         _thread_work_condition.notify_all ();
456 }
457