1 /** \file   quartz_renderer.c
2  * \brief   Quartz-based renderer for the GTK3 backend
3  *
4  * \author  Michael C. Martin <mcmartin@gmail.com>
5  */
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 "quartz_renderer.h"
29 
30 #ifdef MACOSX_SUPPORT
31 
32 #include <CoreGraphics/CoreGraphics.h>
33 
34 #include <cairo/cairo-quartz.h>
35 
36 #include "lib.h"
37 #include "resources.h"
38 #include "ui.h"
39 #include "video.h"
40 
41 typedef struct vice_quartz_renderer_context_s {
42     CGContextRef bitmap_context;
43     CGLayerRef layer;
44     double scale_x, scale_y;
45 } context_t;
46 
47 /* Note that the ::draw signal receives a ready-to-be-used cairo_t
48  * that is already clipped to only draw the exposed areas of the
49  * widget */
50 static gboolean
draw_canvas_quartz_cb(GtkWidget * widget,cairo_t * cr,gpointer data)51 draw_canvas_quartz_cb (GtkWidget *widget, cairo_t *cr, gpointer data)
52 {
53     video_canvas_t *canvas = (video_canvas_t *)data;
54     context_t *ctx = canvas ? (context_t *)canvas->renderer_context : NULL;
55     cairo_surface_t *target = cairo_get_target(cr);
56     CGContextRef display = NULL;
57     CGRect clip_rect;
58 
59     cairo_surface_flush(target);
60     display = cairo_quartz_surface_get_cg_context(target);
61     if (!display) {
62         fprintf(stderr, "Rendering impossible as the display area isn't a Quartz surface: %d\n", cairo_surface_get_type(target));
63         return FALSE;
64     }
65 
66     CGContextSaveGState(display);
67     CGContextScaleCTM(display, 1.0, -1.0);
68     clip_rect = CGContextGetClipBoundingBox(display);
69 
70     if (ui_is_fullscreen()) {
71         CGContextSetRGBFillColor(display, 0.0f, 0.0f, 0.0f, 1.0f);
72     } else {
73         CGContextSetRGBFillColor(display, 0.5f, 0.5f, 0.5f, 1.0f);
74     }
75     CGContextFillRect(display, clip_rect);
76 
77     if (ctx && ctx->bitmap_context) {
78         CGImageRef frame = CGBitmapContextCreateImage(ctx->bitmap_context);
79         if (!ctx->layer) {
80             CGSize targetSize = { CGBitmapContextGetWidth(ctx->bitmap_context),
81                                   CGBitmapContextGetHeight(ctx->bitmap_context) };
82             ctx->layer = CGLayerCreateWithContext(ctx->bitmap_context, targetSize, NULL);
83         }
84         if (ctx->layer) {
85             CGRect layer_rect = { { 0, 0 }, CGLayerGetSize(ctx->layer) };
86             CGRect img_rect = clip_rect;
87             img_rect.size.width *= ctx->scale_x;
88             img_rect.size.height *= ctx->scale_y;
89 
90             CGContextDrawImage(CGLayerGetContext(ctx->layer), layer_rect, frame);
91             CGContextScaleCTM(display,
92                               img_rect.size.width / layer_rect.size.width,
93                               img_rect.size.height / layer_rect.size.height);
94             clip_rect = CGContextGetClipBoundingBox(display);
95             img_rect = clip_rect;
96             img_rect.size.width *= ctx->scale_x;
97             img_rect.size.height *= ctx->scale_y;
98             img_rect.origin.x += (clip_rect.size.width - img_rect.size.width) / 2;
99             img_rect.origin.y += (clip_rect.size.height - img_rect.size.height) / 2;
100             CGContextSetInterpolationQuality(display, kCGInterpolationNone);
101             CGContextDrawLayerAtPoint(display, img_rect.origin, ctx->layer);
102         } else {
103             CGRect img_rect = clip_rect;
104             img_rect.size.width *= ctx->scale_x;
105             img_rect.size.height *= ctx->scale_y;
106             img_rect.origin.x += (clip_rect.size.width - img_rect.size.width) / 2;
107             img_rect.origin.y += (clip_rect.size.height - img_rect.size.height) / 2;
108             CGContextDrawImage(display, img_rect, frame);
109         }
110         CGImageRelease(frame);
111     }
112     CGContextRestoreGState(display);
113     cairo_surface_mark_dirty(target);
114 
115     return FALSE;
116 }
117 
118 /** \brief  Callback to handle cases where the window is resized but
119  *          the canvas is not
120  */
121 static gboolean
resize_canvas_container_quartz_cb(GtkWidget * widget,GdkEventConfigure * event,gpointer data)122 resize_canvas_container_quartz_cb (GtkWidget *widget, GdkEventConfigure *event, gpointer data)
123 {
124     /* The GtkDrawingArea that holds the canvas is "widget." */
125     video_canvas_t *canvas = (video_canvas_t *)data;
126     context_t *ctx = canvas ? (context_t *)canvas->renderer_context : NULL;
127     if (ctx && ctx->bitmap_context) {
128         /* Size of source canvas */
129         double source_width = (double)CGBitmapContextGetWidth(ctx->bitmap_context);
130         double source_height = (double)CGBitmapContextGetHeight(ctx->bitmap_context);
131         /* Size of widget */
132         double width = (double)gtk_widget_get_allocated_width(widget);
133         double height = (double)gtk_widget_get_allocated_height(widget);
134         double scale_x = 1.0, scale_y = 1.0;
135         int keepaspect=1, trueaspect=0;
136         resources_get_int("KeepAspectRatio", &keepaspect);
137         resources_get_int("TrueAspectRatio", &trueaspect);
138 
139         if (source_width < 1 || source_height < 1) {
140             return FALSE;
141         }
142 
143         if (keepaspect) {
144             double canvas_aspect, viewport_aspect;
145 
146             viewport_aspect = width / height;
147             canvas_aspect = source_width / source_height;
148             if (trueaspect) {
149                 canvas_aspect *= canvas->geometry->pixel_aspect_ratio;
150             }
151             if (canvas_aspect < viewport_aspect) {
152                 scale_x = canvas_aspect / viewport_aspect;
153                 scale_y = 1.0f;
154             } else {
155                 scale_x = 1.0f;
156                 scale_y = viewport_aspect / canvas_aspect;
157             }
158         } else {
159             scale_x = 1.0;
160             scale_y = 1.0;
161         }
162         ctx->scale_x = scale_x;
163         ctx->scale_y = scale_y;
164         canvas->screen_display_w = (double)width * ctx->scale_x;
165         canvas->screen_display_h = (double)height * ctx->scale_y;
166         canvas->screen_origin_x = ((double)width - canvas->screen_display_w) / 2.0;
167         canvas->screen_origin_y = ((double)height - canvas->screen_display_h) / 2.0;
168     }
169     /* No further processing should be needed */
170     return FALSE;
171 }
172 
vice_quartz_create_widget(video_canvas_t * canvas)173 static GtkWidget *vice_quartz_create_widget(video_canvas_t *canvas)
174 {
175     GtkWidget *widget = gtk_drawing_area_new();
176     gtk_widget_set_hexpand(widget, TRUE);
177     gtk_widget_set_vexpand(widget, TRUE);
178     canvas->drawing_area = widget;
179     g_signal_connect(widget, "draw", G_CALLBACK(draw_canvas_quartz_cb), canvas);
180     g_signal_connect(widget, "configure_event", G_CALLBACK(resize_canvas_container_quartz_cb), canvas);
181     return widget;
182 }
183 
vice_quartz_destroy_context(video_canvas_t * canvas)184 static void vice_quartz_destroy_context(video_canvas_t *canvas)
185 {
186     if (canvas) {
187         context_t *ctx = (context_t *)canvas->renderer_context;
188         if (!ctx) {
189             return;
190         }
191         if (ctx->bitmap_context) {
192             CGContextRelease(ctx->bitmap_context);
193         }
194         if (ctx->layer) {
195             CGLayerRelease(ctx->layer);
196         }
197         lib_free(ctx);
198         canvas->renderer_context = NULL;
199     }
200 }
201 
vice_quartz_update_context(video_canvas_t * canvas,unsigned int width,unsigned int height)202 static void vice_quartz_update_context(video_canvas_t *canvas, unsigned int width, unsigned int height)
203 {
204     if (canvas) {
205         context_t *ctx = (context_t *)canvas->renderer_context;
206 
207         if (ctx && ctx->bitmap_context) {
208             unsigned int source_width, source_height;
209             source_width = CGBitmapContextGetWidth(ctx->bitmap_context);
210             source_height = CGBitmapContextGetHeight(ctx->bitmap_context);
211             if (source_width == width && source_height == height) {
212                 /* Canvas already exists and is the proper size */
213                 return;
214             }
215         } else {
216             if (width == 0 || height == 0) {
217                 /* We have no surface and were asked to create a
218                  * surface of area zero, so we're done */
219                 return;
220             }
221         }
222         /* If we get this far, we have new dimensions for our canvas */
223         vice_quartz_destroy_context(canvas);
224         ctx = lib_malloc(sizeof(context_t));
225         if (ctx) {
226             ctx->bitmap_context = NULL;
227             ctx->layer = NULL;
228             ctx->scale_x = 1.0;
229             ctx->scale_y = 1.0;
230         }
231         canvas->renderer_context = ctx;
232         if (width != 0 && height != 0 && ctx) {
233             /* Actually create the bitmap context */
234             int keepaspect=1, trueaspect=0;
235             double aspect = 1.0;
236             CGColorSpaceRef colorspace = NULL;
237             resources_get_int("KeepAspectRatio", &keepaspect);
238             resources_get_int("TrueAspectRatio", &trueaspect);
239             if (keepaspect && trueaspect) {
240                 aspect = canvas->geometry->pixel_aspect_ratio;
241             }
242 
243             /* Possible TODO: If someone wants to get real fancy they
244              * could they could, at least on Macs, define reference
245              * whites and blacks and use a calibrated RGB system. As a
246              * rule we seem to address this by tuning our palettes in
247              * VICE itself, though. */
248             colorspace = CGColorSpaceCreateDeviceRGB();
249             ctx->bitmap_context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorspace, kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little);
250             CGColorSpaceRelease(colorspace);
251 
252             /* Configure the matrix to fit it in the widget as it exists */
253             resize_canvas_container_quartz_cb (canvas->drawing_area, NULL, canvas);
254 
255             /* Fix the widget's size request */
256             gtk_widget_set_size_request(canvas->drawing_area, width * aspect, height);
257         }
258     }
259 }
260 
vice_quartz_refresh_rect(video_canvas_t * canvas,unsigned int xs,unsigned int ys,unsigned int xi,unsigned int yi,unsigned int w,unsigned int h)261 static void vice_quartz_refresh_rect(video_canvas_t *canvas,
262                                     unsigned int xs, unsigned int ys,
263                                     unsigned int xi, unsigned int yi,
264                                     unsigned int w, unsigned int h)
265 {
266     context_t *ctx = canvas ? (context_t *)canvas->renderer_context : NULL;
267     unsigned char *backbuffer;
268     if (!ctx || !ctx->bitmap_context) {
269         return;
270     }
271 
272     if (((xi + w) > CGBitmapContextGetWidth(ctx->bitmap_context)) ||
273         ((yi+h) > CGBitmapContextGetHeight(ctx->bitmap_context))) {
274         /* Trying to draw outside canvas? */
275         fprintf(stderr, "Attempt to draw outside canvas!\nXI%u YI%u W%u H%u CW%lu CH%lu\n", xi, yi, w, h, CGBitmapContextGetWidth(ctx->bitmap_context), CGBitmapContextGetHeight(ctx->bitmap_context));
276         return;
277     }
278 
279     backbuffer = CGBitmapContextGetData(ctx->bitmap_context);
280     if (!backbuffer) {
281         return;
282     }
283     video_canvas_render(canvas, backbuffer, w, h, xs, ys, xi, yi,  CGBitmapContextGetBytesPerRow(ctx->bitmap_context), 32);
284     gtk_widget_queue_draw(canvas->drawing_area);
285 }
286 
vice_quartz_set_palette(video_canvas_t * canvas)287 static void vice_quartz_set_palette(video_canvas_t *canvas)
288 {
289     int i;
290     struct palette_s *palette = canvas ? canvas->palette : NULL;
291     if (!palette) {
292         return;
293     }
294     /* If we get this far we know canvas is also non-NULL */
295 
296     /* We use use the Device RGB color space with "no alpha, skip first", which leaves the most significant bits unused and should have R, G, B in decreasing significance in machine byte order. */
297     for (i = 0; i < palette->num_entries; i++) {
298         palette_entry_t color = palette->entries[i];
299         uint32_t rgb_color = (color.red << 16) | (color.green << 8) | color.blue;
300         video_render_setphysicalcolor(canvas->videoconfig, i, rgb_color, 32);
301     }
302 
303     for (i = 0; i < 256; i++) {
304         video_render_setrawrgb(i, i << 16, i << 8, i);
305     }
306     video_render_initraw(canvas->videoconfig);
307 }
308 
309 vice_renderer_backend_t vice_quartz_backend = {
310     vice_quartz_create_widget,
311     vice_quartz_update_context,
312     vice_quartz_destroy_context,
313     vice_quartz_refresh_rect,
314     vice_quartz_set_palette
315 };
316 
317 #endif
318