1 /**
2  * \file    cairo_renderer.c
3  * \brief   Cairo-based renderer for the GTK3 backend
4  *
5  * \author  Michael C. Martin <mcmartin@gmail.com>
6  */
7 
8 /* This file is part of VICE, the Versatile Commodore Emulator.
9  * See README for copyright notice.
10  *
11  *  This program is free software; you can redistribute it and/or modify
12  *  it under the terms of the GNU General Public License as published by
13  *  the Free Software Foundation; either version 2 of the License, or
14  *  (at your option) any later version.
15  *
16  *  This program is distributed in the hope that it will be useful,
17  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  *  GNU General Public License for more details.
20  *
21  *  You should have received a copy of the GNU General Public License
22  *  along with this program; if not, write to the Free Software
23  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
24  *  02111-1307  USA.
25  *
26  */
27 
28 #include "cairo_renderer.h"
29 
30 #include <string.h>
31 
32 #include "lib.h"
33 #include "resources.h"
34 #include "ui.h"
35 #include "video.h"
36 
37 /** \brief Rendering context for the Cairo backend.
38  *  \sa video_canvas_s::renderer_context */
39 typedef struct vice_cairo_renderer_context_s {
40     /** \brief The cairo image surface that holds the pixels of the
41      *         machine's screen. */
42     cairo_surface_t *backing_surface;
43     /** \brief The affine transform to scale and translate the backing
44      *         surface when it is displayed on the canvas. */
45     cairo_matrix_t transform;
46 } context_t;
47 
48 /** \brief Rendering callback to display the screen as we understand
49  *         it.
50  *  \param widget The GtkDrawingArea we are rendering to.
51  *  \param cr     The Cairo context that we draw through.
52  *  \param data   The video_canvas_t we're rendering from.
53  *  \return TRUE if no further processing is needed on this event.
54  */
55 static gboolean
draw_canvas_cairo_cb(GtkWidget * widget,cairo_t * cr,gpointer data)56 draw_canvas_cairo_cb (GtkWidget *widget, cairo_t *cr, gpointer data)
57 {
58     video_canvas_t *canvas = (video_canvas_t *)data;
59     context_t *ctx = canvas ? (context_t *)canvas->renderer_context : NULL;
60 
61     /* Half-grey background for those parts of the window that aren't
62      * video, or black if it's fullscreen.
63      * TODO: configurable? */
64     if (ui_is_fullscreen()) {
65         cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
66     } else {
67         cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
68     }
69     cairo_paint(cr);
70 
71     if (ctx && ctx->backing_surface) {
72         cairo_pattern_t *pattern = cairo_pattern_create_for_surface(ctx->backing_surface);
73         cairo_pattern_set_matrix(pattern, &ctx->transform);
74         cairo_pattern_set_filter(pattern, CAIRO_FILTER_FAST);
75         cairo_set_source(cr, pattern);
76         cairo_paint(cr);
77         cairo_pattern_destroy(pattern);
78     }
79 
80     return FALSE;
81 }
82 
83 /** \brief  Callback to adjust scaling and offset when the window is
84  *          resized but the underlying machine screen is not.
85  *  \param widget The GtkDrawingArea being resized.
86  *  \param event  The GdkEventConfigure that is triggered this callback.
87  *  \param data   The video_canvas_t that controls this drawing area.
88  *  \return TRUE if no further processing is needed on this event.
89  */
90 static gboolean
resize_canvas_container_cairo_cb(GtkWidget * widget,GdkEventConfigure * event,gpointer data)91 resize_canvas_container_cairo_cb (GtkWidget *widget, GdkEventConfigure *event, gpointer data)
92 {
93     /* The GtkDrawingArea that holds the canvas is "widget." */
94     video_canvas_t *canvas = (video_canvas_t *)data;
95     context_t *ctx = canvas ? (context_t *)canvas->renderer_context : NULL;
96     if (ctx && ctx->backing_surface) {
97         /* Size of source canvas */
98         double source_width = (double)cairo_image_surface_get_width(ctx->backing_surface);
99         double source_height = (double)cairo_image_surface_get_height(ctx->backing_surface);
100         /* Size of widget */
101         double width = (double)gtk_widget_get_allocated_width(widget);
102         double height = (double)gtk_widget_get_allocated_height(widget);
103         double scale_x = 1.0, scale_y = 1.0;
104         double offset_x = 0.0, offset_y = 0.0;
105         int keepaspect=1, trueaspect=0;
106         resources_get_int("KeepAspectRatio", &keepaspect);
107         resources_get_int("TrueAspectRatio", &trueaspect);
108 
109         if (keepaspect) {
110             double aspect_fix = 1.0;
111             if (trueaspect) {
112                 aspect_fix = canvas->geometry->pixel_aspect_ratio;
113             }
114             /* Try the Y-fit first */
115             double scale = source_height / height;
116             if (source_width * aspect_fix / scale > width) {
117                 /* Need to X-fit instead */
118                 scale = (double)source_width * aspect_fix / width;
119                 offset_y = ((source_height / scale) - height) / 2.0;
120             } else {
121                 offset_x = ((source_width * aspect_fix / scale) - width) / 2.0;
122             }
123             scale_x = scale / aspect_fix;
124             scale_y = scale;
125         } else {
126             scale_x = (double)source_width / width;
127             scale_y = (double)source_height / height;
128         }
129         /* Apply the computed scaling factor to both dimensions */
130         cairo_matrix_init_scale(&ctx->transform, scale_x, scale_y);
131         /* Center the result in the widget */
132         cairo_matrix_translate(&ctx->transform, offset_x, offset_y);
133         /* Record the window coordinates of where the result will be */
134         canvas->screen_display_w = (double)source_width / scale_x;
135         canvas->screen_display_h = (double)source_height / scale_y;
136         canvas->screen_origin_x = ((double)width - canvas->screen_display_w) / 2.0;
137         canvas->screen_origin_y = ((double)height - canvas->screen_display_h) / 2.0;
138     }
139     /* No further processing should be needed */
140     return FALSE;
141 }
142 
143 /** \brief Cairo implementation of create_widget.
144  *
145  *  \param canvas The canvas to create the widget for.
146  *  \return The newly created canvas.
147  *  \sa vice_renderer_backend_s::create_widget
148  */
vice_cairo_create_widget(video_canvas_t * canvas)149 static GtkWidget *vice_cairo_create_widget(video_canvas_t *canvas)
150 {
151     GtkWidget *widget = gtk_drawing_area_new();
152     gtk_widget_set_hexpand(widget, TRUE);
153     gtk_widget_set_vexpand(widget, TRUE);
154     canvas->drawing_area = widget;
155     g_signal_connect(widget, "draw", G_CALLBACK(draw_canvas_cairo_cb), canvas);
156     g_signal_connect(widget, "configure_event", G_CALLBACK(resize_canvas_container_cairo_cb), canvas);
157     return widget;
158 }
159 
160 /** \brief Cairo implementation of destroy_context.
161  *
162  *  \param canvas The canvas whose renderer_context is to be
163  *                deleted
164  *  \sa vice_renderer_backend_s::destroy_context
165  */
vice_cairo_destroy_context(video_canvas_t * canvas)166 static void vice_cairo_destroy_context(video_canvas_t *canvas)
167 {
168     if (canvas) {
169         context_t *ctx = (context_t *)canvas->renderer_context;
170         if (!ctx) {
171             return;
172         }
173         if (ctx->backing_surface) {
174             cairo_surface_finish(ctx->backing_surface);
175             cairo_surface_destroy(ctx->backing_surface);
176         }
177         lib_free(ctx);
178         canvas->renderer_context = NULL;
179     }
180 }
181 
182 /** \brief Cairo implementation of update_context.
183  * \param canvas The canvas being resized or initially created.
184  * \param width The new width for the machine's screen.
185  * \param height The new height for the machine's screen.
186  * \sa vice_renderer_backend_s::update_context
187  */
vice_cairo_update_context(video_canvas_t * canvas,unsigned int width,unsigned int height)188 static void vice_cairo_update_context(video_canvas_t *canvas, unsigned int width, unsigned int height)
189 {
190     if (canvas) {
191         context_t *ctx = (context_t *)canvas->renderer_context;
192 
193         if (ctx && ctx->backing_surface) {
194             unsigned int source_width, source_height;
195             source_width = cairo_image_surface_get_width(ctx->backing_surface);
196             source_height = cairo_image_surface_get_height(ctx->backing_surface);
197             if (source_width == width && source_height == height) {
198                 /* Canvas already exists and is the proper size */
199                 return;
200             }
201         } else {
202             if (width == 0 || height == 0) {
203                 /* We have no surface and were asked to create a
204                  * surface of area zero, so we're done */
205                 return;
206             }
207         }
208         /* If we get this far, we have new dimensions for our canvas */
209         vice_cairo_destroy_context(canvas);
210         ctx = lib_malloc(sizeof(context_t));
211         if (ctx) {
212             ctx->backing_surface = NULL;
213             cairo_matrix_init_identity(&ctx->transform);
214         }
215         canvas->renderer_context = ctx;
216         if (width != 0 && height != 0) {
217             /* Actually create the backing surface */
218             int keepaspect=1, trueaspect=0;
219             double aspect = 1.0;
220             resources_get_int("KeepAspectRatio", &keepaspect);
221             resources_get_int("TrueAspectRatio", &trueaspect);
222             if (keepaspect && trueaspect) {
223                 aspect = canvas->geometry->pixel_aspect_ratio;
224             }
225 
226             ctx->backing_surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
227 
228             /* Configure the matrix to fit it in the widget as it exists */
229             resize_canvas_container_cairo_cb (canvas->drawing_area, NULL, canvas);
230 
231             /* Fix the widget's size request */
232             gtk_widget_set_size_request(canvas->drawing_area, width * aspect, height);
233         }
234     }
235 }
236 
237 /** \brief Cairo implementation of refresh_rect.
238  * \param canvas The canvas being rendered to
239  * \param xs     A parameter to forward to video_canvas_render()
240  * \param ys     A parameter to forward to video_canvas_render()
241  * \param xi     X coordinate of the leftmost pixel to update
242  * \param yi     Y coordinate of the topmost pixel to update
243  * \param w      Width of the rectangle to update
244  * \param h      Height of the rectangle to update
245  * \sa vice_renderer_backend_s::refresh_rect */
vice_cairo_refresh_rect(video_canvas_t * canvas,unsigned int xs,unsigned int ys,unsigned int xi,unsigned int yi,unsigned int w,unsigned int h)246 static void vice_cairo_refresh_rect(video_canvas_t *canvas,
247                                     unsigned int xs, unsigned int ys,
248                                     unsigned int xi, unsigned int yi,
249                                     unsigned int w, unsigned int h)
250 {
251     context_t *ctx = canvas ? (context_t *)canvas->renderer_context : NULL;
252     unsigned char *backbuffer;
253     if (!ctx || !ctx->backing_surface) {
254         return;
255     }
256 
257     if (((xi + w) > cairo_image_surface_get_width(ctx->backing_surface)) ||
258         ((yi+h) > cairo_image_surface_get_height(ctx->backing_surface))) {
259         /* Trying to draw outside canvas? */
260         fprintf(stderr, "Attempt to draw outside canvas!\nXI%u YI%u W%u H%u CW%u CH%u\n", xi, yi, w, h, cairo_image_surface_get_width(ctx->backing_surface), cairo_image_surface_get_height(ctx->backing_surface));
261         return;
262     }
263 
264     cairo_surface_flush(ctx->backing_surface);
265     backbuffer = cairo_image_surface_get_data(ctx->backing_surface);
266     if (!backbuffer) {
267         return;
268     }
269     video_canvas_render(canvas, backbuffer, w, h, xs, ys, xi, yi, cairo_image_surface_get_stride(ctx->backing_surface), 32);
270     cairo_surface_mark_dirty_rectangle(ctx->backing_surface, xi, yi, w, h);
271     gtk_widget_queue_draw(canvas->drawing_area);
272 }
273 
274 /** \brief Cairo implementation of set_palette.
275  * \param canvas The canvas being initialized
276  * \sa vice_renderer_backend_s::set_palette */
vice_cairo_set_palette(video_canvas_t * canvas)277 static void vice_cairo_set_palette(video_canvas_t *canvas)
278 {
279     int i;
280     struct palette_s *palette = canvas ? canvas->palette : NULL;
281     if (!palette) {
282         return;
283     }
284     /* If we get this far we know canvas is also non-NULL */
285 
286     /* We use CAIRO_FORMAT_RGB24, which is defined as follows: "Each
287      * pixel is a 32-bit quantity, with the upper 8 bits unused. Red,
288      * Green, and Blue are stored in the remaining 24 bits in that
289      * order." */
290     for (i = 0; i < palette->num_entries; i++) {
291         palette_entry_t color = palette->entries[i];
292         uint32_t cairo_color = (color.red << 16) | (color.green << 8) | color.blue;
293         video_render_setphysicalcolor(canvas->videoconfig, i, cairo_color, 32);
294     }
295 
296     for (i = 0; i < 256; i++) {
297         video_render_setrawrgb(i, i << 16, i << 8, i);
298     }
299     video_render_initraw(canvas->videoconfig);
300 }
301 
302 vice_renderer_backend_t vice_cairo_backend = {
303     vice_cairo_create_widget,
304     vice_cairo_update_context,
305     vice_cairo_destroy_context,
306     vice_cairo_refresh_rect,
307     vice_cairo_set_palette
308 };
309