1 /*
2  * Copyright (C) 2014-2018 Christopho, Solarus - http://www.solarus-games.org
3  *
4  * Solarus Quest Editor is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * Solarus Quest Editor is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 #include <solarus/graphics/VertexArray.h>
18 
19 // Workaround for conflicting declaration of GLsizeiptrARB on Windows.
20 #ifndef GL_ARB_vertex_buffer_object
21 #define GL_ARB_vertex_buffer_object 1
22 #endif
23 
24 #include "widgets/shader_previewer.h"
25 #include "quest.h"
26 #include "shader_model.h"
27 #include "view_settings.h"
28 
29 #define GLM_FORCE_INLINE
30 #include <solarus/graphics/DefaultShaders.h>
31 #include <solarus/graphics/Shader.h>
32 
33 #include <QMatrix3x3>
34 #include <QMouseEvent>
35 #include <QOpenGLContext>
36 #include <QOpenGLFunctions>
37 #include <QWheelEvent>
38 
39 
40 namespace SolarusEditor {
41 
42 constexpr const char* SWIPE_VERTEX_SHADER =
43     R"(
44       #if __VERSION__ >= 130
45       #define COMPAT_VARYING out
46       #define COMPAT_ATTRIBUTE in
47       #else
48       #define COMPAT_VARYING varying
49       #define COMPAT_ATTRIBUTE attribute
50       #endif
51 
52       #ifdef GL_ES
53       precision mediump float;
54       #define COMPAT_PRECISION mediump
55       #else
56       #define COMPAT_PRECISION
57       #endif
58 
59       uniform mat4 sol_mvp_matrix;
60       uniform mat3 sol_uv_matrix;
61       COMPAT_ATTRIBUTE vec2 sol_vertex;
62       COMPAT_ATTRIBUTE vec2 sol_tex_coord;
63       COMPAT_ATTRIBUTE vec4 sol_color;
64 
65       COMPAT_VARYING vec2 sol_vtex_coord;
66       COMPAT_VARYING vec4 sol_vcolor;
67       COMPAT_VARYING float vfactor;
68 
69       void main() {
70          gl_Position = sol_mvp_matrix * vec4(sol_vertex,0,1);
71          vfactor = (gl_Position.x + 1.0)*0.5;
72          sol_vcolor = sol_color;
73          sol_vtex_coord = (sol_uv_matrix * vec3(sol_tex_coord,1)).xy;
74       }
75     )";
76 
77 constexpr const char* SWIPE_FRAGMENT_SHADER =
78     R"(
79       #if __VERSION__ >= 130
80       #define COMPAT_VARYING in
81       #define COMPAT_TEXTURE texture
82       out vec4 FragColor;
83       #else
84       #define COMPAT_VARYING varying
85       #define FragColor gl_FragColor
86       #define COMPAT_TEXTURE texture2D
87       #endif
88 
89       #ifdef GL_ES
90       precision mediump float;
91       #define COMPAT_PRECISION mediump
92       #else
93       #define COMPAT_PRECISION
94       #endif
95 
96       uniform float factor;
97       uniform sampler2D input_tex;
98       uniform sampler2D output_tex;
99       uniform vec2 sol_output_size;
100       COMPAT_VARYING vec2 sol_vtex_coord;
101       COMPAT_VARYING vec4 sol_vcolor;
102       COMPAT_VARYING float vfactor;
103 
104       void main() {
105         vec4 tex_i = COMPAT_TEXTURE(input_tex, sol_vtex_coord);
106         vec4 tex_o = COMPAT_TEXTURE(output_tex, vec2(sol_vtex_coord.x,1.0-sol_vtex_coord.y));
107         if(vfactor<factor) {
108           FragColor = tex_i;
109         } else {
110           FragColor = tex_o;
111         }
112       }
113     )";
114 
115 /**
116  * @brief Creates a shader previewer.
117  * @param parent The parent object or nullptr.
118  */
ShaderPreviewer(QWidget * parent)119 ShaderPreviewer::ShaderPreviewer(QWidget *parent) :
120   QOpenGLWidget(parent),
121   program(this),
122   model(nullptr),
123   preview_mode(ShaderPreviewMode::SIDE_BY_SIDE)
124 #ifdef SOLARUSEDITOR_DEBUG_GL
125   ,gl_logger(this)
126 #endif
127 {
128 
129   QSurfaceFormat format;
130   format.setProfile(QSurfaceFormat::CoreProfile);
131 #ifdef SOLARUSEDITOR_DEBUG_GL
132   format.setMajorVersion(3);
133   format.setMinorVersion(2);
134   format.setOption(QSurfaceFormat::DebugContext);
135 #endif
136   setFormat(format);
137 
138   // Setup cursors
139   grab_cursor.setShape(Qt::ClosedHandCursor);
140   hover_cursor.setShape(Qt::OpenHandCursor);
141 
142   setCursor(hover_cursor);
143   setMouseTracking(true);
144 
145   connect(&fps_timer,&QTimer::timeout,[this]{
146     update();
147   });
148   fps_timer.setInterval(10);
149   fps_timer.start();
150 }
151 
152 /**
153  * @brief handle mouse movement, moving scene if grabbed
154  * @param event
155  */
mouseMoveEvent(QMouseEvent * event)156 void ShaderPreviewer::mouseMoveEvent(QMouseEvent* event) {
157   Q_UNUSED(event);
158   if (grabbing) {
159     QPointF d = event->localPos() - last_mouse_pos;
160     d.setY(-d.y());
161     translation += QVector2D(d) / (zoom*pixelFactor());
162     last_mouse_pos = event->localPos();
163     event->accept();
164   }
165   update();
166 }
167 
168 /**
169  * @brief handle mouse press
170  * @param event
171  */
mousePressEvent(QMouseEvent * event)172 void ShaderPreviewer::mousePressEvent(QMouseEvent* event) {
173   Q_UNUSED(event);
174   if (event->button() == Qt::LeftButton ||
175       event->button() == Qt::MiddleButton) {
176     grabbing = true;
177     last_mouse_pos = event->localPos();
178     setCursor(grab_cursor);
179     event->accept();
180   }
181 }
182 
183 /**
184  * @brief handle mouse release
185  * @param event
186  */
mouseReleaseEvent(QMouseEvent * event)187 void ShaderPreviewer::mouseReleaseEvent(QMouseEvent* event) {
188   Q_UNUSED(event);
189   if (event->button() == Qt::LeftButton ||
190       event->button() == Qt::MiddleButton) {
191     grabbing = false;
192     setCursor(hover_cursor);
193     event->accept();
194   }
195 }
196 
197 
198 /**
199  * @brief get displacement to the frame center
200  * @param mouse_position
201  * @return
202  */
to_frame_center(const QPoint & mouse_position) const203 QVector2D ShaderPreviewer::to_frame_center(const QPoint& mouse_position) const {
204   auto center = [&]()->QVector2D{
205     switch(preview_mode) {
206     case ShaderPreviewMode::SIDE_BY_SIDE:
207     {
208       float y = frameSize().height() / 2.f;
209       QSize qsize = model->get_quest().get_properties().get_normal_quest_size();
210       qsize.setWidth(qsize.width()*2);
211       QSize letterb = get_letter_box(qsize,frameSize());
212       int rx = (frameSize().width() - letterb.width()) / 2;
213       if(mouse_position.x() < frameSize().width() / 2) {
214         return {rx+letterb.width()/4.f,y};
215       } else {
216         return {rx+(letterb.width()*3.f)/4.f,y};
217       }
218     }
219     default:
220     {
221       QSize fsize = frameSize();
222       return {fsize.width()/2.f,fsize.height()/2.f};
223     }
224     };
225   };
226   return (center() - QVector2D(mouse_position))*QVector2D(1,-1);
227 }
228 
229 /**
230  * @brief Receives a mouse wheel event.
231  * @param event The event to handle.
232  */
wheelEvent(QWheelEvent * event)233 void ShaderPreviewer::wheelEvent(QWheelEvent* event) {
234 
235   float old_zoom = zoom;
236   if (event->delta() > 0) {
237     zoom_in();
238     if(zoom != old_zoom) {
239       translation += (to_frame_center(event->pos())/zoom) / pixelFactor();
240     }
241   }
242   else {
243     zoom_out();
244     if(zoom != old_zoom) {
245       translation -= 0.5*(to_frame_center(event->pos())/zoom) / pixelFactor();
246     }
247   }
248 
249 }
250 
251 /**
252  * @brief Scales the view by a factor of 2.
253  *
254  * Zooming will be anchored at the mouse position.
255  * The maximum zoom value is 4.0: this function does nothing if you try to
256  * zoom more.
257  */
zoom_in()258 void ShaderPreviewer::zoom_in() {
259 
260   if (view_settings == nullptr) {
261     return;
262   }
263 
264   view_settings->set_zoom(view_settings->get_zoom() * 2.0);
265 }
266 
267 /**
268  * @brief Scales the view by a factor of 0.5.
269  *
270  * Zooming will be anchored at the mouse position.
271  * The maximum zoom value is 0.25: this function does nothing if you try to
272  * zoom less.
273  */
zoom_out()274 void ShaderPreviewer::zoom_out() {
275 
276   if (view_settings == nullptr) {
277     return;
278   }
279 
280   view_settings->set_zoom(view_settings->get_zoom() / 2.0);
281 }
282 
283 /**
284  * @brief Sets the zoom level of the view from the settings.
285  *
286  * Zooming will be anchored at the mouse position.
287  * The zoom value will be clamped between 1 and 4.0.
288  */
289 
update_zoom()290 void ShaderPreviewer::update_zoom() {
291 
292   if (view_settings == nullptr) {
293     return;
294   }
295 
296   float zoom = static_cast<float>(view_settings->get_zoom());
297   zoom = qMin(4.0f, qMax(1.f, zoom));
298   view_settings->set_zoom(zoom);
299 
300   if (zoom == this->zoom) {
301     return;
302   }
303 
304 
305   this->zoom = zoom;
306   update();
307 }
308 
309 /**
310  * @brief compute letter boxed size
311  * @param qsize size of the box to fit
312  * @param basesize size of the enclosing box
313  * @return qsize scaled to fit in basesize
314  */
get_letter_box(const QSize & qsize,const QSize & basesize) const315 QSize ShaderPreviewer::get_letter_box(const QSize& qsize, const QSize& basesize) const {
316   float qratio = qsize.width() / (float) qsize.height();
317   float wratio = basesize.width() / (float) basesize.height();
318   if (qratio > wratio) {
319     return QSize(basesize.width(), basesize.width() / qratio);
320   } else {
321     return QSize(basesize.height() * qratio, basesize.height());
322   }
323 }
324 
325 /**
326  * @brief ShaderPreviewer::sanitizeShaderCode
327  * @param code
328  * @return
329  */
sanitizeShaderCode(const QString & code) const330 QString ShaderPreviewer::sanitizeShaderCode(const QString& code) const {
331   if(code.contains("#version")) {
332     return code;
333   } else {
334     return glsl_version + code;
335   }
336 }
337 
338 /**
339  * @brief return the factor between inputframebuffer pixels and screen pixels
340  * @return
341  */
pixelFactor() const342 float ShaderPreviewer::pixelFactor() const  {
343   QSize qsize = model->get_quest().get_properties().get_normal_quest_size();
344   switch(preview_mode) {
345     case ShaderPreviewMode::SIDE_BY_SIDE: {
346       qsize.setWidth(qsize.width()*2);
347       QSize lb = get_letter_box(qsize,frameSize());
348       return 0.5f*lb.width() / (float) input_fb->width();
349     }
350     default: {
351       QSize lb = get_letter_box(qsize,frameSize());
352       return lb.width() / (float) input_fb->width();
353     }
354   }
355 }
356 
357 /**
358  * @brief Sets the shader model.
359  * @param model The shader model, or nullptr to remove any model.
360  * This class does not take ownership on the model.
361  * The model can be deleted safely.
362  */
set_model(ShaderModel * model)363 void ShaderPreviewer::set_model(ShaderModel* model) {
364 
365   if (this->model != nullptr) {
366     this->model->disconnect(this);
367     this->model = nullptr;
368   }
369 
370   this->model = model;
371 
372   if (model != nullptr) {
373     connect(model, &ShaderModel::fragment_file_changed,
374             this, &ShaderPreviewer::on_source_changed);
375     connect(model, &ShaderModel::vertex_file_changed,
376             this, &ShaderPreviewer::on_source_changed);
377     connect(model, &ShaderModel::scaling_factor_changed,
378             this, &ShaderPreviewer::on_scaling_factor_changed);
379     on_source_changed();
380   }
381 }
382 
383 /**
384  * @brief Sets the view settings for this view.
385  *
386  * When they change, the view is updated accordingly.
387  *
388  * @param view_settings The settings to watch.
389  */
set_view_settings(ViewSettings & view_settings)390 void ShaderPreviewer::set_view_settings(ViewSettings& view_settings) {
391 
392   this->view_settings = &view_settings;
393 
394   connect(&view_settings, &ViewSettings::zoom_changed,
395           this, &ShaderPreviewer::update_zoom);
396   update_zoom();
397 }
398 
399 /**
400  * @brief Returns the current preview mode.
401  * @return The preview mode.
402  */
get_preview_mode() const403 ShaderPreviewMode ShaderPreviewer::get_preview_mode() const {
404   return preview_mode;
405 }
406 
407 /**
408  * @brief Changes the preview mode.
409  * @param preview_mode The new mode to set.
410  */
set_preview_mode(ShaderPreviewMode preview_mode)411 void ShaderPreviewer::set_preview_mode(ShaderPreviewMode preview_mode) {
412   this->preview_mode = preview_mode;
413   update();
414 }
415 
416 /**
417  * @brief re-create framebuffer with appropriate size
418  * @param output_size size of the output buffer
419  */
setup_framebuffers(const QSize & output_size)420 void ShaderPreviewer::setup_framebuffers(const QSize& output_size) {
421   makeCurrent();
422   delete input_fb;
423   delete output_fb;
424 
425   QSize size = model->get_quest().get_properties().get_normal_quest_size();
426   input_fb = new QOpenGLFramebufferObject(size);
427   if (!input_fb->isValid()) {
428     qWarning() << "Failed to create input frame buffer with size" << size;
429   }
430   output_fb = new QOpenGLFramebufferObject(output_size);
431   if (!output_fb->isValid()) {
432     qWarning() << "Failed to create output frame buffer with size" << output_size;
433   }
434 }
435 
436 /**
437  * @brief Render input to input buffer and rerender it with shader to output buffer
438  */
render_fbs()439 void ShaderPreviewer::render_fbs() {
440   if (input_texture == nullptr) {
441     return;
442   }
443   QOpenGLFunctions* gl = context()->functions();
444   { //Render input to simulate quest surface
445     input_fb->bind();
446     QMatrix4x4 mvp;
447     //TODO mouse control
448     mvp.ortho(0, input_fb->width(), input_fb->height(), 0, -1, 1);
449     //mvp.scale(zoom);
450     mvp.translate(floor(translation.x()), floor(translation.y()), 0);
451     mvp.scale(input_texture->width(), input_texture->height(), 1);
452     QMatrix3x3 uvm;
453     gl->glClearColor(0, 0, 0, 1.f);
454     gl->glViewport(0, 0, input_fb->width(), input_fb->height());
455     gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
456     simple_program.bind();
457     simple_program.setUniformValue(Solarus::Shader::UV_MATRIX_NAME, uvm);
458     simple_program.setUniformValue(Solarus::Shader::MVP_MATRIX_NAME, mvp);
459     const char* texture_name = Solarus::Shader::TEXTURE_NAME;
460     render_quad(simple_program, {{texture_name, input_texture->textureId()}});
461     input_fb->release();
462   }
463   { // Render output to simulate screen
464     output_fb->bind();
465     QMatrix4x4 mvp;
466     mvp.translate(-1, -1, 0);
467     mvp.scale(2);
468     QMatrix3x3 uvm;
469     gl->glClearColor(0, 0, 0, 0);
470     gl->glViewport(0, 0, output_fb->width(), output_fb->height());
471     gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
472     program.bind();
473     program.setUniformValue(Solarus::Shader::UV_MATRIX_NAME,uvm);
474     program.setUniformValue(Solarus::Shader::MVP_MATRIX_NAME,mvp);
475     QSize isize = model->get_quest().get_properties().get_normal_quest_size();
476     program.setUniformValue(
477           Solarus::Shader::INPUT_SIZE_NAME,
478           QVector2D(isize.width(), isize.height()));
479     program.setUniformValue(
480           Solarus::Shader::OUTPUT_SIZE_NAME,
481           QVector2D(output_fb->width(), output_fb->height()));
482     int time_loc = program.uniformLocation(Solarus::Shader::TIME_NAME);
483     int ms = QTime::currentTime().msecsSinceStartOfDay();
484     gl->glUniform1i(time_loc, ms);
485     const char* texture_name = Solarus::Shader::TEXTURE_NAME;
486     render_quad(program, {{texture_name, input_fb->texture()}});
487     output_fb->release();
488   }
489 
490 }
491 
492 /**
493  * @brief render a quad with the given shader and textures
494  * @param shader a valid Solarus-Style shader
495  * @param textures a vector of name-handle pairs
496  */
render_quad(QOpenGLShaderProgram & shader,const Textures & textures)497 void ShaderPreviewer::render_quad(QOpenGLShaderProgram& shader,  const Textures& textures) {
498   shader.bind();
499   vao->bind();
500   vertex_buffer->bind();
501   QOpenGLFunctions* gl = context()->functions();
502   int pos_loc = shader.attributeLocation(Solarus::Shader::POSITION_NAME);
503   if (pos_loc > -1) {
504     gl->glEnableVertexAttribArray(pos_loc);
505     gl->glVertexAttribPointer(pos_loc, 2, GL_FLOAT, GL_FALSE, sizeof(Solarus::Vertex), (void*) offsetof(Solarus::Vertex, position));
506   }
507   int uv_loc = shader.attributeLocation(Solarus::Shader::TEXCOORD_NAME);
508   if (uv_loc > -1) {
509     gl->glEnableVertexAttribArray(uv_loc);
510     gl->glVertexAttribPointer(uv_loc, 2, GL_FLOAT, GL_FALSE, sizeof(Solarus::Vertex), (void*) offsetof(Solarus::Vertex, texcoords));
511   }
512   int color_loc = shader.attributeLocation(Solarus::Shader::COLOR_NAME);
513   if (color_loc > -1) {
514     gl->glEnableVertexAttribArray(color_loc);
515     gl->glVertexAttribPointer(color_loc, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Solarus::Vertex), (void*) offsetof(Solarus::Vertex, color));
516   }
517 
518   for (size_t i = 0; i < textures.size(); ++i) {
519     int tex_loc = shader.uniformLocation(textures[i].first);
520     gl->glUniform1i(tex_loc,i);
521     gl->glActiveTexture(GL_TEXTURE0 + i);
522     gl->glBindTexture(GL_TEXTURE_2D, textures[i].second);
523     gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
524     gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
525   }
526   gl->glDrawArrays(GL_TRIANGLES, 0, 6);
527   vertex_buffer->release();
528   vao->release();
529   shader.release();
530 }
531 
532 /**
533  * @brief render the framebuffers in a swipe fashion
534  * @param factor position of the vertical split [0,1]
535  */
render_swipe(float factor)536 void ShaderPreviewer::render_swipe(float factor) {
537   swipe_program.bind();
538   QMatrix4x4 mvp;
539   mvp.scale(zoom);
540   mvp.translate(-1, -1, 0);
541   mvp.scale(2);
542   QMatrix3x3 uvm;
543   swipe_program.setUniformValue(Solarus::Shader::UV_MATRIX_NAME, uvm);
544   swipe_program.setUniformValue(Solarus::Shader::MVP_MATRIX_NAME, mvp);
545   swipe_program.setUniformValue("factor", factor);
546   QSize vp = get_letter_box(model->get_quest().get_properties().get_normal_quest_size(), frameSize());
547   swipe_program.setUniformValue(
548         Solarus::Shader::OUTPUT_SIZE_NAME,
549         QVector2D(vp.width(),vp.height()));
550   render_quad(swipe_program, {{"input_tex", input_fb->texture()}, {"output_tex", output_fb->texture()}});
551 }
552 
553 /**
554  * @brief render the framebuffers in a Side by Side fashion
555  */
render_sbs()556 void ShaderPreviewer::render_sbs() {
557   QSize qsize = model->get_quest().get_properties().get_normal_quest_size();
558   qsize.setWidth(qsize.width()*2);
559   QSize letterb = get_letter_box(qsize,frameSize());
560   auto render_side = [&](GLuint tex, bool invert) {
561     QMatrix4x4 mvp;
562     mvp.scale(zoom);
563     mvp.translate(-1, -1, 0);
564     mvp.scale(2);
565     QMatrix3x3 uvm;
566     if (invert) {
567       uvm.data()[4] = -1;
568       uvm.data()[7] = 1;
569     }
570     simple_program.bind();
571     simple_program.setUniformValue(Solarus::Shader::UV_MATRIX_NAME, uvm);
572     simple_program.setUniformValue(Solarus::Shader::MVP_MATRIX_NAME, mvp);
573     const char* name = Solarus::Shader::TEXTURE_NAME;
574     render_quad(simple_program, {{name, tex}});
575   };
576   QOpenGLFunctions* gl = context()->functions();
577   int x = (frameSize().width() - letterb.width()) / 2;
578   int y = (frameSize().height() - letterb.height()) / 2;
579   gl->glViewport(x, y, letterb.width() / 2, letterb.height());
580   render_side(input_fb->texture(), false);
581   gl->glViewport(x + letterb.width() / 2, y, letterb.width()/2, letterb.height());
582   render_side(output_fb->texture(), true);
583 }
584 
585 /**
586  * @brief QOpengl pain event all drawing happens here
587  */
paintGL()588 void ShaderPreviewer::paintGL() {
589   QOpenGLFunctions* gl = context()->functions();
590   gl->glClearColor(0.3f, 0.3f, 0.3f, 1);
591   gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
592   if (model == nullptr) {
593     return;
594   }
595   if (should_recompile) {
596     compile_program();
597   }
598   render_fbs();
599   QSize letterb = get_letter_box(model->get_quest().get_properties().get_normal_quest_size(), frameSize());
600   int x = (frameSize().width() - letterb.width()) / 2;
601   int y = (frameSize().height() - letterb.height()) / 2;
602   gl->glViewport(x, y, letterb.width(), letterb.height());
603   switch (preview_mode) {
604   case ShaderPreviewMode::INPUT:
605     render_swipe(1.f);
606     break;
607   case ShaderPreviewMode::OUTPUT:
608     render_swipe(0.f);
609     break;
610   case ShaderPreviewMode::SIDE_BY_SIDE:
611     render_sbs();
612     break;
613   case ShaderPreviewMode::SWIPE:
614   {
615     QPoint p = mapFromGlobal(QCursor::pos());
616     float factor = (p.x() - x) / (float) letterb.width();
617     render_swipe(factor); // TODO pass actual factor
618     break;
619   }
620   default:
621     break;
622   }
623 }
624 
625 /**
626  * @brief QOpengl initialisation event, initialize as much as possible
627  */
initializeGL()628 void ShaderPreviewer::initializeGL() {
629 
630 #ifdef SOLARUSEDITOR_DEBUG_GL
631   gl_logger.initialize();
632   gl_logger.startLogging(QOpenGLDebugLogger::SynchronousLogging);
633   connect(&gl_logger,&QOpenGLDebugLogger::messageLogged, this, &ShaderPreviewer::on_gl_log);
634 #endif
635   QSurfaceFormat format = context()->format();
636 
637   auto make_number = [&](int major,int minor) -> QString {
638     switch(major*10+minor){
639       case 20:
640         return "110";
641       case 21:
642         return "120";
643       case 30:
644         return "130";
645       case 31:
646         return "140";
647       case 32:
648         return "150";
649       default:
650       if(major*10+minor >= 33) {
651         return QString::number(major*100+minor*10);
652       } else {
653         return "110";
654       }
655     }
656   };
657 
658   glsl_version = QString("#version %1\n").arg(make_number(format.majorVersion(),format.minorVersion()));
659 
660   // Setup quad
661   QOpenGLFunctions* gl = context()->functions();
662   gl->initializeOpenGLFunctions();
663   gl->glClearColor(0.3f,0.3f,0.3f,1);
664   gl->glDisable(GL_DEPTH_TEST);
665   gl->glDisable(GL_CULL_FACE);
666   vertex_buffer = new QOpenGLBuffer();
667   if (!vertex_buffer->create()) {
668     qWarning() << "Failed to create glbuffer!"; // TODO fail gracefully
669   }
670   Solarus::VertexArray array;
671   array.add_quad(Solarus::Rectangle(0, 0, 1, 1),
672                  Solarus::Rectangle(0, 1, 1, -1),
673                  Solarus::Color::white);
674   vertex_buffer->bind();
675   vertex_buffer->allocate(array.data(), array.vertex_count() * sizeof(Solarus::Vertex));
676   vertex_buffer->release();
677   // Create empty vao for core profiles
678   vao = new QOpenGLVertexArrayObject();
679   vao->create();
680 
681   // Create simple shader
682   simple_program.addShaderFromSourceCode(
683         QOpenGLShader::Vertex,
684         Solarus::DefaultShaders::get_default_vertex_source().c_str());
685   simple_program.addShaderFromSourceCode(
686         QOpenGLShader::Fragment,
687         Solarus::DefaultShaders::get_default_fragment_source().c_str());
688   if (!simple_program.link()) {
689     emit shader_error("Failed to link basic shader program:\n" + simple_program.log());
690   }
691 
692   // Create swipe shader
693   swipe_program.addShaderFromSourceCode(
694         QOpenGLShader::Vertex,
695         sanitizeShaderCode(SWIPE_VERTEX_SHADER));
696 
697   swipe_program.addShaderFromSourceCode(
698         QOpenGLShader::Fragment,
699         sanitizeShaderCode(SWIPE_FRAGMENT_SHADER));
700   if (!swipe_program.link()) {
701     emit shader_error("Failed to link swipe shader program:\n" + swipe_program.log());
702   }
703 
704   // OpenGL is now ready, build the texture if it was already set.
705   build_preview_texture();
706 }
707 
708 /**
709  * @brief QOpenGL resize event, framebuffer are recreated at the right size
710  * @param w width
711  * @param h height
712  */
resizeGL(int w,int h)713 void ShaderPreviewer::resizeGL(int w, int h) {
714   Q_UNUSED(w);
715   Q_UNUSED(h);
716   if (model != nullptr) {
717     if (model->get_scaling_factor() > 0) {
718       // Intermediate surface with fixed size
719       setup_framebuffers(
720           model->get_scaling_factor() * model->get_quest().get_properties().get_normal_quest_size());
721     } else {
722       QSize letterb = get_letter_box(model->get_quest().get_properties().get_normal_quest_size(), frameSize());
723       setup_framebuffers(letterb);
724     }
725   }
726 }
727 
728 /**
729  * @brief notify change of the model scaling factor
730  * @param factor
731  */
on_scaling_factor_changed(double factor)732 void ShaderPreviewer::on_scaling_factor_changed(double factor) {
733   Q_UNUSED(factor);
734   /*setup_framebuffers(
735         factor * model->get_quest().get_properties().get_normal_quest_size());*/
736   resizeGL(0,0);
737   update();
738 }
739 
740 /**
741  * @brief notify change of the model sources
742  */
on_source_changed()743 void ShaderPreviewer::on_source_changed() {
744   should_recompile = true;
745   update();
746 }
747 
748 /**
749  * @brief compile the model shader
750  */
compile_program()751 void ShaderPreviewer::compile_program() {
752   program.removeAllShaders();
753   emit shader_compilation_started(model->get_shader_id());
754   auto check_warnings = [this]{
755     if(program.log().size()) {
756       emit shader_warning(program.log());
757     }
758   };
759   auto fail = [this](const QString& s){
760     emit shader_error(s);
761     should_recompile = false;
762   };
763 
764   auto readFile = [](const QString& path) -> QString {
765         QFile file(path);
766         file.open(QIODevice::ReadOnly);
767 
768         QTextStream s1(&file);
769         return s1.readAll();
770   };
771 
772   QString vertex_source;
773   QString fragment_source;
774 
775   QString vertex_file_path = model->get_quest().get_shader_code_file_path(model->get_vertex_file());
776   if (!model->get_quest().exists(vertex_file_path) ||
777       !model->get_quest().is_shader_code_file(vertex_file_path)) {
778     vertex_file_path = QString();
779   }
780   if (!vertex_file_path.isEmpty()) {
781     vertex_source = readFile(vertex_file_path);
782   } else {
783     vertex_source = QString::fromStdString(Solarus::DefaultShaders::get_default_vertex_source());
784   }
785 
786   QString fragment_file_path = model->get_quest().get_shader_code_file_path(model->get_fragment_file());
787   if (!model->get_quest().exists(fragment_file_path) ||
788       !model->get_quest().is_shader_code_file(fragment_file_path)) {
789     fragment_file_path = QString();
790   }
791   if (!fragment_file_path.isEmpty()) {
792     fragment_source = readFile(fragment_file_path);
793   } else {
794     fragment_source = QString::fromStdString(Solarus::DefaultShaders::get_default_fragment_source());
795   }
796 
797   if (!program.addShaderFromSourceCode(
798         QOpenGLShader::Vertex,
799         sanitizeShaderCode(vertex_source))) {
800     return fail("Failed to compile vertex shader:\n" + program.log());
801   }
802   if (!program.addShaderFromSourceCode(
803         QOpenGLShader::Fragment,
804         sanitizeShaderCode(fragment_source))) {
805     return fail("Failed to compile fragment shader:\n" + program.log());
806   }
807 
808   if (!program.link()) {
809     return fail("Failed to link shader program:\n" + program.log());
810   } else {
811     check_warnings();
812   }
813 
814   should_recompile = false;
815   emit shader_compilation_finished(model->get_shader_id());
816 }
817 
818 #ifdef SOLARUSEDITOR_DEBUG_GL
819 /**
820  * @brief OpenGL error log slot
821  * @param message
822  */
on_gl_log(const QOpenGLDebugMessage & message)823 void ShaderPreviewer::on_gl_log(const QOpenGLDebugMessage& message) {
824   qDebug() << message;
825 }
826 #endif
827 
828 /**
829  * @brief Changes the image to be displayed in the preview widget.
830  * @param image The new image to show.
831  */
set_preview_image(QImage image)832 void ShaderPreviewer::set_preview_image(QImage image) {
833 
834   if (image == this->preview_image) {
835     return;
836   }
837   this->preview_image = image;
838   build_preview_texture();
839 }
840 
841 /**
842  * @brief Creates the OpenGL texture to be displayed.
843  */
build_preview_texture()844 void ShaderPreviewer::build_preview_texture() {
845 
846   if (vertex_buffer == nullptr) {
847     // OpenGL is not initialized yet.
848     return;
849   }
850 
851   // TODO manage memory
852   makeCurrent();
853   delete input_texture;
854   input_texture = nullptr;
855 
856   if (!preview_image.isNull()) {
857     input_texture = new QOpenGLTexture(preview_image, QOpenGLTexture::MipMapGeneration::DontGenerateMipMaps);
858   }
859   zoom = 1;
860   translation = QVector2D();
861 
862   update();
863 }
864 
865 }
866