/* * 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 #include #include #import 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 * 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, MEMTXATTRS_UNSPECIFIED)) { 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 command_buffer = [s->mtl_queue commandBuffer]; id 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 blit = [command_buffer blitCommandEncoder]; [blit synchronizeResource:texture]; [blit endEncoding]; } [texture release]; [command_buffer addCompletedHandler: ^(id 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 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 *apple_gfx_create_display_mode_array( const AppleGFXDisplayMode display_modes[], uint32_t display_mode_count) { PGDisplayMode *mode_obj; NSMutableArray *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 copy_suitable_metal_device(void) { id dev = nil; NSArray> *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 *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 'x@'"); } const PropertyInfo qdev_prop_apple_gfx_display_mode = { .type = "display_mode", .description = "Display mode in pixels and Hertz, as x@ " "Example: 3840x2160@60", .get = apple_gfx_get_display_mode, .set = apple_gfx_set_display_mode, };