1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License
4  * as published by the Free Software Foundation; either version 2
5  * of the License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software
14  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  *
16  * See the COPYING file for license information.
17  *
18  * Guillaume Chazarain <guichaz@gmail.com>
19  */
20 
21 /***********************
22  * Rendering functions *
23  ***********************/
24 
25 #include <stdlib.h>             /* abs() */
26 
27 #include "gliv.h"
28 #include "rendering.h"
29 #include "options.h"
30 #include "gliv-image.h"
31 #include "matrix.h"
32 #include "params.h"
33 #include "zoom_frame.h"
34 #include "scrollbars.h"
35 #include "textures.h"
36 #include "files_list.h"
37 #include "loading.h"
38 #include "windows.h"
39 #include "history.h"
40 #include "next_image.h"
41 #include "opengl.h"
42 #include "transition.h"
43 #include "images_menus.h"
44 
45 extern rt_struct *rt;
46 extern options_struct *options;
47 extern GlivImage *current_image;
48 extern GtkWidget *gl_widget;
49 
50 static GTimeVal last_redraw_scheduled;
51 static gint last_redraw_delay;
52 
set_filter(gint filter)53 static void set_filter(gint filter)
54 {
55     gint id;
56     texture_map *map;
57 
58     /*
59      * Only the textures in the first map change their filter,
60      * since in the others maps the image is zoomed out.
61      */
62     map = current_image->maps;
63 
64     for (id = 0; id < map->nb_tiles; id++) {
65         glBindTexture(GL_TEXTURE_2D, map->tex_ids[id]);
66         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
67         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
68     }
69 }
70 
is_filtering_enabled(void)71 static gboolean is_filtering_enabled(void)
72 {
73     texture_map *map;
74     gint filter;
75 
76     /* Only the first map changes its filter. */
77     map = current_image->maps;
78 
79     glBindTexture(GL_TEXTURE_2D, map->tex_ids[0]);
80     glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &filter);
81 
82     return filter == GL_LINEAR;
83 }
84 
call_lists(gboolean smooth,gint level)85 static void call_lists(gboolean smooth, gint level)
86 {
87     gint id;
88     texture_map *map;
89 
90     if (level == 0 && is_filtering_enabled() != smooth)
91         set_filter(smooth ? GL_LINEAR : GL_NEAREST);
92 
93     map = current_image->maps + level;
94 
95     for (id = 0; id < map->nb_tiles; id++)
96         if (matrix_tile_visible(map->tiles + id))
97             glCallList(map->list + id);
98 }
99 
draw_checker(void)100 static void draw_checker(void)
101 {
102     static guint tex_id = 0;
103     gint i;
104     gfloat half_w, half_h;
105     gboolean matrix_changed;
106 
107     glDisable(GL_DITHER);
108     glClear(GL_COLOR_BUFFER_BIT);
109     matrix_changed = get_matrix_has_changed();
110 
111     if (rt->alpha_checks_changed) {
112         gushort texture[12];
113         gushort alpha1[3] = {
114             options->alpha1.red,
115             options->alpha1.green,
116             options->alpha1.blue
117         };
118         gushort alpha2[3] = {
119             options->alpha2.red,
120             options->alpha2.green,
121             options->alpha2.blue
122         };
123 
124         if (tex_id == 0)
125             glGenTextures(1, &tex_id);
126 
127         for (i = 0; i < 3; i++) {
128             texture[i] = texture[9 + i] = alpha1[i];
129             texture[3 + i] = texture[6 + i] = alpha2[i];
130         }
131 
132         glBindTexture(GL_TEXTURE_2D, tex_id);
133 
134         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
135         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
136 
137         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
138         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
139 
140         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB,
141                      GL_UNSIGNED_SHORT, texture);
142 
143         rt->alpha_checks_changed = FALSE;
144     } else
145         glBindTexture(GL_TEXTURE_2D, tex_id);
146 
147     if (matrix_changed == FALSE)
148         /* glMatrixMode(GL_MODELVIEW); */
149         /* Save the matrix only if we will not replace it when redrawing. */
150         glPushMatrix();
151 
152     glLoadIdentity();
153 
154     half_w = rt->wid_size->width / 2.0;
155     half_h = rt->wid_size->height / 2.0;
156 
157     glBegin(GL_QUADS);
158 
159     glTexCoord2f(0.0, 0.0);
160     glVertex2f(-half_w, -half_h);
161 
162     glTexCoord2f(half_w / 16.0, 0.0);
163     glVertex2f(half_w, -half_h);
164 
165     glTexCoord2f(half_w / 16.0, half_h / 16.0);
166     glVertex2f(half_w, half_h);
167 
168     glTexCoord2f(0.0, half_h / 16.0);
169     glVertex2f(-half_w, half_h);
170 
171     glEnd();
172 
173     if (matrix_changed == FALSE)
174         /* glMatrixMode(GL_MODELVIEW); */
175         glPopMatrix();
176 
177     glEnable(GL_DITHER);
178 }
179 
choose_mipmap_level(void)180 static gint choose_mipmap_level(void)
181 {
182     gfloat zoom, mipmap_ratio = MIPMAP_RATIO;
183     gint level = 1;
184 
185     if (options->mipmap) {
186         zoom = get_matrix_zoom();
187 
188         while (mipmap_ratio > zoom && level < current_image->nb_maps) {
189             mipmap_ratio *= MIPMAP_RATIO;
190             level++;
191         }
192     }
193 
194     /* Mipmaps should only be scaled down. */
195     return level - 1;
196 }
197 
display_last_image_notice(void)198 static void display_last_image_notice(void)
199 {
200     static GdkGC *gc = NULL;
201     const gchar *msg;
202     PangoContext *pc;
203     PangoLayout *pl;
204     gint x = 10, y = 10, dx = 5, dy = 5;
205     gint w, h;
206 
207     msg = get_image_notice();
208     if (msg == NULL)
209         return;
210 
211     pc = gtk_widget_get_pango_context(gl_widget);
212     pl = pango_layout_new(pc);
213 
214     if (gc == NULL)
215         /* First time. */
216         gc = gdk_gc_new(gl_widget->window);
217 
218     pango_layout_set_text(pl, msg, -1);
219 
220     gdk_gc_set_foreground(gc, &(gl_widget->style->white));
221     pango_layout_get_pixel_size(pl, &w, &h);
222     gdk_draw_rectangle(gl_widget->window, gc, TRUE,
223                        x - dx, y - dy, w + 2 * dx, h + 2 * dy);
224 
225     gdk_gc_set_foreground(gc, &(gl_widget->style->black));
226     gdk_draw_layout(gl_widget->window, gc, x, y, pl);
227 
228     g_object_unref(pl);
229 }
230 
need_alpha_checks(void)231 static gboolean need_alpha_checks(void)
232 {
233     return current_image != NULL && current_image->has_alpha &&
234         options->alpha_checks;
235 }
236 
draw_current_image(void)237 void draw_current_image(void)
238 {
239     gint mipmap_level;
240     gboolean filtering;
241     gboolean alpha_checks = need_alpha_checks();
242 
243     if (alpha_checks)
244         draw_checker();
245     else {
246         /*
247          * I don't know why but this seems to be required to see images instead
248          * black rectangles with Mesa's software rendering.
249          */
250         glBegin(GL_QUADS);
251         glVertex2f(0, 0);
252         glEnd();
253     }
254 
255     write_gl_matrix();
256 
257     mipmap_level = choose_mipmap_level();
258     if (options->filtering == FALSE)
259         filtering = FALSE;
260     else if (mipmap_level == 0)
261         filtering = is_filtering_needed();
262     else
263         filtering = TRUE;
264 
265     if (alpha_checks) {
266         glPushAttrib(GL_ENABLE_BIT);
267         glEnable(GL_BLEND);
268     }
269 
270     call_lists(filtering, mipmap_level);
271 
272     if (alpha_checks)
273         glPopAttrib();
274 }
275 
276 /* Called by the timer when a redraw is needed. */
redraw(void)277 static void redraw(void)
278 {
279     static GdkGLDrawable *gldrawable = NULL;
280     GTimeVal now;
281 
282     if (is_in_transition())
283         return;
284 
285     if (gldrawable == NULL)
286         /* First time. */
287         gldrawable = gtk_widget_get_gl_drawable(gl_widget);
288 
289     gdk_gl_drawable_wait_gdk(gldrawable);
290     clear_zoom_frame();
291 
292     if (need_alpha_checks() == FALSE) {
293         glDisable(GL_DITHER);
294         glClear(GL_COLOR_BUFFER_BIT);
295         glEnable(GL_DITHER);
296     }
297 
298     if (current_image != NULL)
299         draw_current_image();
300 
301     gdk_gl_drawable_swap_buffers(gldrawable);
302     gdk_gl_drawable_wait_gl(gldrawable);
303 
304     display_last_image_notice();
305     refresh(REFRESH_SCROLL);
306 
307     g_get_current_time(&now);
308     last_redraw_delay = diff_timeval_us(&now, &last_redraw_scheduled);
309 }
310 
311 /* Called the first time an image is displayed. */
render(void)312 void render(void)
313 {
314     gboolean list_changed;
315 
316     configure_matrix(current_image);
317     prioritize_textures(current_image, TRUE);
318 
319     refresh(REFRESH_NOW | APPEND_HISTORY | REFRESH_STATUS);
320 
321     /* A bit dumb, but needed on some cards. */
322     refresh(REFRESH_NOW);
323 
324     /* Post rendering */
325     list_changed = remove_obsolete_nodes();
326     if (list_changed)
327         do_later(G_PRIORITY_LOW, cond_rebuild_menus);
328     update_current_image_status(list_changed);
329 
330     refresh(REFRESH_TITLE);
331     current_image->first_image = FALSE;
332 }
333 
zoom_in(gfloat ratio)334 void zoom_in(gfloat ratio)
335 {
336     gint pointer_x, pointer_y;
337     gfloat x, y;
338 
339     if (options->zoom_pointer) {
340         /* The pointer is the zoom center. */
341         gdk_window_get_pointer(gl_widget->window, &pointer_x, &pointer_y, NULL);
342         x = (gfloat) pointer_x;
343         y = (gfloat) pointer_y;
344     } else {
345         /* The zoom center is the midle of the window. */
346         x = rt->wid_size->width / 2.0;
347         y = rt->wid_size->height / 2.0;
348     }
349 
350     matrix_zoom(ratio, x, y);
351     refresh(REFRESH_IMAGE | REFRESH_STATUS | APPEND_HISTORY);
352 }
353 
354 static gboolean has_do_later(GSourceDummyMarshal idle_func);
355 static void add_do_later(GSourceDummyMarshal idle_func);
356 static void remove_do_later(GSourceDummyMarshal idle_func);
357 
refresh_burst(void)358 static gboolean refresh_burst(void)
359 {
360     remove_do_later(redraw);
361     refresh(REFRESH_BURST);
362 
363     return FALSE;
364 }
365 
diff_timeval_us(GTimeVal * after,GTimeVal * before)366 gint diff_timeval_us(GTimeVal * after, GTimeVal * before)
367 {
368     GTimeVal diff;
369 
370     diff_timeval(&diff, after, before);
371     return diff.tv_sec * G_USEC_PER_SEC + diff.tv_usec;
372 }
373 
get_fps_limiter_delay(GTimeVal * previous_frame)374 gint get_fps_limiter_delay(GTimeVal * previous_frame)
375 {
376     GTimeVal now;
377     gint diff;
378 
379     if (options->fps <= 0)
380         return 0;
381 
382     g_get_current_time(&now);
383     diff =
384         G_USEC_PER_SEC / options->fps - diff_timeval_us(&now, previous_frame);
385 
386     return diff;
387 }
388 
refresh(gint what)389 void refresh(gint what)
390 {
391     /* GTK does that automatically but with more overhead. */
392     if (what & REFRESH_IMAGE) {
393         if (!has_do_later(redraw)) {
394             g_get_current_time(&last_redraw_scheduled);
395             do_later(GDK_PRIORITY_REDRAW, redraw);
396         }
397     } else if (what & REFRESH_BURST) {
398         if (!has_do_later(redraw)) {
399             gint diff = get_fps_limiter_delay(&last_redraw_scheduled);
400             diff -= last_redraw_delay;
401 
402             if (diff > 0) {
403                 add_do_later(redraw);
404                 g_timeout_add(diff / 4000, (GtkFunction) refresh_burst, NULL);
405             } else
406                 refresh(REFRESH_IMAGE);
407         }
408     } else if (what & REFRESH_NOW) {
409         remove_do_later(redraw);
410         redraw();
411     }
412 
413     if (what & REFRESH_STATUS)
414         do_later(G_PRIORITY_HIGH, update_status_bar);
415 
416     if (what & APPEND_HISTORY)
417         append_history();
418 
419     if (what & REFRESH_TITLE)
420         do_later(G_PRIORITY_DEFAULT, update_window_title);
421 
422     if (what & REFRESH_SCROLL)
423         update_scrollbars();
424 }
425 
update_current_image_status(gboolean find_number)426 void update_current_image_status(gboolean find_number)
427 {
428     if (current_image != NULL) {
429         if (find_number)
430             current_image->number = -1;
431 
432         fill_ident(current_image);
433     }
434 
435     refresh(REFRESH_STATUS);
436 }
437 
438 static GHashTable *idle_functions = NULL;
439 
init_do_later(void)440 static void init_do_later(void)
441 {
442     if (idle_functions == NULL)
443         /* First time */
444         idle_functions = g_hash_table_new(g_direct_hash, NULL);
445 }
446 
has_do_later(GSourceDummyMarshal idle_func)447 static gboolean has_do_later(GSourceDummyMarshal idle_func)
448 {
449     init_do_later();
450     return g_hash_table_lookup(idle_functions, idle_func) != NULL;
451 }
452 
add_do_later(GSourceDummyMarshal idle_func)453 static void add_do_later(GSourceDummyMarshal idle_func)
454 {
455     init_do_later();
456     g_hash_table_insert(idle_functions, idle_func, idle_func);
457 }
458 
remove_do_later(GSourceDummyMarshal idle_func)459 static void remove_do_later(GSourceDummyMarshal idle_func)
460 {
461     init_do_later();
462     g_hash_table_remove(idle_functions, idle_func);
463 }
464 
465 
wrapper(gpointer data)466 static gboolean wrapper(gpointer data)
467 {
468     GSourceDummyMarshal func = data;
469     remove_do_later(func);
470     func();
471     return FALSE;
472 }
473 
do_later(gint priority,GSourceDummyMarshal func)474 void do_later(gint priority, GSourceDummyMarshal func)
475 {
476     if (has_do_later(func))
477         /* Already scheduled */
478         return;
479 
480     add_do_later(func);
481     g_idle_add_full(priority, wrapper, func, NULL);
482 }
483