| 1 | /* |
| 2 | * QEMU Apple ParavirtualizedGraphics.framework device |
| 3 | * |
| 4 | * Copyright © 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 5 | * |
| 6 | * SPDX-License-Identifier: GPL-2.0-or-later |
| 7 | * |
| 8 | * ParavirtualizedGraphics.framework is a set of libraries that macOS provides |
| 9 | * which implements 3d graphics passthrough to the host as well as a |
| 10 | * proprietary guest communication channel to drive it. This device model |
| 11 | * implements support to drive that library from within QEMU. |
| 12 | */ |
| 13 | |
| 14 | #include "qemu/osdep.h" |
| 15 | #include "qemu/lockable.h" |
| 16 | #include "qemu/cutils.h" |
| 17 | #include "qemu/log.h" |
| 18 | #include "qapi/visitor.h" |
| 19 | #include "qapi/error.h" |
| 20 | #include "qemu/aio-wait.h" |
| 21 | #include "system/address-spaces.h" |
| 22 | #include "system/dma.h" |
| 23 | #include "migration/blocker.h" |
| 24 | #include "ui/console.h" |
| 25 | #include "apple-gfx.h" |
| 26 | #include "trace.h" |
| 27 | |
| 28 | #include <mach/mach.h> |
| 29 | #include <mach/mach_vm.h> |
| 30 | #include <dispatch/dispatch.h> |
| 31 | |
| 32 | #import <ParavirtualizedGraphics/ParavirtualizedGraphics.h> |
| 33 | |
| 34 | static const AppleGFXDisplayMode apple_gfx_default_modes[] = { |
| 35 | { 1920, 1080, 60 }, |
| 36 | { 1440, 1080, 60 }, |
| 37 | { 1280, 1024, 60 }, |
| 38 | }; |
| 39 | |
| 40 | static Error *apple_gfx_mig_blocker; |
| 41 | static uint32_t next_pgdisplay_serial_num = 1; |
| 42 | |
| 43 | static dispatch_queue_t get_background_queue(void) |
| 44 | { |
| 45 | return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); |
| 46 | } |
| 47 | |
| 48 | /* ------ PGTask and task operations: new/destroy/map/unmap ------ */ |
| 49 | |
| 50 | /* |
| 51 | * This implements the type declared in <ParavirtualizedGraphics/PGDevice.h> |
| 52 | * which is opaque from the framework's point of view. It is used in callbacks |
| 53 | * in the form of its typedef PGTask_t, which also already exists in the |
| 54 | * framework headers. |
| 55 | * |
| 56 | * A "task" in PVG terminology represents a host-virtual contiguous address |
| 57 | * range which is reserved in a large chunk on task creation. The mapMemory |
| 58 | * callback then requests ranges of guest system memory (identified by their |
| 59 | * GPA) to be mapped into subranges of this reserved address space. |
| 60 | * This type of operation isn't well-supported by QEMU's memory subsystem, |
| 61 | * but it is fortunately trivial to achieve with Darwin's mach_vm_remap() call, |
| 62 | * which allows us to refer to the same backing memory via multiple virtual |
| 63 | * address ranges. The Mach VM APIs are therefore used throughout for managing |
| 64 | * task memory. |
| 65 | */ |
| 66 | struct PGTask_s { |
| 67 | QTAILQ_ENTRY(PGTask_s) node; |
| 68 | AppleGFXState *s; |
| 69 | mach_vm_address_t address; |
| 70 | uint64_t len; |
| 71 | /* |
| 72 | * All unique MemoryRegions for which a mapping has been created in this |
| 73 | * task, and on which we have thus called memory_region_ref(). There are |
| 74 | * usually very few regions of system RAM in total, so we expect this array |
| 75 | * to be very short. Therefore, no need for sorting or fancy search |
| 76 | * algorithms, linear search will do. |
| 77 | * Protected by AppleGFXState's task_mutex. |
| 78 | */ |
| 79 | GPtrArray *mapped_regions; |
| 80 | }; |
| 81 | |
| 82 | static PGTask_t *apple_gfx_new_task(AppleGFXState *s, uint64_t len) |
| 83 | { |
| 84 | mach_vm_address_t task_mem; |
| 85 | PGTask_t *task; |
| 86 | kern_return_t r; |
| 87 | |
| 88 | r = mach_vm_allocate(mach_task_self(), &task_mem, len, VM_FLAGS_ANYWHERE); |
| 89 | if (r != KERN_SUCCESS) { |
| 90 | return NULL; |
| 91 | } |
| 92 | |
| 93 | task = g_new0(PGTask_t, 1); |
| 94 | task->s = s; |
| 95 | task->address = task_mem; |
| 96 | task->len = len; |
| 97 | task->mapped_regions = g_ptr_array_sized_new(2 /* Usually enough */); |
| 98 | |
| 99 | QEMU_LOCK_GUARD(&s->task_mutex); |
| 100 | QTAILQ_INSERT_TAIL(&s->tasks, task, node); |
| 101 | |
| 102 | return task; |
| 103 | } |
| 104 | |
| 105 | static void apple_gfx_destroy_task(AppleGFXState *s, PGTask_t *task) |
| 106 | { |
| 107 | GPtrArray *regions = task->mapped_regions; |
| 108 | MemoryRegion *region; |
| 109 | size_t i; |
| 110 | |
| 111 | for (i = 0; i < regions->len; ++i) { |
| 112 | region = g_ptr_array_index(regions, i); |
| 113 | memory_region_unref(region); |
| 114 | } |
| 115 | g_ptr_array_unref(regions); |
| 116 | |
| 117 | mach_vm_deallocate(mach_task_self(), task->address, task->len); |
| 118 | |
| 119 | QEMU_LOCK_GUARD(&s->task_mutex); |
| 120 | QTAILQ_REMOVE(&s->tasks, task, node); |
| 121 | g_free(task); |
| 122 | } |
| 123 | |
| 124 | void *apple_gfx_host_ptr_for_gpa_range(uint64_t guest_physical, |
| 125 | uint64_t length, bool read_only, |
| 126 | MemoryRegion **mapping_in_region) |
| 127 | { |
| 128 | MemoryRegion *ram_region; |
| 129 | char *host_ptr; |
| 130 | hwaddr ram_region_offset = 0; |
| 131 | hwaddr ram_region_length = length; |
| 132 | |
| 133 | ram_region = address_space_translate(&address_space_memory, |
| 134 | guest_physical, |
| 135 | &ram_region_offset, |
| 136 | &ram_region_length, !read_only, |
| 137 | MEMTXATTRS_UNSPECIFIED); |
| 138 | |
| 139 | if (!ram_region || ram_region_length < length || |
| 140 | !memory_access_is_direct(ram_region, !read_only, |
| 141 | MEMTXATTRS_UNSPECIFIED)) { |
| 142 | return NULL; |
| 143 | } |
| 144 | |
| 145 | host_ptr = memory_region_get_ram_ptr(ram_region); |
| 146 | if (!host_ptr) { |
| 147 | return NULL; |
| 148 | } |
| 149 | host_ptr += ram_region_offset; |
| 150 | *mapping_in_region = ram_region; |
| 151 | return host_ptr; |
| 152 | } |
| 153 | |
| 154 | static bool apple_gfx_task_map_memory(AppleGFXState *s, PGTask_t *task, |
| 155 | uint64_t virtual_offset, |
| 156 | PGPhysicalMemoryRange_t *ranges, |
| 157 | uint32_t range_count, bool read_only) |
| 158 | { |
| 159 | kern_return_t r; |
| 160 | void *source_ptr; |
| 161 | mach_vm_address_t target; |
| 162 | vm_prot_t cur_protection, max_protection; |
| 163 | bool success = true; |
| 164 | MemoryRegion *region; |
| 165 | |
| 166 | RCU_READ_LOCK_GUARD(); |
| 167 | QEMU_LOCK_GUARD(&s->task_mutex); |
| 168 | |
| 169 | trace_apple_gfx_map_memory(task, range_count, virtual_offset, read_only); |
| 170 | for (int i = 0; i < range_count; i++) { |
| 171 | PGPhysicalMemoryRange_t *range = &ranges[i]; |
| 172 | |
| 173 | target = task->address + virtual_offset; |
| 174 | virtual_offset += range->physicalLength; |
| 175 | |
| 176 | trace_apple_gfx_map_memory_range(i, range->physicalAddress, |
| 177 | range->physicalLength); |
| 178 | |
| 179 | region = NULL; |
| 180 | source_ptr = apple_gfx_host_ptr_for_gpa_range(range->physicalAddress, |
| 181 | range->physicalLength, |
| 182 | read_only, ®ion); |
| 183 | if (!source_ptr) { |
| 184 | success = false; |
| 185 | continue; |
| 186 | } |
| 187 | |
| 188 | if (!g_ptr_array_find(task->mapped_regions, region, NULL)) { |
| 189 | g_ptr_array_add(task->mapped_regions, region); |
| 190 | memory_region_ref(region); |
| 191 | } |
| 192 | |
| 193 | cur_protection = 0; |
| 194 | max_protection = 0; |
| 195 | /* Map guest RAM at range->physicalAddress into PG task memory range */ |
| 196 | r = mach_vm_remap(mach_task_self(), |
| 197 | &target, range->physicalLength, vm_page_size - 1, |
| 198 | VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, |
| 199 | mach_task_self(), (mach_vm_address_t)source_ptr, |
| 200 | false /* shared mapping, no copy */, |
| 201 | &cur_protection, &max_protection, |
| 202 | VM_INHERIT_COPY); |
| 203 | trace_apple_gfx_remap(r, source_ptr, target); |
| 204 | g_assert(r == KERN_SUCCESS); |
| 205 | } |
| 206 | |
| 207 | return success; |
| 208 | } |
| 209 | |
| 210 | static void apple_gfx_task_unmap_memory(AppleGFXState *s, PGTask_t *task, |
| 211 | uint64_t virtual_offset, uint64_t length) |
| 212 | { |
| 213 | kern_return_t r; |
| 214 | mach_vm_address_t range_address; |
| 215 | |
| 216 | trace_apple_gfx_unmap_memory(task, virtual_offset, length); |
| 217 | |
| 218 | /* |
| 219 | * Replace task memory range with fresh 0 pages, undoing the mapping |
| 220 | * from guest RAM. |
| 221 | */ |
| 222 | range_address = task->address + virtual_offset; |
| 223 | r = mach_vm_allocate(mach_task_self(), &range_address, length, |
| 224 | VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE); |
| 225 | g_assert(r == KERN_SUCCESS); |
| 226 | } |
| 227 | |
| 228 | /* ------ Rendering and frame management ------ */ |
| 229 | |
| 230 | static void apple_gfx_render_frame_completed_bh(void *opaque); |
| 231 | |
| 232 | static void apple_gfx_render_new_frame(AppleGFXState *s) |
| 233 | { |
| 234 | bool managed_texture = s->using_managed_texture_storage; |
| 235 | uint32_t width = surface_width(s->surface); |
| 236 | uint32_t height = surface_height(s->surface); |
| 237 | MTLRegion region = MTLRegionMake2D(0, 0, width, height); |
| 238 | id<MTLCommandBuffer> command_buffer = [s->mtl_queue commandBuffer]; |
| 239 | id<MTLTexture> texture = s->texture; |
| 240 | |
| 241 | assert(bql_locked()); |
| 242 | [texture retain]; |
| 243 | [command_buffer retain]; |
| 244 | |
| 245 | s->rendering_frame_width = width; |
| 246 | s->rendering_frame_height = height; |
| 247 | |
| 248 | dispatch_async(get_background_queue(), ^{ |
| 249 | /* |
| 250 | * This is not safe to call from the BQL/BH due to PVG-internal locks |
| 251 | * causing deadlocks. |
| 252 | */ |
| 253 | bool r = [s->pgdisp encodeCurrentFrameToCommandBuffer:command_buffer |
| 254 | texture:texture |
| 255 | region:region]; |
| 256 | if (!r) { |
| 257 | [texture release]; |
| 258 | [command_buffer release]; |
| 259 | qemu_log_mask(LOG_GUEST_ERROR, |
| 260 | "%s: encodeCurrentFrameToCommandBuffer:texture:region: " |
| 261 | "failed\n", __func__); |
| 262 | bql_lock(); |
| 263 | --s->pending_frames; |
| 264 | if (s->pending_frames > 0) { |
| 265 | apple_gfx_render_new_frame(s); |
| 266 | } |
| 267 | bql_unlock(); |
| 268 | return; |
| 269 | } |
| 270 | |
| 271 | if (managed_texture) { |
| 272 | /* "Managed" textures exist in both VRAM and RAM and must be synced. */ |
| 273 | id<MTLBlitCommandEncoder> blit = [command_buffer blitCommandEncoder]; |
| 274 | [blit synchronizeResource:texture]; |
| 275 | [blit endEncoding]; |
| 276 | } |
| 277 | [texture release]; |
| 278 | [command_buffer addCompletedHandler: |
| 279 | ^(id<MTLCommandBuffer> cb) |
| 280 | { |
| 281 | aio_bh_schedule_oneshot(qemu_get_aio_context(), |
| 282 | apple_gfx_render_frame_completed_bh, s); |
| 283 | }]; |
| 284 | [command_buffer commit]; |
| 285 | [command_buffer release]; |
| 286 | }); |
| 287 | } |
| 288 | |
| 289 | static void copy_mtl_texture_to_surface_mem(id<MTLTexture> texture, void *vram) |
| 290 | { |
| 291 | /* |
| 292 | * TODO: Skip this entirely on a pure Metal or headless/guest-only |
| 293 | * rendering path, else use a blit command encoder? Needs careful |
| 294 | * (double?) buffering design. |
| 295 | */ |
| 296 | size_t width = texture.width, height = texture.height; |
| 297 | MTLRegion region = MTLRegionMake2D(0, 0, width, height); |
| 298 | [texture getBytes:vram |
| 299 | bytesPerRow:(width * 4) |
| 300 | bytesPerImage:(width * height * 4) |
| 301 | fromRegion:region |
| 302 | mipmapLevel:0 |
| 303 | slice:0]; |
| 304 | } |
| 305 | |
| 306 | static void apple_gfx_render_frame_completed_bh(void *opaque) |
| 307 | { |
| 308 | AppleGFXState *s = opaque; |
| 309 | |
| 310 | @autoreleasepool { |
| 311 | --s->pending_frames; |
| 312 | assert(s->pending_frames >= 0); |
| 313 | |
| 314 | /* Only update display if mode hasn't changed since we started rendering. */ |
| 315 | if (s->rendering_frame_width == surface_width(s->surface) && |
| 316 | s->rendering_frame_height == surface_height(s->surface)) { |
| 317 | copy_mtl_texture_to_surface_mem(s->texture, surface_data(s->surface)); |
| 318 | if (s->gfx_update_requested) { |
| 319 | s->gfx_update_requested = false; |
| 320 | qemu_console_update_full(s->con); |
| 321 | qemu_console_hw_update_done(s->con); |
| 322 | s->new_frame_ready = false; |
| 323 | } else { |
| 324 | s->new_frame_ready = true; |
| 325 | } |
| 326 | } |
| 327 | if (s->pending_frames > 0) { |
| 328 | apple_gfx_render_new_frame(s); |
| 329 | } |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | static bool apple_gfx_fb_update_display(void *opaque) |
| 334 | { |
| 335 | AppleGFXState *s = opaque; |
| 336 | bool done = true; |
| 337 | |
| 338 | assert(bql_locked()); |
| 339 | if (s->new_frame_ready) { |
| 340 | qemu_console_update_full(s->con); |
| 341 | s->new_frame_ready = false; |
| 342 | } else if (s->pending_frames > 0) { |
| 343 | s->gfx_update_requested = true; |
| 344 | done = false; |
| 345 | } |
| 346 | |
| 347 | return done; |
| 348 | } |
| 349 | |
| 350 | static const GraphicHwOps apple_gfx_fb_ops = { |
| 351 | .gfx_update = apple_gfx_fb_update_display, |
| 352 | }; |
| 353 | |
| 354 | /* ------ Mouse cursor and display mode setting ------ */ |
| 355 | |
| 356 | static void set_mode(AppleGFXState *s, uint32_t width, uint32_t height) |
| 357 | { |
| 358 | MTLTextureDescriptor *textureDescriptor; |
| 359 | |
| 360 | if (s->surface && |
| 361 | width == surface_width(s->surface) && |
| 362 | height == surface_height(s->surface)) { |
| 363 | return; |
| 364 | } |
| 365 | |
| 366 | [s->texture release]; |
| 367 | |
| 368 | s->surface = qemu_create_displaysurface(width, height); |
| 369 | |
| 370 | @autoreleasepool { |
| 371 | textureDescriptor = |
| 372 | [MTLTextureDescriptor |
| 373 | texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm |
| 374 | width:width |
| 375 | height:height |
| 376 | mipmapped:NO]; |
| 377 | textureDescriptor.usage = s->pgdisp.minimumTextureUsage; |
| 378 | s->texture = [s->mtl newTextureWithDescriptor:textureDescriptor]; |
| 379 | s->using_managed_texture_storage = |
| 380 | (s->texture.storageMode == MTLStorageModeManaged); |
| 381 | } |
| 382 | |
| 383 | qemu_console_set_surface(s->con, s->surface); |
| 384 | } |
| 385 | |
| 386 | static void update_cursor(AppleGFXState *s) |
| 387 | { |
| 388 | assert(bql_locked()); |
| 389 | qemu_console_set_mouse(s->con, s->pgdisp.cursorPosition.x, |
| 390 | s->pgdisp.cursorPosition.y, qatomic_read(&s->cursor_show)); |
| 391 | } |
| 392 | |
| 393 | static void update_cursor_bh(void *opaque) |
| 394 | { |
| 395 | AppleGFXState *s = opaque; |
| 396 | update_cursor(s); |
| 397 | } |
| 398 | |
| 399 | typedef struct AppleGFXSetCursorGlyphJob { |
| 400 | AppleGFXState *s; |
| 401 | NSBitmapImageRep *glyph; |
| 402 | PGDisplayCoord_t hotspot; |
| 403 | } AppleGFXSetCursorGlyphJob; |
| 404 | |
| 405 | static void set_cursor_glyph(void *opaque) |
| 406 | { |
| 407 | AppleGFXSetCursorGlyphJob *job = opaque; |
| 408 | AppleGFXState *s = job->s; |
| 409 | NSBitmapImageRep *glyph = job->glyph; |
| 410 | uint32_t bpp = glyph.bitsPerPixel; |
| 411 | size_t width = glyph.pixelsWide; |
| 412 | size_t height = glyph.pixelsHigh; |
| 413 | size_t padding_bytes_per_row = glyph.bytesPerRow - width * 4; |
| 414 | const uint8_t* px_data = glyph.bitmapData; |
| 415 | |
| 416 | trace_apple_gfx_cursor_set(bpp, width, height); |
| 417 | |
| 418 | if (s->cursor) { |
| 419 | cursor_unref(s->cursor); |
| 420 | s->cursor = NULL; |
| 421 | } |
| 422 | |
| 423 | if (bpp == 32) { /* Shouldn't be anything else, but just to be safe... */ |
| 424 | s->cursor = cursor_alloc(width, height); |
| 425 | s->cursor->hot_x = job->hotspot.x; |
| 426 | s->cursor->hot_y = job->hotspot.y; |
| 427 | |
| 428 | uint32_t *dest_px = s->cursor->data; |
| 429 | |
| 430 | for (size_t y = 0; y < height; ++y) { |
| 431 | for (size_t x = 0; x < width; ++x) { |
| 432 | /* |
| 433 | * NSBitmapImageRep's red & blue channels are swapped |
| 434 | * compared to QEMUCursor's. |
| 435 | */ |
| 436 | *dest_px = |
| 437 | (px_data[0] << 16u) | |
| 438 | (px_data[1] << 8u) | |
| 439 | (px_data[2] << 0u) | |
| 440 | (px_data[3] << 24u); |
| 441 | ++dest_px; |
| 442 | px_data += 4; |
| 443 | } |
| 444 | px_data += padding_bytes_per_row; |
| 445 | } |
| 446 | qemu_console_set_cursor(s->con, s->cursor); |
| 447 | update_cursor(s); |
| 448 | } |
| 449 | [glyph release]; |
| 450 | |
| 451 | g_free(job); |
| 452 | } |
| 453 | |
| 454 | /* ------ DMA (device reading system memory) ------ */ |
| 455 | |
| 456 | typedef struct AppleGFXReadMemoryJob { |
| 457 | QemuEvent event; |
| 458 | hwaddr physical_address; |
| 459 | uint64_t length; |
| 460 | void *dst; |
| 461 | bool success; |
| 462 | } AppleGFXReadMemoryJob; |
| 463 | |
| 464 | static void apple_gfx_do_read_memory(void *opaque) |
| 465 | { |
| 466 | AppleGFXReadMemoryJob *job = opaque; |
| 467 | MemTxResult r; |
| 468 | |
| 469 | r = dma_memory_read(&address_space_memory, job->physical_address, |
| 470 | job->dst, job->length, MEMTXATTRS_UNSPECIFIED); |
| 471 | job->success = (r == MEMTX_OK); |
| 472 | |
| 473 | qemu_event_set(&job->event); |
| 474 | } |
| 475 | |
| 476 | static bool apple_gfx_read_memory(AppleGFXState *s, hwaddr physical_address, |
| 477 | uint64_t length, void *dst) |
| 478 | { |
| 479 | AppleGFXReadMemoryJob job = { |
| 480 | .physical_address = physical_address, .length = length, .dst = dst |
| 481 | }; |
| 482 | |
| 483 | trace_apple_gfx_read_memory(physical_address, length, dst); |
| 484 | |
| 485 | /* Performing DMA requires BQL, so do it in a BH. */ |
| 486 | qemu_event_init(&job.event, 0); |
| 487 | aio_bh_schedule_oneshot(qemu_get_aio_context(), |
| 488 | apple_gfx_do_read_memory, &job); |
| 489 | qemu_event_wait(&job.event); |
| 490 | qemu_event_destroy(&job.event); |
| 491 | return job.success; |
| 492 | } |
| 493 | |
| 494 | /* ------ Memory-mapped device I/O operations ------ */ |
| 495 | |
| 496 | typedef struct AppleGFXIOJob { |
| 497 | AppleGFXState *state; |
| 498 | uint64_t offset; |
| 499 | uint64_t value; |
| 500 | bool completed; |
| 501 | } AppleGFXIOJob; |
| 502 | |
| 503 | static void apple_gfx_do_read(void *opaque) |
| 504 | { |
| 505 | AppleGFXIOJob *job = opaque; |
| 506 | job->value = [job->state->pgdev mmioReadAtOffset:job->offset]; |
| 507 | qatomic_set(&job->completed, true); |
| 508 | aio_wait_kick(); |
| 509 | } |
| 510 | |
| 511 | static uint64_t apple_gfx_read(void *opaque, hwaddr offset, unsigned size) |
| 512 | { |
| 513 | AppleGFXIOJob job = { |
| 514 | .state = opaque, |
| 515 | .offset = offset, |
| 516 | .completed = false, |
| 517 | }; |
| 518 | dispatch_queue_t queue = get_background_queue(); |
| 519 | |
| 520 | dispatch_async_f(queue, &job, apple_gfx_do_read); |
| 521 | AIO_WAIT_WHILE(NULL, !qatomic_read(&job.completed)); |
| 522 | |
| 523 | trace_apple_gfx_read(offset, job.value); |
| 524 | return job.value; |
| 525 | } |
| 526 | |
| 527 | static void apple_gfx_do_write(void *opaque) |
| 528 | { |
| 529 | AppleGFXIOJob *job = opaque; |
| 530 | [job->state->pgdev mmioWriteAtOffset:job->offset value:job->value]; |
| 531 | qatomic_set(&job->completed, true); |
| 532 | aio_wait_kick(); |
| 533 | } |
| 534 | |
| 535 | static void apple_gfx_write(void *opaque, hwaddr offset, uint64_t val, |
| 536 | unsigned size) |
| 537 | { |
| 538 | /* |
| 539 | * The methods mmioReadAtOffset: and especially mmioWriteAtOffset: can |
| 540 | * trigger synchronous operations on other dispatch queues, which in turn |
| 541 | * may call back out on one or more of the callback blocks. For this reason, |
| 542 | * and as we are holding the BQL, we invoke the I/O methods on a pool |
| 543 | * thread and handle AIO tasks while we wait. Any work in the callbacks |
| 544 | * requiring the BQL will in turn schedule BHs which this thread will |
| 545 | * process while waiting. |
| 546 | */ |
| 547 | AppleGFXIOJob job = { |
| 548 | .state = opaque, |
| 549 | .offset = offset, |
| 550 | .value = val, |
| 551 | .completed = false, |
| 552 | }; |
| 553 | dispatch_queue_t queue = get_background_queue(); |
| 554 | |
| 555 | dispatch_async_f(queue, &job, apple_gfx_do_write); |
| 556 | AIO_WAIT_WHILE(NULL, !qatomic_read(&job.completed)); |
| 557 | |
| 558 | trace_apple_gfx_write(offset, val); |
| 559 | } |
| 560 | |
| 561 | static const MemoryRegionOps apple_gfx_ops = { |
| 562 | .read = apple_gfx_read, |
| 563 | .write = apple_gfx_write, |
| 564 | .endianness = DEVICE_LITTLE_ENDIAN, |
| 565 | .valid = { |
| 566 | .min_access_size = 4, |
| 567 | .max_access_size = 8, |
| 568 | }, |
| 569 | .impl = { |
| 570 | .min_access_size = 4, |
| 571 | .max_access_size = 4, |
| 572 | }, |
| 573 | }; |
| 574 | |
| 575 | static size_t apple_gfx_get_default_mmio_range_size(void) |
| 576 | { |
| 577 | size_t mmio_range_size; |
| 578 | @autoreleasepool { |
| 579 | PGDeviceDescriptor *desc = [PGDeviceDescriptor new]; |
| 580 | mmio_range_size = desc.mmioLength; |
| 581 | [desc release]; |
| 582 | } |
| 583 | return mmio_range_size; |
| 584 | } |
| 585 | |
| 586 | /* ------ Initialisation and startup ------ */ |
| 587 | |
| 588 | void apple_gfx_common_init(Object *obj, AppleGFXState *s, const char* obj_name) |
| 589 | { |
| 590 | size_t mmio_range_size = apple_gfx_get_default_mmio_range_size(); |
| 591 | |
| 592 | trace_apple_gfx_common_init(obj_name, mmio_range_size); |
| 593 | memory_region_init_io(&s->iomem_gfx, obj, &apple_gfx_ops, s, obj_name, |
| 594 | mmio_range_size); |
| 595 | |
| 596 | /* TODO: PVG framework supports serialising device state: integrate it! */ |
| 597 | } |
| 598 | |
| 599 | static void apple_gfx_register_task_mapping_handlers(AppleGFXState *s, |
| 600 | PGDeviceDescriptor *desc) |
| 601 | { |
| 602 | desc.createTask = ^(uint64_t vmSize, void * _Nullable * _Nonnull baseAddress) { |
| 603 | PGTask_t *task = apple_gfx_new_task(s, vmSize); |
| 604 | *baseAddress = (void *)task->address; |
| 605 | trace_apple_gfx_create_task(vmSize, *baseAddress); |
| 606 | return task; |
| 607 | }; |
| 608 | |
| 609 | desc.destroyTask = ^(PGTask_t * _Nonnull task) { |
| 610 | trace_apple_gfx_destroy_task(task, task->mapped_regions->len); |
| 611 | |
| 612 | apple_gfx_destroy_task(s, task); |
| 613 | }; |
| 614 | |
| 615 | desc.mapMemory = ^bool(PGTask_t * _Nonnull task, uint32_t range_count, |
| 616 | uint64_t virtual_offset, bool read_only, |
| 617 | PGPhysicalMemoryRange_t * _Nonnull ranges) { |
| 618 | return apple_gfx_task_map_memory(s, task, virtual_offset, |
| 619 | ranges, range_count, read_only); |
| 620 | }; |
| 621 | |
| 622 | desc.unmapMemory = ^bool(PGTask_t * _Nonnull task, uint64_t virtual_offset, |
| 623 | uint64_t length) { |
| 624 | apple_gfx_task_unmap_memory(s, task, virtual_offset, length); |
| 625 | return true; |
| 626 | }; |
| 627 | |
| 628 | desc.readMemory = ^bool(uint64_t physical_address, uint64_t length, |
| 629 | void * _Nonnull dst) { |
| 630 | return apple_gfx_read_memory(s, physical_address, length, dst); |
| 631 | }; |
| 632 | } |
| 633 | |
| 634 | static void new_frame_handler_bh(void *opaque) |
| 635 | { |
| 636 | AppleGFXState *s = opaque; |
| 637 | |
| 638 | /* Drop frames if guest gets too far ahead. */ |
| 639 | if (s->pending_frames >= 2) { |
| 640 | return; |
| 641 | } |
| 642 | ++s->pending_frames; |
| 643 | if (s->pending_frames > 1) { |
| 644 | return; |
| 645 | } |
| 646 | |
| 647 | @autoreleasepool { |
| 648 | apple_gfx_render_new_frame(s); |
| 649 | } |
| 650 | } |
| 651 | |
| 652 | static PGDisplayDescriptor *apple_gfx_prepare_display_descriptor(AppleGFXState *s) |
| 653 | { |
| 654 | PGDisplayDescriptor *disp_desc = [PGDisplayDescriptor new]; |
| 655 | |
| 656 | disp_desc.name = @"QEMU display"; |
| 657 | disp_desc.sizeInMillimeters = NSMakeSize(400., 300.); /* A 20" display */ |
| 658 | disp_desc.queue = dispatch_get_main_queue(); |
| 659 | disp_desc.newFrameEventHandler = ^(void) { |
| 660 | trace_apple_gfx_new_frame(); |
| 661 | aio_bh_schedule_oneshot(qemu_get_aio_context(), new_frame_handler_bh, s); |
| 662 | }; |
| 663 | disp_desc.modeChangeHandler = ^(PGDisplayCoord_t sizeInPixels, |
| 664 | OSType pixelFormat) { |
| 665 | trace_apple_gfx_mode_change(sizeInPixels.x, sizeInPixels.y); |
| 666 | |
| 667 | BQL_LOCK_GUARD(); |
| 668 | set_mode(s, sizeInPixels.x, sizeInPixels.y); |
| 669 | }; |
| 670 | disp_desc.cursorGlyphHandler = ^(NSBitmapImageRep *glyph, |
| 671 | PGDisplayCoord_t hotspot) { |
| 672 | AppleGFXSetCursorGlyphJob *job = g_malloc0(sizeof(*job)); |
| 673 | job->s = s; |
| 674 | job->glyph = glyph; |
| 675 | job->hotspot = hotspot; |
| 676 | [glyph retain]; |
| 677 | aio_bh_schedule_oneshot(qemu_get_aio_context(), |
| 678 | set_cursor_glyph, job); |
| 679 | }; |
| 680 | disp_desc.cursorShowHandler = ^(BOOL show) { |
| 681 | trace_apple_gfx_cursor_show(show); |
| 682 | qatomic_set(&s->cursor_show, show); |
| 683 | aio_bh_schedule_oneshot(qemu_get_aio_context(), |
| 684 | update_cursor_bh, s); |
| 685 | }; |
| 686 | disp_desc.cursorMoveHandler = ^(void) { |
| 687 | trace_apple_gfx_cursor_move(); |
| 688 | aio_bh_schedule_oneshot(qemu_get_aio_context(), |
| 689 | update_cursor_bh, s); |
| 690 | }; |
| 691 | |
| 692 | return disp_desc; |
| 693 | } |
| 694 | |
| 695 | static NSArray<PGDisplayMode *> *apple_gfx_create_display_mode_array( |
| 696 | const AppleGFXDisplayMode display_modes[], uint32_t display_mode_count) |
| 697 | { |
| 698 | PGDisplayMode *mode_obj; |
| 699 | NSMutableArray<PGDisplayMode *> *mode_array = |
| 700 | [[NSMutableArray alloc] initWithCapacity:display_mode_count]; |
| 701 | |
| 702 | for (unsigned i = 0; i < display_mode_count; i++) { |
| 703 | const AppleGFXDisplayMode *mode = &display_modes[i]; |
| 704 | trace_apple_gfx_display_mode(i, mode->width_px, mode->height_px); |
| 705 | PGDisplayCoord_t mode_size = { mode->width_px, mode->height_px }; |
| 706 | |
| 707 | mode_obj = |
| 708 | [[PGDisplayMode alloc] initWithSizeInPixels:mode_size |
| 709 | refreshRateInHz:mode->refresh_rate_hz]; |
| 710 | [mode_array addObject:mode_obj]; |
| 711 | [mode_obj release]; |
| 712 | } |
| 713 | |
| 714 | return mode_array; |
| 715 | } |
| 716 | |
| 717 | static id<MTLDevice> copy_suitable_metal_device(void) |
| 718 | { |
| 719 | id<MTLDevice> dev = nil; |
| 720 | NSArray<id<MTLDevice>> *devs = MTLCopyAllDevices(); |
| 721 | |
| 722 | /* Prefer a unified memory GPU. Failing that, pick a non-removable GPU. */ |
| 723 | for (size_t i = 0; i < devs.count; ++i) { |
| 724 | if (devs[i].hasUnifiedMemory) { |
| 725 | dev = devs[i]; |
| 726 | break; |
| 727 | } |
| 728 | if (!devs[i].removable) { |
| 729 | dev = devs[i]; |
| 730 | } |
| 731 | } |
| 732 | |
| 733 | if (dev != nil) { |
| 734 | [dev retain]; |
| 735 | } else { |
| 736 | dev = MTLCreateSystemDefaultDevice(); |
| 737 | } |
| 738 | [devs release]; |
| 739 | |
| 740 | return dev; |
| 741 | } |
| 742 | |
| 743 | bool apple_gfx_common_realize(AppleGFXState *s, DeviceState *dev, |
| 744 | PGDeviceDescriptor *desc, Error **errp) |
| 745 | { |
| 746 | PGDisplayDescriptor *disp_desc; |
| 747 | const AppleGFXDisplayMode *display_modes = apple_gfx_default_modes; |
| 748 | uint32_t num_display_modes = ARRAY_SIZE(apple_gfx_default_modes); |
| 749 | NSArray<PGDisplayMode *> *mode_array; |
| 750 | |
| 751 | if (apple_gfx_mig_blocker == NULL) { |
| 752 | error_setg(&apple_gfx_mig_blocker, |
| 753 | "Migration state blocked by apple-gfx display device"); |
| 754 | if (migrate_add_blocker(&apple_gfx_mig_blocker, errp) < 0) { |
| 755 | return false; |
| 756 | } |
| 757 | } |
| 758 | |
| 759 | qemu_mutex_init(&s->task_mutex); |
| 760 | QTAILQ_INIT(&s->tasks); |
| 761 | s->mtl = copy_suitable_metal_device(); |
| 762 | s->mtl_queue = [s->mtl newCommandQueue]; |
| 763 | |
| 764 | desc.device = s->mtl; |
| 765 | |
| 766 | apple_gfx_register_task_mapping_handlers(s, desc); |
| 767 | |
| 768 | s->cursor_show = true; |
| 769 | |
| 770 | s->pgdev = PGNewDeviceWithDescriptor(desc); |
| 771 | |
| 772 | disp_desc = apple_gfx_prepare_display_descriptor(s); |
| 773 | /* |
| 774 | * Although the framework does, this integration currently does not support |
| 775 | * multiple virtual displays connected to a single PV graphics device. |
| 776 | * It is however possible to create |
| 777 | * more than one instance of the device, each with one display. The macOS |
| 778 | * guest will ignore these displays if they share the same serial number, |
| 779 | * so ensure each instance gets a unique one. |
| 780 | */ |
| 781 | s->pgdisp = [s->pgdev newDisplayWithDescriptor:disp_desc |
| 782 | port:0 |
| 783 | serialNum:next_pgdisplay_serial_num++]; |
| 784 | [disp_desc release]; |
| 785 | |
| 786 | if (s->display_modes != NULL && s->num_display_modes > 0) { |
| 787 | trace_apple_gfx_common_realize_modes_property(s->num_display_modes); |
| 788 | display_modes = s->display_modes; |
| 789 | num_display_modes = s->num_display_modes; |
| 790 | } |
| 791 | s->pgdisp.modeList = mode_array = |
| 792 | apple_gfx_create_display_mode_array(display_modes, num_display_modes); |
| 793 | [mode_array release]; |
| 794 | |
| 795 | s->con = qemu_graphic_console_create(dev, 0, &apple_gfx_fb_ops, s); |
| 796 | return true; |
| 797 | } |
| 798 | |
| 799 | /* ------ Display mode list device property ------ */ |
| 800 | |
| 801 | static void apple_gfx_get_display_mode(Object *obj, Visitor *v, |
| 802 | const char *name, void *opaque, |
| 803 | Error **errp) |
| 804 | { |
| 805 | Property *prop = opaque; |
| 806 | AppleGFXDisplayMode *mode = object_field_prop_ptr(obj, prop); |
| 807 | /* 3 uint16s (max 5 digits) + 2 separator characters + nul. */ |
| 808 | char buffer[5 * 3 + 2 + 1]; |
| 809 | char *pos = buffer; |
| 810 | |
| 811 | int rc = snprintf(buffer, sizeof(buffer), |
| 812 | "%"PRIu16"x%"PRIu16"@%"PRIu16, |
| 813 | mode->width_px, mode->height_px, |
| 814 | mode->refresh_rate_hz); |
| 815 | assert(rc < sizeof(buffer)); |
| 816 | |
| 817 | visit_type_str(v, name, &pos, errp); |
| 818 | } |
| 819 | |
| 820 | static void apple_gfx_set_display_mode(Object *obj, Visitor *v, |
| 821 | const char *name, void *opaque, |
| 822 | Error **errp) |
| 823 | { |
| 824 | Property *prop = opaque; |
| 825 | AppleGFXDisplayMode *mode = object_field_prop_ptr(obj, prop); |
| 826 | const char *endptr; |
| 827 | g_autofree char *str = NULL; |
| 828 | int ret; |
| 829 | int val; |
| 830 | |
| 831 | if (!visit_type_str(v, name, &str, errp)) { |
| 832 | return; |
| 833 | } |
| 834 | |
| 835 | endptr = str; |
| 836 | |
| 837 | ret = qemu_strtoi(endptr, &endptr, 10, &val); |
| 838 | if (ret || val > UINT16_MAX || val <= 0) { |
| 839 | error_setg(errp, "width in '%s' must be a decimal integer number" |
| 840 | " of pixels in the range 1..65535", name); |
| 841 | return; |
| 842 | } |
| 843 | mode->width_px = val; |
| 844 | if (*endptr != 'x') { |
| 845 | goto separator_error; |
| 846 | } |
| 847 | |
| 848 | ret = qemu_strtoi(endptr + 1, &endptr, 10, &val); |
| 849 | if (ret || val > UINT16_MAX || val <= 0) { |
| 850 | error_setg(errp, "height in '%s' must be a decimal integer number" |
| 851 | " of pixels in the range 1..65535", name); |
| 852 | return; |
| 853 | } |
| 854 | mode->height_px = val; |
| 855 | if (*endptr != '@') { |
| 856 | goto separator_error; |
| 857 | } |
| 858 | |
| 859 | ret = qemu_strtoi(endptr + 1, &endptr, 10, &val); |
| 860 | if (ret || val > UINT16_MAX || val <= 0) { |
| 861 | error_setg(errp, "refresh rate in '%s'" |
| 862 | " must be a positive decimal integer (Hertz)", name); |
| 863 | return; |
| 864 | } |
| 865 | mode->refresh_rate_hz = val; |
| 866 | return; |
| 867 | |
| 868 | separator_error: |
| 869 | error_setg(errp, |
| 870 | "Each display mode takes the format '<width>x<height>@<rate>'"); |
| 871 | } |
| 872 | |
| 873 | const PropertyInfo qdev_prop_apple_gfx_display_mode = { |
| 874 | .type = "display_mode", |
| 875 | .description = |
| 876 | "Display mode in pixels and Hertz, as <width>x<height>@<refresh-rate> " |
| 877 | "Example: 3840x2160@60", |
| 878 | .get = apple_gfx_get_display_mode, |
| 879 | .set = apple_gfx_set_display_mode, |
| 880 | }; |