| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gpu.h" |
| |
| #include <assert.h> |
| #include <inttypes.h> |
| #include <magenta/compiler.h> |
| #include <fbl/auto_lock.h> |
| #include <pretty/hexdump.h> |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/param.h> |
| |
| #include "trace.h" |
| #include "utils.h" |
| |
| #include "virtio_gpu.h" |
| |
| #define LOCAL_TRACE 0 |
| |
| namespace virtio { |
| |
| // DDK level ops |
| |
| // queue an iotxn. iotxn's are always completed by its complete() op |
| mx_status_t GpuDevice::virtio_gpu_set_mode(void* ctx, mx_display_info_t* info) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| |
| LTRACEF("dev %p, info %p\n", gd, info); |
| |
| return MX_ERR_NOT_SUPPORTED; |
| } |
| |
| mx_status_t GpuDevice::virtio_gpu_get_mode(void* ctx, mx_display_info_t* info) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| |
| LTRACEF("dev %p, info %p\n", gd, info); |
| |
| *info = {}; |
| |
| auto pmode = gd->pmode(); |
| |
| info->format = MX_PIXEL_FORMAT_RGB_x888; |
| info->width = pmode->r.width; |
| info->height = pmode->r.height; |
| info->stride = pmode->r.width; |
| info->pixelsize = 4; |
| info->flags = MX_DISPLAY_FLAG_HW_FRAMEBUFFER; |
| |
| return MX_OK; |
| } |
| |
| mx_status_t GpuDevice::virtio_gpu_get_framebuffer(void* ctx, void** framebuffer) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| |
| LTRACEF("dev %p, framebuffer %p\n", gd, framebuffer); |
| |
| void* fb = gd->framebuffer(); |
| if (!fb) |
| return MX_ERR_NOT_SUPPORTED; |
| |
| *framebuffer = fb; |
| return MX_OK; |
| } |
| |
| void GpuDevice::virtio_gpu_flush(void* ctx) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| |
| LTRACEF("dev %p\n", gd); |
| |
| gd->Flush(); |
| } |
| |
| GpuDevice::GpuDevice(mx_device_t* bus_device) |
| : Device(bus_device) { |
| |
| cnd_init(&request_cond_); |
| cnd_init(&flush_cond_); |
| } |
| |
| GpuDevice::~GpuDevice() { |
| // TODO: clean up allocated physical memory |
| cnd_destroy(&request_cond_); |
| cnd_destroy(&flush_cond_); |
| } |
| |
| static void dump_gpu_config(const volatile struct virtio_gpu_config* config) { |
| LTRACEF("events_read 0x%x\n", config->events_read); |
| LTRACEF("events_clear 0x%x\n", config->events_clear); |
| LTRACEF("num_scanouts 0x%x\n", config->num_scanouts); |
| LTRACEF("reserved 0x%x\n", config->reserved); |
| } |
| |
| mx_status_t GpuDevice::send_command_response(const void* cmd, size_t cmd_len, void** _res, size_t res_len) { |
| LTRACEF("dev %p, cmd %p, cmd_len %zu, res %p, res_len %zu\n", this, cmd, cmd_len, _res, res_len); |
| |
| uint16_t i; |
| struct vring_desc* desc = vring_.AllocDescChain(2, &i); |
| assert(desc); |
| |
| memcpy(gpu_req_, cmd, cmd_len); |
| |
| desc->addr = gpu_req_pa_; |
| desc->len = (uint32_t)cmd_len; |
| desc->flags |= VRING_DESC_F_NEXT; |
| |
| /* set the second descriptor to the response with the write bit set */ |
| desc = vring_.DescFromIndex(desc->next); |
| assert(desc); |
| |
| void* res = (void*)((uint8_t*)gpu_req_ + cmd_len); |
| *_res = res; |
| mx_paddr_t res_phys = gpu_req_pa_ + cmd_len; |
| memset(res, 0, res_len); |
| |
| desc->addr = res_phys; |
| desc->len = (uint32_t)res_len; |
| desc->flags = VRING_DESC_F_WRITE; |
| |
| /* submit the transfer */ |
| vring_.SubmitChain(i); |
| |
| /* kick it off */ |
| vring_.Kick(); |
| |
| /* wait for result */ |
| cnd_wait(&request_cond_, request_lock_.GetInternal()); |
| |
| return MX_OK; |
| } |
| |
| mx_status_t GpuDevice::get_display_info() { |
| LTRACEF("dev %p\n", this); |
| |
| /* grab a lock to keep this single message at a time */ |
| fbl::AutoLock lock(&request_lock_); |
| |
| /* construct the get display info message */ |
| virtio_gpu_ctrl_hdr req; |
| memset(&req, 0, sizeof(req)); |
| req.type = VIRTIO_GPU_CMD_GET_DISPLAY_INFO; |
| |
| /* send the message and get a response */ |
| virtio_gpu_resp_display_info* info; |
| auto err = send_command_response(&req, sizeof(req), (void**)&info, sizeof(*info)); |
| if (err < MX_OK) { |
| return MX_ERR_NOT_FOUND; |
| } |
| |
| /* we got response */ |
| if (info->hdr.type != VIRTIO_GPU_RESP_OK_DISPLAY_INFO) { |
| return MX_ERR_NOT_FOUND; |
| } |
| |
| LTRACEF("response:\n"); |
| for (int i = 0; i < VIRTIO_GPU_MAX_SCANOUTS; i++) { |
| if (info->pmodes[i].enabled) { |
| LTRACEF("%u: x %u y %u w %u h %u flags 0x%x\n", i, |
| info->pmodes[i].r.x, info->pmodes[i].r.y, info->pmodes[i].r.width, info->pmodes[i].r.height, |
| info->pmodes[i].flags); |
| if (pmode_id_ < 0) { |
| /* save the first valid pmode we see */ |
| memcpy(&pmode_, &info->pmodes[i], sizeof(pmode_)); |
| pmode_id_ = i; |
| } |
| } |
| } |
| |
| return MX_OK; |
| } |
| |
| mx_status_t GpuDevice::allocate_2d_resource(uint32_t* resource_id, uint32_t width, uint32_t height) { |
| LTRACEF("dev %p\n", this); |
| |
| assert(resource_id); |
| |
| /* grab a lock to keep this single message at a time */ |
| fbl::AutoLock lock(&request_lock_); |
| |
| /* construct the request */ |
| virtio_gpu_resource_create_2d req; |
| memset(&req, 0, sizeof(req)); |
| |
| req.hdr.type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D; |
| req.resource_id = next_resource_id_++; |
| *resource_id = req.resource_id; |
| req.format = VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM; |
| req.width = width; |
| req.height = height; |
| |
| /* send the command and get a response */ |
| struct virtio_gpu_ctrl_hdr* res; |
| auto err = send_command_response(&req, sizeof(req), (void**)&res, sizeof(*res)); |
| assert(err == MX_OK); |
| |
| /* see if we got a valid response */ |
| LTRACEF("response type 0x%x\n", res->type); |
| err = (res->type == VIRTIO_GPU_RESP_OK_NODATA) ? MX_OK : MX_ERR_NO_MEMORY; |
| |
| return err; |
| } |
| |
| mx_status_t GpuDevice::attach_backing(uint32_t resource_id, mx_paddr_t ptr, size_t buf_len) { |
| LTRACEF("dev %p, resource_id %u, ptr %#" PRIxPTR ", buf_len %zu\n", this, resource_id, ptr, buf_len); |
| |
| assert(ptr); |
| |
| /* grab a lock to keep this single message at a time */ |
| fbl::AutoLock lock(&request_lock_); |
| |
| /* construct the request */ |
| struct { |
| struct virtio_gpu_resource_attach_backing req; |
| struct virtio_gpu_mem_entry mem; |
| } req; |
| memset(&req, 0, sizeof(req)); |
| |
| req.req.hdr.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING; |
| req.req.resource_id = resource_id; |
| req.req.nr_entries = 1; |
| |
| req.mem.addr = ptr; |
| req.mem.length = (uint32_t)buf_len; |
| |
| /* send the command and get a response */ |
| struct virtio_gpu_ctrl_hdr* res; |
| auto err = send_command_response(&req, sizeof(req), (void**)&res, sizeof(*res)); |
| assert(err == MX_OK); |
| |
| /* see if we got a valid response */ |
| LTRACEF("response type 0x%x\n", res->type); |
| err = (res->type == VIRTIO_GPU_RESP_OK_NODATA) ? MX_OK : MX_ERR_NO_MEMORY; |
| |
| return err; |
| } |
| |
| mx_status_t GpuDevice::set_scanout(uint32_t scanout_id, uint32_t resource_id, uint32_t width, uint32_t height) { |
| LTRACEF("dev %p, scanout_id %u, resource_id %u, width %u, height %u\n", this, scanout_id, resource_id, width, height); |
| |
| /* grab a lock to keep this single message at a time */ |
| fbl::AutoLock lock(&request_lock_); |
| |
| /* construct the request */ |
| virtio_gpu_set_scanout req; |
| memset(&req, 0, sizeof(req)); |
| |
| req.hdr.type = VIRTIO_GPU_CMD_SET_SCANOUT; |
| req.r.x = req.r.y = 0; |
| req.r.width = width; |
| req.r.height = height; |
| req.scanout_id = scanout_id; |
| req.resource_id = resource_id; |
| |
| /* send the command and get a response */ |
| virtio_gpu_ctrl_hdr* res; |
| auto err = send_command_response(&req, sizeof(req), (void**)&res, sizeof(*res)); |
| assert(err == MX_OK); |
| |
| /* see if we got a valid response */ |
| LTRACEF("response type 0x%x\n", res->type); |
| err = (res->type == VIRTIO_GPU_RESP_OK_NODATA) ? MX_OK : MX_ERR_NO_MEMORY; |
| |
| return err; |
| } |
| |
| mx_status_t GpuDevice::flush_resource(uint32_t resource_id, uint32_t width, uint32_t height) { |
| LTRACEF("dev %p, resource_id %u, width %u, height %u\n", this, resource_id, width, height); |
| |
| /* grab a lock to keep this single message at a time */ |
| fbl::AutoLock lock(&request_lock_); |
| |
| /* construct the request */ |
| virtio_gpu_resource_flush req; |
| memset(&req, 0, sizeof(req)); |
| |
| req.hdr.type = VIRTIO_GPU_CMD_RESOURCE_FLUSH; |
| req.r.x = req.r.y = 0; |
| req.r.width = width; |
| req.r.height = height; |
| req.resource_id = resource_id; |
| |
| /* send the command and get a response */ |
| virtio_gpu_ctrl_hdr* res; |
| auto err = send_command_response(&req, sizeof(req), (void**)&res, sizeof(*res)); |
| assert(err == MX_OK); |
| |
| /* see if we got a valid response */ |
| LTRACEF("response type 0x%x\n", res->type); |
| err = (res->type == VIRTIO_GPU_RESP_OK_NODATA) ? MX_OK : MX_ERR_NO_MEMORY; |
| |
| return err; |
| } |
| |
| mx_status_t GpuDevice::transfer_to_host_2d(uint32_t resource_id, uint32_t width, uint32_t height) { |
| LTRACEF("dev %p, resource_id %u, width %u, height %u\n", this, resource_id, width, height); |
| |
| /* grab a lock to keep this single message at a time */ |
| fbl::AutoLock lock(&request_lock_); |
| |
| /* construct the request */ |
| virtio_gpu_transfer_to_host_2d req; |
| memset(&req, 0, sizeof(req)); |
| |
| req.hdr.type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D; |
| req.r.x = req.r.y = 0; |
| req.r.width = width; |
| req.r.height = height; |
| req.offset = 0; |
| req.resource_id = resource_id; |
| |
| /* send the command and get a response */ |
| virtio_gpu_ctrl_hdr* res; |
| auto err = send_command_response(&req, sizeof(req), (void**)&res, sizeof(*res)); |
| assert(err == MX_OK); |
| |
| /* see if we got a valid response */ |
| LTRACEF("response type 0x%x\n", res->type); |
| err = (res->type == VIRTIO_GPU_RESP_OK_NODATA) ? MX_OK : MX_ERR_NO_MEMORY; |
| |
| return err; |
| } |
| |
| void GpuDevice::Flush() { |
| fbl::AutoLock al(&flush_lock_); |
| flush_pending_ = true; |
| cnd_signal(&flush_cond_); |
| } |
| |
| void GpuDevice::virtio_gpu_flusher() { |
| LTRACE_ENTRY; |
| for (;;) { |
| { |
| fbl::AutoLock al(&flush_lock_); |
| if (!flush_pending_) |
| cnd_wait(&flush_cond_, flush_lock_.GetInternal()); |
| flush_pending_ = false; |
| } |
| |
| LTRACEF("flushing\n"); |
| |
| /* transfer to host 2d */ |
| auto err = transfer_to_host_2d(display_resource_id_, pmode_.r.width, pmode_.r.height); |
| if (err < 0) { |
| LTRACEF("failed to flush resource\n"); |
| continue; |
| } |
| |
| /* resource flush */ |
| err = flush_resource(display_resource_id_, pmode_.r.width, pmode_.r.height); |
| if (err < 0) { |
| LTRACEF("failed to flush resource\n"); |
| continue; |
| } |
| } |
| } |
| |
| int GpuDevice::virtio_gpu_flusher_entry(void* arg) { |
| GpuDevice* gd = static_cast<GpuDevice*>(arg); |
| |
| gd->virtio_gpu_flusher(); |
| |
| return 0; |
| } |
| |
| mx_status_t GpuDevice::virtio_gpu_start() { |
| mx_status_t err; |
| |
| LTRACEF("dev %p\n", this); |
| |
| /* get the display info and see if we find a valid pmode */ |
| err = get_display_info(); |
| if (err < 0) { |
| VIRTIO_ERROR("failed to get display info\n"); |
| return err; |
| } |
| |
| if (pmode_id_ < 0) { |
| VIRTIO_ERROR("we failed to find a pmode, exiting\n"); |
| return MX_ERR_NOT_FOUND; |
| } |
| |
| printf("virtio-gpu: found display x %u y %u w %u h %u flags 0x%x\n", |
| pmode_.r.x, pmode_.r.y, pmode_.r.width, pmode_.r.height, |
| pmode_.flags); |
| |
| /* allocate a resource */ |
| err = allocate_2d_resource(&display_resource_id_, pmode_.r.width, pmode_.r.height); |
| if (err < 0) { |
| VIRTIO_ERROR("failed to allocate 2d resource\n"); |
| return err; |
| } |
| |
| /* attach a backing store to the resource */ |
| size_t len = pmode_.r.width * pmode_.r.height * 4; |
| |
| err = map_contiguous_memory(len, (uintptr_t*)&fb_, &fb_pa_); |
| if (err < 0) { |
| VIRTIO_ERROR("failed to allocate framebuffer, wanted 0x%zx bytes\n", len); |
| return MX_ERR_NO_MEMORY; |
| } |
| |
| LTRACEF("framebuffer at %p, 0x%zx bytes\n", fb_, len); |
| |
| err = attach_backing(display_resource_id_, fb_pa_, len); |
| if (err < 0) { |
| VIRTIO_ERROR("failed to attach backing store\n"); |
| return err; |
| } |
| |
| /* attach this resource as a scanout */ |
| err = set_scanout(pmode_id_, display_resource_id_, pmode_.r.width, pmode_.r.height); |
| if (err < 0) { |
| VIRTIO_ERROR("failed to set scanout\n"); |
| return err; |
| } |
| |
| // run a worker thread to shove in flush events |
| thrd_create_with_name(&flush_thread_, virtio_gpu_flusher_entry, this, "virtio-gpu-flusher"); |
| thrd_detach(flush_thread_); |
| |
| LTRACEF("publishing device\n"); |
| |
| display_proto_ops_.set_mode = virtio_gpu_set_mode; |
| display_proto_ops_.get_mode = virtio_gpu_get_mode; |
| display_proto_ops_.get_framebuffer = virtio_gpu_get_framebuffer; |
| display_proto_ops_.flush = virtio_gpu_flush; |
| |
| // initialize the mx_device and publish us |
| // point the ctx of our DDK device at ourself |
| |
| device_add_args_t args = {}; |
| args.version = DEVICE_ADD_ARGS_VERSION; |
| args.name = "virtio-gpu"; |
| args.ctx = this; |
| args.ops = &device_ops_; |
| args.proto_id = MX_PROTOCOL_DISPLAY; |
| args.proto_ops = &display_proto_ops_; |
| |
| auto status = device_add(bus_device_, &args, &bus_device_); |
| if (status < 0) { |
| device_ = nullptr; |
| return status; |
| } |
| |
| LTRACE_EXIT; |
| |
| return MX_OK; |
| } |
| |
| int GpuDevice::virtio_gpu_start_entry(void* arg) { |
| |
| GpuDevice* gd = static_cast<GpuDevice*>(arg); |
| |
| gd->virtio_gpu_start(); |
| |
| return 0; |
| } |
| |
| mx_status_t GpuDevice::Init() { |
| LTRACE_ENTRY; |
| |
| // reset the device |
| Reset(); |
| |
| volatile virtio_gpu_config* config = (virtio_gpu_config*)mmio_regs_.device_config; |
| dump_gpu_config(config); |
| |
| // ack and set the driver status bit |
| StatusAcknowledgeDriver(); |
| |
| // XXX check features bits and ack/nak them |
| |
| // allocate the main vring |
| auto err = vring_.Init(0, 16); |
| if (err < 0) { |
| VIRTIO_ERROR("failed to allocate vring\n"); |
| return err; |
| } |
| |
| // allocate a gpu request |
| auto r = map_contiguous_memory(PAGE_SIZE, (uintptr_t*)&gpu_req_, &gpu_req_pa_); |
| if (r < 0) { |
| VIRTIO_ERROR("cannot alloc gpu_req buffers %d\n", r); |
| return r; |
| } |
| |
| LTRACEF("allocated gpu request at %p, physical address %#" PRIxPTR "\n", gpu_req_, gpu_req_pa_); |
| |
| // start the interrupt thread |
| StartIrqThread(); |
| |
| // set DRIVER_OK |
| StatusDriverOK(); |
| |
| // start a worker thread that runs through a sequence to finish initializing the gpu |
| thrd_create_with_name(&start_thread_, virtio_gpu_start_entry, this, "virtio-gpu-starter"); |
| thrd_detach(start_thread_); |
| |
| return MX_OK; |
| } |
| |
| void GpuDevice::IrqRingUpdate() { |
| LTRACE_ENTRY; |
| |
| // parse our descriptor chain, add back to the free queue |
| auto free_chain = [this](vring_used_elem* used_elem) { |
| uint32_t i = (uint16_t)used_elem->id; |
| struct vring_desc* desc = vring_.DescFromIndex((uint16_t)i); |
| __UNUSED auto head_desc = desc; // save the first element |
| for (;;) { |
| int next; |
| |
| if (desc->flags & VRING_DESC_F_NEXT) { |
| next = desc->next; |
| } else { |
| /* end of chain */ |
| next = -1; |
| } |
| |
| vring_.FreeDesc((uint16_t)i); |
| |
| if (next < 0) |
| break; |
| i = next; |
| desc = vring_.DescFromIndex((uint16_t)i); |
| } |
| |
| // wack the request condition |
| request_lock_.Acquire(); |
| cnd_signal(&request_cond_); |
| request_lock_.Release(); |
| }; |
| |
| // tell the ring to find free chains and hand it back to our lambda |
| vring_.IrqRingUpdate(free_chain); |
| } |
| |
| void GpuDevice::IrqConfigChange() { |
| LTRACE_ENTRY; |
| } |
| |
| } // namespace virtio |