|  | /* | 
|  | * 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, | 
|  | }; |