| /* | 
 |  * QEMU Apple ParavirtualizedGraphics.framework device | 
 |  * | 
 |  * Copyright © 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. | 
 |  * | 
 |  * SPDX-License-Identifier: GPL-2.0-or-later | 
 |  * | 
 |  * ParavirtualizedGraphics.framework is a set of libraries that macOS provides | 
 |  * which implements 3d graphics passthrough to the host as well as a | 
 |  * proprietary guest communication channel to drive it. This device model | 
 |  * implements support to drive that library from within QEMU. | 
 |  */ | 
 |  | 
 | #include "qemu/osdep.h" | 
 | #include "qemu/lockable.h" | 
 | #include "qemu/cutils.h" | 
 | #include "qemu/log.h" | 
 | #include "qapi/visitor.h" | 
 | #include "qapi/error.h" | 
 | #include "block/aio-wait.h" | 
 | #include "exec/address-spaces.h" | 
 | #include "system/dma.h" | 
 | #include "migration/blocker.h" | 
 | #include "ui/console.h" | 
 | #include "apple-gfx.h" | 
 | #include "trace.h" | 
 |  | 
 | #include <mach/mach.h> | 
 | #include <mach/mach_vm.h> | 
 | #include <dispatch/dispatch.h> | 
 |  | 
 | #import <ParavirtualizedGraphics/ParavirtualizedGraphics.h> | 
 |  | 
 | static const AppleGFXDisplayMode apple_gfx_default_modes[] = { | 
 |     { 1920, 1080, 60 }, | 
 |     { 1440, 1080, 60 }, | 
 |     { 1280, 1024, 60 }, | 
 | }; | 
 |  | 
 | static Error *apple_gfx_mig_blocker; | 
 | static uint32_t next_pgdisplay_serial_num = 1; | 
 |  | 
 | static dispatch_queue_t get_background_queue(void) | 
 | { | 
 |     return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); | 
 | } | 
 |  | 
 | /* ------ PGTask and task operations: new/destroy/map/unmap ------ */ | 
 |  | 
 | /* | 
 |  * This implements the type declared in <ParavirtualizedGraphics/PGDevice.h> | 
 |  * which is opaque from the framework's point of view. It is used in callbacks | 
 |  * in the form of its typedef PGTask_t, which also already exists in the | 
 |  * framework headers. | 
 |  * | 
 |  * A "task" in PVG terminology represents a host-virtual contiguous address | 
 |  * range which is reserved in a large chunk on task creation. The mapMemory | 
 |  * callback then requests ranges of guest system memory (identified by their | 
 |  * GPA) to be mapped into subranges of this reserved address space. | 
 |  * This type of operation isn't well-supported by QEMU's memory subsystem, | 
 |  * but it is fortunately trivial to achieve with Darwin's mach_vm_remap() call, | 
 |  * which allows us to refer to the same backing memory via multiple virtual | 
 |  * address ranges. The Mach VM APIs are therefore used throughout for managing | 
 |  * task memory. | 
 |  */ | 
 | struct PGTask_s { | 
 |     QTAILQ_ENTRY(PGTask_s) node; | 
 |     AppleGFXState *s; | 
 |     mach_vm_address_t address; | 
 |     uint64_t len; | 
 |     /* | 
 |      * All unique MemoryRegions for which a mapping has been created in in this | 
 |      * task, and on which we have thus called memory_region_ref(). There are | 
 |      * usually very few regions of system RAM in total, so we expect this array | 
 |      * to be very short. Therefore, no need for sorting or fancy search | 
 |      * algorithms, linear search will do. | 
 |      * Protected by AppleGFXState's task_mutex. | 
 |      */ | 
 |     GPtrArray *mapped_regions; | 
 | }; | 
 |  | 
 | static PGTask_t *apple_gfx_new_task(AppleGFXState *s, uint64_t len) | 
 | { | 
 |     mach_vm_address_t task_mem; | 
 |     PGTask_t *task; | 
 |     kern_return_t r; | 
 |  | 
 |     r = mach_vm_allocate(mach_task_self(), &task_mem, len, VM_FLAGS_ANYWHERE); | 
 |     if (r != KERN_SUCCESS) { | 
 |         return NULL; | 
 |     } | 
 |  | 
 |     task = g_new0(PGTask_t, 1); | 
 |     task->s = s; | 
 |     task->address = task_mem; | 
 |     task->len = len; | 
 |     task->mapped_regions = g_ptr_array_sized_new(2 /* Usually enough */); | 
 |  | 
 |     QEMU_LOCK_GUARD(&s->task_mutex); | 
 |     QTAILQ_INSERT_TAIL(&s->tasks, task, node); | 
 |  | 
 |     return task; | 
 | } | 
 |  | 
 | static void apple_gfx_destroy_task(AppleGFXState *s, PGTask_t *task) | 
 | { | 
 |     GPtrArray *regions = task->mapped_regions; | 
 |     MemoryRegion *region; | 
 |     size_t i; | 
 |  | 
 |     for (i = 0; i < regions->len; ++i) { | 
 |         region = g_ptr_array_index(regions, i); | 
 |         memory_region_unref(region); | 
 |     } | 
 |     g_ptr_array_unref(regions); | 
 |  | 
 |     mach_vm_deallocate(mach_task_self(), task->address, task->len); | 
 |  | 
 |     QEMU_LOCK_GUARD(&s->task_mutex); | 
 |     QTAILQ_REMOVE(&s->tasks, task, node); | 
 |     g_free(task); | 
 | } | 
 |  | 
 | void *apple_gfx_host_ptr_for_gpa_range(uint64_t guest_physical, | 
 |                                        uint64_t length, bool read_only, | 
 |                                        MemoryRegion **mapping_in_region) | 
 | { | 
 |     MemoryRegion *ram_region; | 
 |     char *host_ptr; | 
 |     hwaddr ram_region_offset = 0; | 
 |     hwaddr ram_region_length = length; | 
 |  | 
 |     ram_region = address_space_translate(&address_space_memory, | 
 |                                          guest_physical, | 
 |                                          &ram_region_offset, | 
 |                                          &ram_region_length, !read_only, | 
 |                                          MEMTXATTRS_UNSPECIFIED); | 
 |  | 
 |     if (!ram_region || ram_region_length < length || | 
 |         !memory_access_is_direct(ram_region, !read_only)) { | 
 |         return NULL; | 
 |     } | 
 |  | 
 |     host_ptr = memory_region_get_ram_ptr(ram_region); | 
 |     if (!host_ptr) { | 
 |         return NULL; | 
 |     } | 
 |     host_ptr += ram_region_offset; | 
 |     *mapping_in_region = ram_region; | 
 |     return host_ptr; | 
 | } | 
 |  | 
 | static bool apple_gfx_task_map_memory(AppleGFXState *s, PGTask_t *task, | 
 |                                       uint64_t virtual_offset, | 
 |                                       PGPhysicalMemoryRange_t *ranges, | 
 |                                       uint32_t range_count, bool read_only) | 
 | { | 
 |     kern_return_t r; | 
 |     void *source_ptr; | 
 |     mach_vm_address_t target; | 
 |     vm_prot_t cur_protection, max_protection; | 
 |     bool success = true; | 
 |     MemoryRegion *region; | 
 |  | 
 |     RCU_READ_LOCK_GUARD(); | 
 |     QEMU_LOCK_GUARD(&s->task_mutex); | 
 |  | 
 |     trace_apple_gfx_map_memory(task, range_count, virtual_offset, read_only); | 
 |     for (int i = 0; i < range_count; i++) { | 
 |         PGPhysicalMemoryRange_t *range = &ranges[i]; | 
 |  | 
 |         target = task->address + virtual_offset; | 
 |         virtual_offset += range->physicalLength; | 
 |  | 
 |         trace_apple_gfx_map_memory_range(i, range->physicalAddress, | 
 |                                          range->physicalLength); | 
 |  | 
 |         region = NULL; | 
 |         source_ptr = apple_gfx_host_ptr_for_gpa_range(range->physicalAddress, | 
 |                                                       range->physicalLength, | 
 |                                                       read_only, ®ion); | 
 |         if (!source_ptr) { | 
 |             success = false; | 
 |             continue; | 
 |         } | 
 |  | 
 |         if (!g_ptr_array_find(task->mapped_regions, region, NULL)) { | 
 |             g_ptr_array_add(task->mapped_regions, region); | 
 |             memory_region_ref(region); | 
 |         } | 
 |  | 
 |         cur_protection = 0; | 
 |         max_protection = 0; | 
 |         /* Map guest RAM at range->physicalAddress into PG task memory range */ | 
 |         r = mach_vm_remap(mach_task_self(), | 
 |                           &target, range->physicalLength, vm_page_size - 1, | 
 |                           VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, | 
 |                           mach_task_self(), (mach_vm_address_t)source_ptr, | 
 |                           false /* shared mapping, no copy */, | 
 |                           &cur_protection, &max_protection, | 
 |                           VM_INHERIT_COPY); | 
 |         trace_apple_gfx_remap(r, source_ptr, target); | 
 |         g_assert(r == KERN_SUCCESS); | 
 |     } | 
 |  | 
 |     return success; | 
 | } | 
 |  | 
 | static void apple_gfx_task_unmap_memory(AppleGFXState *s, PGTask_t *task, | 
 |                                         uint64_t virtual_offset, uint64_t length) | 
 | { | 
 |     kern_return_t r; | 
 |     mach_vm_address_t range_address; | 
 |  | 
 |     trace_apple_gfx_unmap_memory(task, virtual_offset, length); | 
 |  | 
 |     /* | 
 |      * Replace task memory range with fresh 0 pages, undoing the mapping | 
 |      * from guest RAM. | 
 |      */ | 
 |     range_address = task->address + virtual_offset; | 
 |     r = mach_vm_allocate(mach_task_self(), &range_address, length, | 
 |                          VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE); | 
 |     g_assert(r == KERN_SUCCESS); | 
 | } | 
 |  | 
 | /* ------ Rendering and frame management ------ */ | 
 |  | 
 | static void apple_gfx_render_frame_completed_bh(void *opaque); | 
 |  | 
 | static void apple_gfx_render_new_frame(AppleGFXState *s) | 
 | { | 
 |     bool managed_texture = s->using_managed_texture_storage; | 
 |     uint32_t width = surface_width(s->surface); | 
 |     uint32_t height = surface_height(s->surface); | 
 |     MTLRegion region = MTLRegionMake2D(0, 0, width, height); | 
 |     id<MTLCommandBuffer> command_buffer = [s->mtl_queue commandBuffer]; | 
 |     id<MTLTexture> texture = s->texture; | 
 |  | 
 |     assert(bql_locked()); | 
 |     [texture retain]; | 
 |     [command_buffer retain]; | 
 |  | 
 |     s->rendering_frame_width = width; | 
 |     s->rendering_frame_height = height; | 
 |  | 
 |     dispatch_async(get_background_queue(), ^{ | 
 |         /* | 
 |          * This is not safe to call from the BQL/BH due to PVG-internal locks | 
 |          * causing deadlocks. | 
 |          */ | 
 |         bool r = [s->pgdisp encodeCurrentFrameToCommandBuffer:command_buffer | 
 |                                                  texture:texture | 
 |                                                   region:region]; | 
 |         if (!r) { | 
 |             [texture release]; | 
 |             [command_buffer release]; | 
 |             qemu_log_mask(LOG_GUEST_ERROR, | 
 |                           "%s: encodeCurrentFrameToCommandBuffer:texture:region: " | 
 |                           "failed\n", __func__); | 
 |             bql_lock(); | 
 |             --s->pending_frames; | 
 |             if (s->pending_frames > 0) { | 
 |                 apple_gfx_render_new_frame(s); | 
 |             } | 
 |             bql_unlock(); | 
 |             return; | 
 |         } | 
 |  | 
 |         if (managed_texture) { | 
 |             /* "Managed" textures exist in both VRAM and RAM and must be synced. */ | 
 |             id<MTLBlitCommandEncoder> blit = [command_buffer blitCommandEncoder]; | 
 |             [blit synchronizeResource:texture]; | 
 |             [blit endEncoding]; | 
 |         } | 
 |         [texture release]; | 
 |         [command_buffer addCompletedHandler: | 
 |             ^(id<MTLCommandBuffer> cb) | 
 |             { | 
 |                 aio_bh_schedule_oneshot(qemu_get_aio_context(), | 
 |                                         apple_gfx_render_frame_completed_bh, s); | 
 |             }]; | 
 |         [command_buffer commit]; | 
 |         [command_buffer release]; | 
 |     }); | 
 | } | 
 |  | 
 | static void copy_mtl_texture_to_surface_mem(id<MTLTexture> texture, void *vram) | 
 | { | 
 |     /* | 
 |      * TODO: Skip this entirely on a pure Metal or headless/guest-only | 
 |      * rendering path, else use a blit command encoder? Needs careful | 
 |      * (double?) buffering design. | 
 |      */ | 
 |     size_t width = texture.width, height = texture.height; | 
 |     MTLRegion region = MTLRegionMake2D(0, 0, width, height); | 
 |     [texture getBytes:vram | 
 |           bytesPerRow:(width * 4) | 
 |         bytesPerImage:(width * height * 4) | 
 |            fromRegion:region | 
 |           mipmapLevel:0 | 
 |                 slice:0]; | 
 | } | 
 |  | 
 | static void apple_gfx_render_frame_completed_bh(void *opaque) | 
 | { | 
 |     AppleGFXState *s = opaque; | 
 |  | 
 |     @autoreleasepool { | 
 |         --s->pending_frames; | 
 |         assert(s->pending_frames >= 0); | 
 |  | 
 |         /* Only update display if mode hasn't changed since we started rendering. */ | 
 |         if (s->rendering_frame_width == surface_width(s->surface) && | 
 |             s->rendering_frame_height == surface_height(s->surface)) { | 
 |             copy_mtl_texture_to_surface_mem(s->texture, surface_data(s->surface)); | 
 |             if (s->gfx_update_requested) { | 
 |                 s->gfx_update_requested = false; | 
 |                 dpy_gfx_update_full(s->con); | 
 |                 graphic_hw_update_done(s->con); | 
 |                 s->new_frame_ready = false; | 
 |             } else { | 
 |                 s->new_frame_ready = true; | 
 |             } | 
 |         } | 
 |         if (s->pending_frames > 0) { | 
 |             apple_gfx_render_new_frame(s); | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | static void apple_gfx_fb_update_display(void *opaque) | 
 | { | 
 |     AppleGFXState *s = opaque; | 
 |  | 
 |     assert(bql_locked()); | 
 |     if (s->new_frame_ready) { | 
 |         dpy_gfx_update_full(s->con); | 
 |         s->new_frame_ready = false; | 
 |         graphic_hw_update_done(s->con); | 
 |     } else if (s->pending_frames > 0) { | 
 |         s->gfx_update_requested = true; | 
 |     } else { | 
 |         graphic_hw_update_done(s->con); | 
 |     } | 
 | } | 
 |  | 
 | static const GraphicHwOps apple_gfx_fb_ops = { | 
 |     .gfx_update = apple_gfx_fb_update_display, | 
 |     .gfx_update_async = true, | 
 | }; | 
 |  | 
 | /* ------ Mouse cursor and display mode setting ------ */ | 
 |  | 
 | static void set_mode(AppleGFXState *s, uint32_t width, uint32_t height) | 
 | { | 
 |     MTLTextureDescriptor *textureDescriptor; | 
 |  | 
 |     if (s->surface && | 
 |         width == surface_width(s->surface) && | 
 |         height == surface_height(s->surface)) { | 
 |         return; | 
 |     } | 
 |  | 
 |     [s->texture release]; | 
 |  | 
 |     s->surface = qemu_create_displaysurface(width, height); | 
 |  | 
 |     @autoreleasepool { | 
 |         textureDescriptor = | 
 |             [MTLTextureDescriptor | 
 |                 texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm | 
 |                                              width:width | 
 |                                             height:height | 
 |                                          mipmapped:NO]; | 
 |         textureDescriptor.usage = s->pgdisp.minimumTextureUsage; | 
 |         s->texture = [s->mtl newTextureWithDescriptor:textureDescriptor]; | 
 |         s->using_managed_texture_storage = | 
 |             (s->texture.storageMode == MTLStorageModeManaged); | 
 |     } | 
 |  | 
 |     dpy_gfx_replace_surface(s->con, s->surface); | 
 | } | 
 |  | 
 | static void update_cursor(AppleGFXState *s) | 
 | { | 
 |     assert(bql_locked()); | 
 |     dpy_mouse_set(s->con, s->pgdisp.cursorPosition.x, | 
 |                   s->pgdisp.cursorPosition.y, qatomic_read(&s->cursor_show)); | 
 | } | 
 |  | 
 | static void update_cursor_bh(void *opaque) | 
 | { | 
 |     AppleGFXState *s = opaque; | 
 |     update_cursor(s); | 
 | } | 
 |  | 
 | typedef struct AppleGFXSetCursorGlyphJob { | 
 |     AppleGFXState *s; | 
 |     NSBitmapImageRep *glyph; | 
 |     PGDisplayCoord_t hotspot; | 
 | } AppleGFXSetCursorGlyphJob; | 
 |  | 
 | static void set_cursor_glyph(void *opaque) | 
 | { | 
 |     AppleGFXSetCursorGlyphJob *job = opaque; | 
 |     AppleGFXState *s = job->s; | 
 |     NSBitmapImageRep *glyph = job->glyph; | 
 |     uint32_t bpp = glyph.bitsPerPixel; | 
 |     size_t width = glyph.pixelsWide; | 
 |     size_t height = glyph.pixelsHigh; | 
 |     size_t padding_bytes_per_row = glyph.bytesPerRow - width * 4; | 
 |     const uint8_t* px_data = glyph.bitmapData; | 
 |  | 
 |     trace_apple_gfx_cursor_set(bpp, width, height); | 
 |  | 
 |     if (s->cursor) { | 
 |         cursor_unref(s->cursor); | 
 |         s->cursor = NULL; | 
 |     } | 
 |  | 
 |     if (bpp == 32) { /* Shouldn't be anything else, but just to be safe... */ | 
 |         s->cursor = cursor_alloc(width, height); | 
 |         s->cursor->hot_x = job->hotspot.x; | 
 |         s->cursor->hot_y = job->hotspot.y; | 
 |  | 
 |         uint32_t *dest_px = s->cursor->data; | 
 |  | 
 |         for (size_t y = 0; y < height; ++y) { | 
 |             for (size_t x = 0; x < width; ++x) { | 
 |                 /* | 
 |                  * NSBitmapImageRep's red & blue channels are swapped | 
 |                  * compared to QEMUCursor's. | 
 |                  */ | 
 |                 *dest_px = | 
 |                     (px_data[0] << 16u) | | 
 |                     (px_data[1] <<  8u) | | 
 |                     (px_data[2] <<  0u) | | 
 |                     (px_data[3] << 24u); | 
 |                 ++dest_px; | 
 |                 px_data += 4; | 
 |             } | 
 |             px_data += padding_bytes_per_row; | 
 |         } | 
 |         dpy_cursor_define(s->con, s->cursor); | 
 |         update_cursor(s); | 
 |     } | 
 |     [glyph release]; | 
 |  | 
 |     g_free(job); | 
 | } | 
 |  | 
 | /* ------ DMA (device reading system memory) ------ */ | 
 |  | 
 | typedef struct AppleGFXReadMemoryJob { | 
 |     QemuSemaphore sem; | 
 |     hwaddr physical_address; | 
 |     uint64_t length; | 
 |     void *dst; | 
 |     bool success; | 
 | } AppleGFXReadMemoryJob; | 
 |  | 
 | static void apple_gfx_do_read_memory(void *opaque) | 
 | { | 
 |     AppleGFXReadMemoryJob *job = opaque; | 
 |     MemTxResult r; | 
 |  | 
 |     r = dma_memory_read(&address_space_memory, job->physical_address, | 
 |                         job->dst, job->length, MEMTXATTRS_UNSPECIFIED); | 
 |     job->success = (r == MEMTX_OK); | 
 |  | 
 |     qemu_sem_post(&job->sem); | 
 | } | 
 |  | 
 | static bool apple_gfx_read_memory(AppleGFXState *s, hwaddr physical_address, | 
 |                                   uint64_t length, void *dst) | 
 | { | 
 |     AppleGFXReadMemoryJob job = { | 
 |         .physical_address = physical_address, .length = length, .dst = dst | 
 |     }; | 
 |  | 
 |     trace_apple_gfx_read_memory(physical_address, length, dst); | 
 |  | 
 |     /* Performing DMA requires BQL, so do it in a BH. */ | 
 |     qemu_sem_init(&job.sem, 0); | 
 |     aio_bh_schedule_oneshot(qemu_get_aio_context(), | 
 |                             apple_gfx_do_read_memory, &job); | 
 |     qemu_sem_wait(&job.sem); | 
 |     qemu_sem_destroy(&job.sem); | 
 |     return job.success; | 
 | } | 
 |  | 
 | /* ------ Memory-mapped device I/O operations ------ */ | 
 |  | 
 | typedef struct AppleGFXIOJob { | 
 |     AppleGFXState *state; | 
 |     uint64_t offset; | 
 |     uint64_t value; | 
 |     bool completed; | 
 | } AppleGFXIOJob; | 
 |  | 
 | static void apple_gfx_do_read(void *opaque) | 
 | { | 
 |     AppleGFXIOJob *job = opaque; | 
 |     job->value = [job->state->pgdev mmioReadAtOffset:job->offset]; | 
 |     qatomic_set(&job->completed, true); | 
 |     aio_wait_kick(); | 
 | } | 
 |  | 
 | static uint64_t apple_gfx_read(void *opaque, hwaddr offset, unsigned size) | 
 | { | 
 |     AppleGFXIOJob job = { | 
 |         .state = opaque, | 
 |         .offset = offset, | 
 |         .completed = false, | 
 |     }; | 
 |     dispatch_queue_t queue = get_background_queue(); | 
 |  | 
 |     dispatch_async_f(queue, &job, apple_gfx_do_read); | 
 |     AIO_WAIT_WHILE(NULL, !qatomic_read(&job.completed)); | 
 |  | 
 |     trace_apple_gfx_read(offset, job.value); | 
 |     return job.value; | 
 | } | 
 |  | 
 | static void apple_gfx_do_write(void *opaque) | 
 | { | 
 |     AppleGFXIOJob *job = opaque; | 
 |     [job->state->pgdev mmioWriteAtOffset:job->offset value:job->value]; | 
 |     qatomic_set(&job->completed, true); | 
 |     aio_wait_kick(); | 
 | } | 
 |  | 
 | static void apple_gfx_write(void *opaque, hwaddr offset, uint64_t val, | 
 |                             unsigned size) | 
 | { | 
 |     /* | 
 |      * The methods mmioReadAtOffset: and especially mmioWriteAtOffset: can | 
 |      * trigger synchronous operations on other dispatch queues, which in turn | 
 |      * may call back out on one or more of the callback blocks. For this reason, | 
 |      * and as we are holding the BQL, we invoke the I/O methods on a pool | 
 |      * thread and handle AIO tasks while we wait. Any work in the callbacks | 
 |      * requiring the BQL will in turn schedule BHs which this thread will | 
 |      * process while waiting. | 
 |      */ | 
 |     AppleGFXIOJob job = { | 
 |         .state = opaque, | 
 |         .offset = offset, | 
 |         .value = val, | 
 |         .completed = false, | 
 |     }; | 
 |     dispatch_queue_t queue = get_background_queue(); | 
 |  | 
 |     dispatch_async_f(queue, &job, apple_gfx_do_write); | 
 |     AIO_WAIT_WHILE(NULL, !qatomic_read(&job.completed)); | 
 |  | 
 |     trace_apple_gfx_write(offset, val); | 
 | } | 
 |  | 
 | static const MemoryRegionOps apple_gfx_ops = { | 
 |     .read = apple_gfx_read, | 
 |     .write = apple_gfx_write, | 
 |     .endianness = DEVICE_LITTLE_ENDIAN, | 
 |     .valid = { | 
 |         .min_access_size = 4, | 
 |         .max_access_size = 8, | 
 |     }, | 
 |     .impl = { | 
 |         .min_access_size = 4, | 
 |         .max_access_size = 4, | 
 |     }, | 
 | }; | 
 |  | 
 | static size_t apple_gfx_get_default_mmio_range_size(void) | 
 | { | 
 |     size_t mmio_range_size; | 
 |     @autoreleasepool { | 
 |         PGDeviceDescriptor *desc = [PGDeviceDescriptor new]; | 
 |         mmio_range_size = desc.mmioLength; | 
 |         [desc release]; | 
 |     } | 
 |     return mmio_range_size; | 
 | } | 
 |  | 
 | /* ------ Initialisation and startup ------ */ | 
 |  | 
 | void apple_gfx_common_init(Object *obj, AppleGFXState *s, const char* obj_name) | 
 | { | 
 |     size_t mmio_range_size = apple_gfx_get_default_mmio_range_size(); | 
 |  | 
 |     trace_apple_gfx_common_init(obj_name, mmio_range_size); | 
 |     memory_region_init_io(&s->iomem_gfx, obj, &apple_gfx_ops, s, obj_name, | 
 |                           mmio_range_size); | 
 |  | 
 |     /* TODO: PVG framework supports serialising device state: integrate it! */ | 
 | } | 
 |  | 
 | static void apple_gfx_register_task_mapping_handlers(AppleGFXState *s, | 
 |                                                      PGDeviceDescriptor *desc) | 
 | { | 
 |     desc.createTask = ^(uint64_t vmSize, void * _Nullable * _Nonnull baseAddress) { | 
 |         PGTask_t *task = apple_gfx_new_task(s, vmSize); | 
 |         *baseAddress = (void *)task->address; | 
 |         trace_apple_gfx_create_task(vmSize, *baseAddress); | 
 |         return task; | 
 |     }; | 
 |  | 
 |     desc.destroyTask = ^(PGTask_t * _Nonnull task) { | 
 |         trace_apple_gfx_destroy_task(task, task->mapped_regions->len); | 
 |  | 
 |         apple_gfx_destroy_task(s, task); | 
 |     }; | 
 |  | 
 |     desc.mapMemory = ^bool(PGTask_t * _Nonnull task, uint32_t range_count, | 
 |                            uint64_t virtual_offset, bool read_only, | 
 |                            PGPhysicalMemoryRange_t * _Nonnull ranges) { | 
 |         return apple_gfx_task_map_memory(s, task, virtual_offset, | 
 |                                          ranges, range_count, read_only); | 
 |     }; | 
 |  | 
 |     desc.unmapMemory = ^bool(PGTask_t * _Nonnull task, uint64_t virtual_offset, | 
 |                              uint64_t length) { | 
 |         apple_gfx_task_unmap_memory(s, task, virtual_offset, length); | 
 |         return true; | 
 |     }; | 
 |  | 
 |     desc.readMemory = ^bool(uint64_t physical_address, uint64_t length, | 
 |                             void * _Nonnull dst) { | 
 |         return apple_gfx_read_memory(s, physical_address, length, dst); | 
 |     }; | 
 | } | 
 |  | 
 | static void new_frame_handler_bh(void *opaque) | 
 | { | 
 |     AppleGFXState *s = opaque; | 
 |  | 
 |     /* Drop frames if guest gets too far ahead. */ | 
 |     if (s->pending_frames >= 2) { | 
 |         return; | 
 |     } | 
 |     ++s->pending_frames; | 
 |     if (s->pending_frames > 1) { | 
 |         return; | 
 |     } | 
 |  | 
 |     @autoreleasepool { | 
 |         apple_gfx_render_new_frame(s); | 
 |     } | 
 | } | 
 |  | 
 | static PGDisplayDescriptor *apple_gfx_prepare_display_descriptor(AppleGFXState *s) | 
 | { | 
 |     PGDisplayDescriptor *disp_desc = [PGDisplayDescriptor new]; | 
 |  | 
 |     disp_desc.name = @"QEMU display"; | 
 |     disp_desc.sizeInMillimeters = NSMakeSize(400., 300.); /* A 20" display */ | 
 |     disp_desc.queue = dispatch_get_main_queue(); | 
 |     disp_desc.newFrameEventHandler = ^(void) { | 
 |         trace_apple_gfx_new_frame(); | 
 |         aio_bh_schedule_oneshot(qemu_get_aio_context(), new_frame_handler_bh, s); | 
 |     }; | 
 |     disp_desc.modeChangeHandler = ^(PGDisplayCoord_t sizeInPixels, | 
 |                                     OSType pixelFormat) { | 
 |         trace_apple_gfx_mode_change(sizeInPixels.x, sizeInPixels.y); | 
 |  | 
 |         BQL_LOCK_GUARD(); | 
 |         set_mode(s, sizeInPixels.x, sizeInPixels.y); | 
 |     }; | 
 |     disp_desc.cursorGlyphHandler = ^(NSBitmapImageRep *glyph, | 
 |                                      PGDisplayCoord_t hotspot) { | 
 |         AppleGFXSetCursorGlyphJob *job = g_malloc0(sizeof(*job)); | 
 |         job->s = s; | 
 |         job->glyph = glyph; | 
 |         job->hotspot = hotspot; | 
 |         [glyph retain]; | 
 |         aio_bh_schedule_oneshot(qemu_get_aio_context(), | 
 |                                 set_cursor_glyph, job); | 
 |     }; | 
 |     disp_desc.cursorShowHandler = ^(BOOL show) { | 
 |         trace_apple_gfx_cursor_show(show); | 
 |         qatomic_set(&s->cursor_show, show); | 
 |         aio_bh_schedule_oneshot(qemu_get_aio_context(), | 
 |                                 update_cursor_bh, s); | 
 |     }; | 
 |     disp_desc.cursorMoveHandler = ^(void) { | 
 |         trace_apple_gfx_cursor_move(); | 
 |         aio_bh_schedule_oneshot(qemu_get_aio_context(), | 
 |                                 update_cursor_bh, s); | 
 |     }; | 
 |  | 
 |     return disp_desc; | 
 | } | 
 |  | 
 | static NSArray<PGDisplayMode *> *apple_gfx_create_display_mode_array( | 
 |     const AppleGFXDisplayMode display_modes[], uint32_t display_mode_count) | 
 | { | 
 |     PGDisplayMode *mode_obj; | 
 |     NSMutableArray<PGDisplayMode *> *mode_array = | 
 |         [[NSMutableArray alloc] initWithCapacity:display_mode_count]; | 
 |  | 
 |     for (unsigned i = 0; i < display_mode_count; i++) { | 
 |         const AppleGFXDisplayMode *mode = &display_modes[i]; | 
 |         trace_apple_gfx_display_mode(i, mode->width_px, mode->height_px); | 
 |         PGDisplayCoord_t mode_size = { mode->width_px, mode->height_px }; | 
 |  | 
 |         mode_obj = | 
 |             [[PGDisplayMode alloc] initWithSizeInPixels:mode_size | 
 |                                         refreshRateInHz:mode->refresh_rate_hz]; | 
 |         [mode_array addObject:mode_obj]; | 
 |         [mode_obj release]; | 
 |     } | 
 |  | 
 |     return mode_array; | 
 | } | 
 |  | 
 | static id<MTLDevice> copy_suitable_metal_device(void) | 
 | { | 
 |     id<MTLDevice> dev = nil; | 
 |     NSArray<id<MTLDevice>> *devs = MTLCopyAllDevices(); | 
 |  | 
 |     /* Prefer a unified memory GPU. Failing that, pick a non-removable GPU. */ | 
 |     for (size_t i = 0; i < devs.count; ++i) { | 
 |         if (devs[i].hasUnifiedMemory) { | 
 |             dev = devs[i]; | 
 |             break; | 
 |         } | 
 |         if (!devs[i].removable) { | 
 |             dev = devs[i]; | 
 |         } | 
 |     } | 
 |  | 
 |     if (dev != nil) { | 
 |         [dev retain]; | 
 |     } else { | 
 |         dev = MTLCreateSystemDefaultDevice(); | 
 |     } | 
 |     [devs release]; | 
 |  | 
 |     return dev; | 
 | } | 
 |  | 
 | bool apple_gfx_common_realize(AppleGFXState *s, DeviceState *dev, | 
 |                               PGDeviceDescriptor *desc, Error **errp) | 
 | { | 
 |     PGDisplayDescriptor *disp_desc; | 
 |     const AppleGFXDisplayMode *display_modes = apple_gfx_default_modes; | 
 |     uint32_t num_display_modes = ARRAY_SIZE(apple_gfx_default_modes); | 
 |     NSArray<PGDisplayMode *> *mode_array; | 
 |  | 
 |     if (apple_gfx_mig_blocker == NULL) { | 
 |         error_setg(&apple_gfx_mig_blocker, | 
 |                   "Migration state blocked by apple-gfx display device"); | 
 |         if (migrate_add_blocker(&apple_gfx_mig_blocker, errp) < 0) { | 
 |             return false; | 
 |         } | 
 |     } | 
 |  | 
 |     qemu_mutex_init(&s->task_mutex); | 
 |     QTAILQ_INIT(&s->tasks); | 
 |     s->mtl = copy_suitable_metal_device(); | 
 |     s->mtl_queue = [s->mtl newCommandQueue]; | 
 |  | 
 |     desc.device = s->mtl; | 
 |  | 
 |     apple_gfx_register_task_mapping_handlers(s, desc); | 
 |  | 
 |     s->cursor_show = true; | 
 |  | 
 |     s->pgdev = PGNewDeviceWithDescriptor(desc); | 
 |  | 
 |     disp_desc = apple_gfx_prepare_display_descriptor(s); | 
 |     /* | 
 |      * Although the framework does, this integration currently does not support | 
 |      * multiple virtual displays connected to a single PV graphics device. | 
 |      * It is however possible to create | 
 |      * more than one instance of the device, each with one display. The macOS | 
 |      * guest will ignore these displays if they share the same serial number, | 
 |      * so ensure each instance gets a unique one. | 
 |      */ | 
 |     s->pgdisp = [s->pgdev newDisplayWithDescriptor:disp_desc | 
 |                                               port:0 | 
 |                                          serialNum:next_pgdisplay_serial_num++]; | 
 |     [disp_desc release]; | 
 |  | 
 |     if (s->display_modes != NULL && s->num_display_modes > 0) { | 
 |         trace_apple_gfx_common_realize_modes_property(s->num_display_modes); | 
 |         display_modes = s->display_modes; | 
 |         num_display_modes = s->num_display_modes; | 
 |     } | 
 |     s->pgdisp.modeList = mode_array = | 
 |         apple_gfx_create_display_mode_array(display_modes, num_display_modes); | 
 |     [mode_array release]; | 
 |  | 
 |     s->con = graphic_console_init(dev, 0, &apple_gfx_fb_ops, s); | 
 |     return true; | 
 | } | 
 |  | 
 | /* ------ Display mode list device property ------ */ | 
 |  | 
 | static void apple_gfx_get_display_mode(Object *obj, Visitor *v, | 
 |                                        const char *name, void *opaque, | 
 |                                        Error **errp) | 
 | { | 
 |     Property *prop = opaque; | 
 |     AppleGFXDisplayMode *mode = object_field_prop_ptr(obj, prop); | 
 |     /* 3 uint16s (max 5 digits) + 2 separator characters + nul. */ | 
 |     char buffer[5 * 3 + 2 + 1]; | 
 |     char *pos = buffer; | 
 |  | 
 |     int rc = snprintf(buffer, sizeof(buffer), | 
 |                       "%"PRIu16"x%"PRIu16"@%"PRIu16, | 
 |                       mode->width_px, mode->height_px, | 
 |                       mode->refresh_rate_hz); | 
 |     assert(rc < sizeof(buffer)); | 
 |  | 
 |     visit_type_str(v, name, &pos, errp); | 
 | } | 
 |  | 
 | static void apple_gfx_set_display_mode(Object *obj, Visitor *v, | 
 |                                        const char *name, void *opaque, | 
 |                                        Error **errp) | 
 | { | 
 |     Property *prop = opaque; | 
 |     AppleGFXDisplayMode *mode = object_field_prop_ptr(obj, prop); | 
 |     const char *endptr; | 
 |     g_autofree char *str = NULL; | 
 |     int ret; | 
 |     int val; | 
 |  | 
 |     if (!visit_type_str(v, name, &str, errp)) { | 
 |         return; | 
 |     } | 
 |  | 
 |     endptr = str; | 
 |  | 
 |     ret = qemu_strtoi(endptr, &endptr, 10, &val); | 
 |     if (ret || val > UINT16_MAX || val <= 0) { | 
 |         error_setg(errp, "width in '%s' must be a decimal integer number" | 
 |                          " of pixels in the range 1..65535", name); | 
 |         return; | 
 |     } | 
 |     mode->width_px = val; | 
 |     if (*endptr != 'x') { | 
 |         goto separator_error; | 
 |     } | 
 |  | 
 |     ret = qemu_strtoi(endptr + 1, &endptr, 10, &val); | 
 |     if (ret || val > UINT16_MAX || val <= 0) { | 
 |         error_setg(errp, "height in '%s' must be a decimal integer number" | 
 |                          " of pixels in the range 1..65535", name); | 
 |         return; | 
 |     } | 
 |     mode->height_px = val; | 
 |     if (*endptr != '@') { | 
 |         goto separator_error; | 
 |     } | 
 |  | 
 |     ret = qemu_strtoi(endptr + 1, &endptr, 10, &val); | 
 |     if (ret || val > UINT16_MAX || val <= 0) { | 
 |         error_setg(errp, "refresh rate in '%s'" | 
 |                          " must be a positive decimal integer (Hertz)", name); | 
 |         return; | 
 |     } | 
 |     mode->refresh_rate_hz = val; | 
 |     return; | 
 |  | 
 | separator_error: | 
 |     error_setg(errp, | 
 |                "Each display mode takes the format '<width>x<height>@<rate>'"); | 
 | } | 
 |  | 
 | const PropertyInfo qdev_prop_apple_gfx_display_mode = { | 
 |     .name  = "display_mode", | 
 |     .description = | 
 |         "Display mode in pixels and Hertz, as <width>x<height>@<refresh-rate> " | 
 |         "Example: 3840x2160@60", | 
 |     .get   = apple_gfx_get_display_mode, | 
 |     .set   = apple_gfx_set_display_mode, | 
 | }; |