This change adds a property 'display_modes' on the graphics device
which permits specifying a list of display modes. (screen resolution
and refresh rate)
The property is an array of a custom type to make the syntax slightly
less awkward to use, for example:
-device '{"driver":"apple-gfx-pci", "display-modes":["1920x1080@60", "3840x2160@60"]}'
Signed-off-by: Phil Dennis-Jordan <phil@philjordan.eu>
Reviewed-by: Akihiko Odaki <akihiko.odaki@daynix.com>
Tested-by: Akihiko Odaki <akihiko.odaki@daynix.com>
Message-ID: <20241223221645.29911-5-phil@philjordan.eu>
Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
		
	
			
		
			
				
	
	
		
			880 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Objective-C
		
	
	
	
	
	
			
		
		
	
	
			880 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Objective-C
		
	
	
	
	
	
/*
 | 
						|
 * 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,
 | 
						|
};
 |