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