|
@@ -0,0 +1,783 @@
|
|
|
|
+/*
|
|
|
|
+ * 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 PGDisplayCoord_t apple_gfx_modes[] = {
|
|
|
|
+ { .x = 1440, .y = 1080 },
|
|
|
|
+ { .x = 1280, .y = 1024 },
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+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_prepare_display_mode_array(void)
|
|
|
|
+{
|
|
|
|
+ PGDisplayMode *modes[ARRAY_SIZE(apple_gfx_modes)];
|
|
|
|
+ NSArray<PGDisplayMode*>* mode_array;
|
|
|
|
+ int i;
|
|
|
|
+
|
|
|
|
+ for (i = 0; i < ARRAY_SIZE(apple_gfx_modes); i++) {
|
|
|
|
+ modes[i] =
|
|
|
|
+ [[PGDisplayMode alloc] initWithSizeInPixels:apple_gfx_modes[i] refreshRateInHz:60.];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ mode_array = [NSArray arrayWithObjects:modes count:ARRAY_SIZE(apple_gfx_modes)];
|
|
|
|
+
|
|
|
|
+ for (i = 0; i < ARRAY_SIZE(apple_gfx_modes); i++) {
|
|
|
|
+ [modes[i] release];
|
|
|
|
+ modes[i] = nil;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ 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;
|
|
|
|
+
|
|
|
|
+ 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];
|
|
|
|
+ s->pgdisp.modeList = apple_gfx_prepare_display_mode_array();
|
|
|
|
+
|
|
|
|
+ s->con = graphic_console_init(dev, 0, &apple_gfx_fb_ops, s);
|
|
|
|
+ return true;
|
|
|
|
+}
|