[PATCH v2 11/12] macdrv/surface: Make data copies explicit so memory pages are reusable.
Elaine Lefler
elaineclefler at gmail.com
Thu Mar 24 23:46:25 CDT 2022
Signed-off-by: Elaine Lefler <elaineclefler at gmail.com>
---
v2: Was PATCH 3/3. Splitting into smaller patches as per feedback.
---
dlls/winemac.drv/cocoa_window.h | 2 +-
dlls/winemac.drv/cocoa_window.m | 8 +-
dlls/winemac.drv/macdrv_cocoa.h | 8 +-
dlls/winemac.drv/surface.c | 425 ++++++++++++++++++++++++++++----
4 files changed, 381 insertions(+), 62 deletions(-)
diff --git a/dlls/winemac.drv/cocoa_window.h b/dlls/winemac.drv/cocoa_window.h
index b1f7e595ec9..596e3c52b3e 100644
--- a/dlls/winemac.drv/cocoa_window.h
+++ b/dlls/winemac.drv/cocoa_window.h
@@ -57,7 +57,7 @@ @interface WineWindow : NSPanel <NSWindowDelegate>
BOOL shapeChangedSinceLastDraw;
BOOL colorKeyed;
- CGFloat colorKeyRed, colorKeyGreen, colorKeyBlue;
+ uint8_t colorKeyRed, colorKeyGreen, colorKeyBlue;
BOOL usePerPixelAlpha;
diff --git a/dlls/winemac.drv/cocoa_window.m b/dlls/winemac.drv/cocoa_window.m
index 1b66c97b19a..656a5ba2283 100644
--- a/dlls/winemac.drv/cocoa_window.m
+++ b/dlls/winemac.drv/cocoa_window.m
@@ -409,7 +409,7 @@ @interface WineWindow ()
@property (readonly, nonatomic) BOOL needsTransparency;
@property (nonatomic) BOOL colorKeyed;
- at property (nonatomic) CGFloat colorKeyRed, colorKeyGreen, colorKeyBlue;
+ at property (nonatomic) uint8_t colorKeyRed, colorKeyGreen, colorKeyBlue;
@property (nonatomic) BOOL usePerPixelAlpha;
@property (assign, nonatomic) void* imeData;
@@ -552,7 +552,7 @@ - (void) updateLayer
imageRect.origin.y *= layer.contentsScale;
imageRect.size.width *= layer.contentsScale;
imageRect.size.height *= layer.contentsScale;
- image = create_surface_image(window.surface, &imageRect, FALSE, window.colorKeyed,
+ image = create_surface_image(window.surface, &imageRect, window.colorKeyed,
window.colorKeyRed, window.colorKeyGreen, window.colorKeyBlue);
}
pthread_mutex_unlock(window.surface_mutex);
@@ -3561,8 +3561,8 @@ void macdrv_set_window_alpha(macdrv_window w, CGFloat alpha)
/***********************************************************************
* macdrv_set_window_color_key
*/
-void macdrv_set_window_color_key(macdrv_window w, CGFloat keyRed, CGFloat keyGreen,
- CGFloat keyBlue)
+void macdrv_set_window_color_key(macdrv_window w, uint8_t keyRed, uint8_t keyGreen,
+ uint8_t keyBlue)
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
WineWindow* window = (WineWindow*)w;
diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h
index c7f87888fdc..b0eb86133c4 100644
--- a/dlls/winemac.drv/macdrv_cocoa.h
+++ b/dlls/winemac.drv/macdrv_cocoa.h
@@ -581,14 +581,14 @@ extern void macdrv_order_cocoa_window(macdrv_window w, macdrv_window prev,
extern void macdrv_get_cocoa_window_frame(macdrv_window w, CGRect* out_frame) DECLSPEC_HIDDEN;
extern void macdrv_set_cocoa_parent_window(macdrv_window w, macdrv_window parent) DECLSPEC_HIDDEN;
extern void macdrv_set_window_surface(macdrv_window w, void *surface, pthread_mutex_t *mutex) DECLSPEC_HIDDEN;
-extern CGImageRef create_surface_image(void *window_surface, CGRect *rect, int copy_data, int color_keyed,
- CGFloat key_red, CGFloat key_green, CGFloat key_blue) DECLSPEC_HIDDEN;
+extern CGImageRef create_surface_image(void *window_surface, CGRect *dirty_area, int color_keyed,
+ uint8_t key_red, uint8_t key_green, uint8_t key_blue) DECLSPEC_HIDDEN;
extern int get_surface_blit_rects(void *window_surface, const CGRect **rects, int *count) DECLSPEC_HIDDEN;
extern void macdrv_window_needs_display(macdrv_window w, CGRect rect) DECLSPEC_HIDDEN;
extern void macdrv_set_window_shape(macdrv_window w, const CGRect *rects, int count) DECLSPEC_HIDDEN;
extern void macdrv_set_window_alpha(macdrv_window w, CGFloat alpha) DECLSPEC_HIDDEN;
-extern void macdrv_set_window_color_key(macdrv_window w, CGFloat keyRed, CGFloat keyGreen,
- CGFloat keyBlue) DECLSPEC_HIDDEN;
+extern void macdrv_set_window_color_key(macdrv_window w, uint8_t keyRed, uint8_t keyGreen,
+ uint8_t keyBlue) DECLSPEC_HIDDEN;
extern void macdrv_clear_window_color_key(macdrv_window w) DECLSPEC_HIDDEN;
extern void macdrv_window_use_per_pixel_alpha(macdrv_window w, int use_per_pixel_alpha) DECLSPEC_HIDDEN;
extern void macdrv_give_cocoa_window_focus(macdrv_window w, int activate) DECLSPEC_HIDDEN;
diff --git a/dlls/winemac.drv/surface.c b/dlls/winemac.drv/surface.c
index 6b0abb10780..eed31229473 100644
--- a/dlls/winemac.drv/surface.c
+++ b/dlls/winemac.drv/surface.c
@@ -65,6 +65,7 @@ struct macdrv_window_surface
HRGN drawn;
BOOL use_alpha;
RGNDATA *blit_data;
+ struct shadow_surface *shadow;
BYTE *bits;
pthread_mutex_t mutex;
BITMAPINFO info; /* variable size, must be last */
@@ -72,6 +73,250 @@ struct macdrv_window_surface
static struct macdrv_window_surface *get_mac_surface(struct window_surface *surface);
+/* Shadow surfaces provide a secondary bitmap to enable multithreaded drawing
+ * without locking the main surface for an extended period of time.
+ *
+ * A shadow may contain one or more bitmaps, allocated on demand. Bitmaps are
+ * kept in a linked list, re-using existing memory whenever possible. This is
+ * important because paging in large regions of memory can have a latency of
+ * >20ms, which is not acceptable for drawing.
+ *
+ * Unlike macdrv_window_surface, shadow data can be allocated or deallocated
+ * from a non-Wine thread, so it requires an independent reference counter and
+ * must not use Windows functions. */
+struct shadow_surface
+{
+ pthread_mutex_t mutex;
+ int refcount;
+ int bitmap_length; /* size of shadow_bitmap in bytes */
+ struct shadow_bitmap *bitmaps; /* linked list or NULL */
+};
+
+struct shadow_bitmap
+{
+ union
+ {
+ struct
+ {
+ struct shadow_surface *parent;
+ struct shadow_bitmap *next;
+ CFAllocatorRef cfdata_deallocator;
+ };
+ BYTE pad[32]; /* should be a multiple of 16 */
+ };
+ BYTE bits[0]; /* variable length */
+};
+
+/* Number of shadow bitmaps to keep. Rasterization is asynchronous, so more than
+ * one bitmap may be alive at any given time. Usually 2 is sufficient. This is
+ * a failsafe to ensure the list can't grow indefinitely. */
+#define SHADOW_BITMAP_MAX 3
+
+static struct shadow_bitmap *shadow_bitmap_free(struct shadow_bitmap *bitmap,
+ int bitmap_length);
+static void shadow_cfdata_dealloc(void *ptr, void *info);
+
+/***********************************************************************
+ * shadow_surface_create
+ *
+ * Creates a new shadow surface. Its refcount is initially set to 1.
+ * Returns NULL on failure.
+ */
+static struct shadow_surface *shadow_surface_create(struct macdrv_window_surface* parent)
+{
+ /* Note: actual bitmap length is rounded up to the nearest pagesize */
+ int bitmap_length = sizeof(struct shadow_bitmap)
+ + parent->info.bmiHeader.biSizeImage;
+
+ /* Although this function can only be called from a Windows thread, the
+ * deallocator can be called from a macOS thread, so we must use calloc
+ * instead of HeapAlloc. */
+ struct shadow_surface *shadow = calloc(1, sizeof(struct shadow_surface));
+ if (!shadow)
+ return NULL;
+
+ if (pthread_mutex_init(&shadow->mutex, NULL) != 0)
+ {
+ free(shadow);
+ return NULL;
+ }
+
+ shadow->refcount = 1;
+ shadow->bitmap_length = bitmap_length;
+ return shadow;
+}
+
+/***********************************************************************
+ * shadow_surface_release
+ *
+ * Decrements shadow surface's refcount. If it becomes zero, the shadow
+ * is freed along with all of its bitmaps.
+ *
+ * shadow->mutex must be held before calling this function. The mutex
+ * will be released on exit and you should not attempt to access the
+ * surface again.
+ *
+ * IMPORTANT: This function is called from non-Wine threads, so it
+ * must not use Win32 or Wine functions, including debug
+ * logging.
+ */
+static void shadow_surface_release(struct shadow_surface *shadow)
+{
+ int bitmap_length = shadow->bitmap_length;
+ int refcount = --shadow->refcount;
+ struct shadow_bitmap *bitmap = shadow->bitmaps;
+
+ pthread_mutex_unlock(&shadow->mutex);
+
+ if (refcount != 0)
+ return;
+
+ pthread_mutex_destroy(&shadow->mutex);
+ free(shadow);
+ while (bitmap)
+ bitmap = shadow_bitmap_free(bitmap, bitmap_length);
+}
+
+/***********************************************************************
+ * shadow_bitmap_take
+ *
+ * Obtains a shadow bitmap from the given shadow surface. Returns the
+ * bitmap on success or NULL on failure.
+ *
+ * On success, shadow->refcount has been incremented. The call must be
+ * balanced with a call to shadow_bitmap_return, usually accomplished by
+ * passing bitmap->cfdata_deallocator to CFDataCreateWithBytesNoCopy as
+ * bytesDeallocator.
+ *
+ * IMPORTANT: This function is called from non-Wine threads, so it
+ * must not use Win32 or Wine functions, including debug
+ * logging.
+ */
+static struct shadow_bitmap *shadow_bitmap_take(struct shadow_surface *shadow)
+{
+ int bitmap_length;
+ struct shadow_bitmap *bitmap;
+ CFAllocatorContext cfa_context = {
+ .version = 0, .info = NULL,
+ .retain = NULL, .release = NULL, .copyDescription = NULL,
+ .allocate = NULL, .reallocate = NULL,
+ .deallocate = shadow_cfdata_dealloc, .preferredSize = NULL
+ };
+
+ pthread_mutex_lock(&shadow->mutex);
+ shadow->refcount++;
+
+ bitmap_length = shadow->bitmap_length;
+ bitmap = shadow->bitmaps;
+ if (bitmap)
+ {
+ /* Got an existing bitmap, remove it from the linked list */
+ shadow->bitmaps = bitmap->next;
+ bitmap->next = NULL;
+ }
+
+ pthread_mutex_unlock(&shadow->mutex);
+
+ if (bitmap)
+ return bitmap;
+
+ /* Nothing was available, make a new bitmap. Once again we cannot use
+ * Windows functions. mmap is most appropriate due to its low CPU overhead
+ * and the fact that bitmaps are usually large. Pass shadow as a hint so the
+ * bitmap will be allocated in a similar memory region. */
+ bitmap = mmap(shadow, bitmap_length, PROT_READ | PROT_WRITE,
+ MAP_ANON | MAP_PRIVATE, -1, 0);
+ if (bitmap == MAP_FAILED)
+ goto failed;
+
+ cfa_context.info = bitmap;
+ bitmap->cfdata_deallocator = CFAllocatorCreate(NULL, &cfa_context);
+ if (!bitmap->cfdata_deallocator)
+ goto failed_unmap;
+
+ bitmap->parent = shadow;
+ return bitmap;
+
+failed_unmap:
+ munmap(bitmap, bitmap_length);
+failed:
+ /* Balance refcount++ since shadow_bitmap_return won't be called */
+ pthread_mutex_lock(&shadow->mutex);
+ shadow_surface_release(shadow);
+ return NULL;
+}
+
+/***********************************************************************
+ * shadow_bitmap_return
+ *
+ * Returns a shadow bitmap to its linked list and decrements the
+ * refcount in the corresponding shadow_surface.
+ *
+ * IMPORTANT: This function is called from non-Wine threads, so it
+ * must not use Win32 or Wine functions, including debug
+ * logging.
+ */
+static void shadow_bitmap_return(struct shadow_bitmap *bitmap)
+{
+ struct shadow_surface *shadow = bitmap->parent;
+ struct shadow_bitmap **tail = &shadow->bitmaps;
+ int bitmap_length;
+ int num_bitmaps = 0;
+
+ pthread_mutex_lock(&shadow->mutex);
+ bitmap_length = shadow->bitmap_length;
+
+ /* Append the bitmap to the end of the list */
+ while (*tail)
+ {
+ num_bitmaps++;
+ tail = &(*tail)->next;
+ }
+
+ /* Refuse to return this bitmap if there are already too many. Free it
+ * instead. */
+ if (num_bitmaps >= SHADOW_BITMAP_MAX)
+ shadow_bitmap_free(bitmap, bitmap_length);
+ else
+ *tail = bitmap;
+
+ shadow_surface_release(shadow);
+}
+
+/***********************************************************************
+ * shadow_bitmap_free
+ *
+ * Deletes a shadow bitmap. Returns bitmap->next.
+ *
+ * IMPORTANT: This function is called from non-Wine threads, so it
+ * must not use Win32 or Wine functions, including debug
+ * logging.
+ */
+static struct shadow_bitmap *shadow_bitmap_free(struct shadow_bitmap *bitmap,
+ int bitmap_length)
+{
+ struct shadow_bitmap *next = bitmap->next;
+
+ CFRelease(bitmap->cfdata_deallocator);
+ munmap(bitmap, bitmap_length);
+
+ return next;
+}
+
+/***********************************************************************
+ * shadow_cfdata_dealloc
+ *
+ * Wraps shadow_bitmap_return for CFAllocator.
+ *
+ * IMPORTANT: This function is called from non-Wine threads, so it
+ * must not use Win32 or Wine functions, including debug
+ * logging.
+ */
+static void shadow_cfdata_dealloc(void *ptr, void *info)
+{
+ shadow_bitmap_return(info);
+}
+
/***********************************************************************
* update_blit_data
*/
@@ -207,8 +452,14 @@ static void macdrv_surface_destroy(struct window_surface *window_surface)
TRACE("freeing %p bits %p\n", surface, surface->bits);
if (surface->region) DeleteObject(surface->region);
if (surface->drawn) DeleteObject(surface->drawn);
+ if (surface->shadow)
+ {
+ pthread_mutex_lock(&surface->shadow->mutex);
+ shadow_surface_release(surface->shadow);
+ }
+ if (surface->bits && surface->bits != MAP_FAILED)
+ munmap(surface->bits, surface->info.bmiHeader.biSizeImage);
HeapFree(GetProcessHeap(), 0, surface->blit_data);
- HeapFree(GetProcessHeap(), 0, surface->bits);
pthread_mutex_destroy(&surface->mutex);
HeapFree(GetProcessHeap(), 0, surface);
}
@@ -293,13 +544,19 @@ struct window_surface *create_surface(macdrv_window window, const RECT *rect,
}
update_blit_data(surface);
surface->use_alpha = use_alpha;
- surface->bits = HeapAlloc(GetProcessHeap(), 0, surface->info.bmiHeader.biSizeImage);
- if (!surface->bits) goto failed;
+ surface->shadow = shadow_surface_create(surface);
+ if (!surface->shadow) goto failed;
+ /* Map bitmap close to the shadow */
+ surface->bits = mmap(surface->shadow, surface->info.bmiHeader.biSizeImage,
+ PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
+ if (surface->bits == MAP_FAILED) goto failed;
window_background = macdrv_window_background_color();
memset_pattern4(surface->bits, &window_background, surface->info.bmiHeader.biSizeImage);
- TRACE("created %p for %p %s bits %p-%p\n", surface, window, wine_dbgstr_rect(rect),
- surface->bits, surface->bits + surface->info.bmiHeader.biSizeImage);
+ TRACE("created %p for %p %s bits %p-%p shadow %p\n",
+ surface, window, wine_dbgstr_rect(rect),
+ surface->bits, surface->bits + surface->info.bmiHeader.biSizeImage,
+ surface->shadow);
return &surface->header;
@@ -362,77 +619,139 @@ int get_surface_blit_rects(void *window_surface, const CGRect **rects, int *coun
/***********************************************************************
* create_surface_image
*
- * Caller must hold the surface lock. On input, *rect is the requested
- * image rect, relative to the window whole_rect, a.k.a. visible_rect.
- * On output, it's been intersected with that part backed by the surface
- * and is the actual size of the returned image. copy_data indicates if
- * the caller will keep the returned image beyond the point where the
- * surface bits can be guaranteed to remain valid and unchanged. If so,
- * the bits are copied instead of merely referenced by the image.
+ * Creates a CGImageRef from the given surface. Returns NULL if there is
+ * nothing to draw. On input, *dirty_area is the requested image rect,
+ * relative to the window whole_rect, a.k.a. visible_rect. On output,
+ * it's been intersected with that part backed by the surface and is the
+ * actual size of the returned image.
*
* IMPORTANT: This function is called from non-Wine threads, so it
* must not use Win32 or Wine functions, including debug
* logging.
*/
-CGImageRef create_surface_image(void *window_surface, CGRect *rect, int copy_data, int color_keyed,
- CGFloat key_red, CGFloat key_green, CGFloat key_blue)
+CGImageRef create_surface_image(void *window_surface, CGRect *dirty_area, int color_keyed,
+ uint8_t key_red, uint8_t key_green, uint8_t key_blue)
{
- CGImageRef cgimage = NULL;
+ static CGColorSpaceRef colorspace = NULL;
+ CGImageAlphaInfo alpha_info = kCGImageAlphaNoneSkipFirst;
+ CGImageRef cgimage;
+ CFDataRef data;
+ CGDataProviderRef provider;
+
struct macdrv_window_surface *surface = get_mac_surface(window_surface);
- int width, height;
+ struct shadow_bitmap *shadow;
- width = surface->header.rect.right - surface->header.rect.left;
- height = surface->header.rect.bottom - surface->header.rect.top;
- *rect = CGRectIntersection(cgrect_from_rect(surface->header.rect), *rect);
- if (!CGRectIsEmpty(*rect))
- {
- CGRect visrect;
- CGColorSpaceRef colorspace;
- CGDataProviderRef provider;
- int bytes_per_row, offset, size;
- CGImageAlphaInfo alphaInfo;
+ int surface_width, bytes_per_row, bitmap_length;
+ int dirty_width, dirty_height, offset;
- visrect = CGRectOffset(*rect, -surface->header.rect.left, -surface->header.rect.top);
+ if (!surface)
+ return NULL;
+ if (!colorspace)
colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
- bytes_per_row = get_dib_stride(width, 32);
- offset = CGRectGetMinX(visrect) * 4 + CGRectGetMinY(visrect) * bytes_per_row;
- size = min(CGRectGetHeight(visrect) * bytes_per_row,
- surface->info.bmiHeader.biSizeImage - offset);
- if (copy_data)
+ pthread_mutex_lock(&surface->mutex);
+ surface_width = surface->header.rect.right - surface->header.rect.left;
+ bytes_per_row = get_dib_stride(surface_width, 32);
+ bitmap_length = surface->info.bmiHeader.biSizeImage;
+ if (surface->use_alpha)
+ alpha_info = kCGImageAlphaPremultipliedFirst;
+
+ /* Clip to bitmap area */
+ *dirty_area = CGRectIntersection(cgrect_from_rect(surface->header.rect),
+ CGRectIntegral(*dirty_area));
+
+ dirty_width = CGRectGetWidth(*dirty_area);
+ dirty_height = CGRectGetHeight(*dirty_area);
+
+ if (dirty_width <= 0 || dirty_height <= 0)
+ goto failed;
+
+ shadow = shadow_bitmap_take(surface->shadow);
+ if (!shadow)
+ goto failed;
+
+ /* Find location from which to read data */
+ offset = (CGRectGetMinX(*dirty_area) - surface->header.rect.left) * 4
+ + (surface->header.rect.bottom - CGRectGetMaxY(*dirty_area))
+ * bytes_per_row;
+
+ if (!color_keyed)
+ {
+ /* Copy pixels to shadow buffer. If the width is close or equal to the
+ * whole bitmap (<= 32 bytes), it's faster to copy one large region. */
+ if (dirty_width >= surface_width - 8)
{
- CFDataRef data = CFDataCreate(NULL, (UInt8*)surface->bits + offset, size);
- provider = CGDataProviderCreateWithCFData(data);
- CFRelease(data);
+ /* Align to 16-byte boundaries to get the best memcpy performance */
+ int align_offset = offset & ~15;
+ int align_size = ((offset + (dirty_height - 1) * bytes_per_row
+ + dirty_width * 4 + 15) & ~15) - align_offset;
+ memcpy(shadow->bits + align_offset,
+ surface->bits + align_offset,
+ align_size);
}
else
- provider = CGDataProviderCreateWithData(NULL, surface->bits + offset, size, NULL);
-
- alphaInfo = surface->use_alpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
- cgimage = CGImageCreate(CGRectGetWidth(visrect), CGRectGetHeight(visrect),
- 8, 32, bytes_per_row, colorspace,
- alphaInfo | kCGBitmapByteOrder32Little,
- provider, NULL, retina_on, kCGRenderingIntentDefault);
- CGDataProviderRelease(provider);
- CGColorSpaceRelease(colorspace);
+ {
+ /* Copying a smaller width of the bitmap, go line by line */
+ int y;
+ for (y = 0; y < dirty_height; y++)
+ {
+ int line_offset = offset + y * bytes_per_row;
+ int align_offset = line_offset & ~15;
+ int align_size = ((line_offset + dirty_width * 4 + 15) & ~15)
+ - align_offset;
+ memcpy(shadow->bits + align_offset,
+ surface->bits + align_offset,
+ align_size);
+ }
+ }
+ }
+ else
+ {
+ /* If using a color key, convert those pixels to transparent. This is
+ * the best time to do it since we have to copy the data anyway. */
+ DWORD key = (key_red << 16) | (key_green << 8) | (key_blue);
+ int x, y;
- if (color_keyed)
+ for (y = 0; y < dirty_height; y++)
{
- CGImageRef maskedImage;
- CGFloat components[] = { key_red - 0.5, key_red + 0.5,
- key_green - 0.5, key_green + 0.5,
- key_blue - 0.5, key_blue + 0.5 };
- maskedImage = CGImageCreateWithMaskingColors(cgimage, components);
- if (maskedImage)
+ for (x = 0; x < dirty_width; x++)
{
- CGImageRelease(cgimage);
- cgimage = maskedImage;
+ DWORD src = *(DWORD*)(surface->bits + offset
+ + x * 4 + y * bytes_per_row);
+ DWORD* dst = (DWORD*)(shadow->bits + offset
+ + x * 4 + y * bytes_per_row);
+ if ((src & 0x00FFFFFF) == key)
+ *dst = 0;
+ else if (!surface->use_alpha)
+ /* dst must have alpha channel even if src does not */
+ *dst = src | 0xFF000000;
+ else
+ *dst = src;
}
}
+
+ alpha_info = kCGImageAlphaPremultipliedFirst;
}
+ /* Safe to unlock surface now */
+ pthread_mutex_unlock(&surface->mutex);
+
+ data = CFDataCreateWithBytesNoCopy(NULL, shadow->bits + offset,
+ bitmap_length - offset, shadow->cfdata_deallocator);
+ provider = CGDataProviderCreateWithCFData(data);
+ CFRelease(data);
+
+ cgimage = CGImageCreate(dirty_width, dirty_height, 8, 32, bytes_per_row,
+ colorspace, alpha_info | kCGBitmapByteOrder32Little,
+ provider, NULL, FALSE, kCGRenderingIntentDefault);
+ CFRelease(provider);
+
return cgimage;
+
+failed:
+ pthread_mutex_unlock(&surface->mutex);
+ return NULL;
}
/***********************************************************************
--
2.32.0 (Apple Git-132)
More information about the wine-devel
mailing list