| // 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 <inttypes.h> |
| #include <string.h> |
| #include <sys/param.h> |
| |
| #include <ddk/debug.h> |
| #include <fbl/auto_call.h> |
| #include <fbl/auto_lock.h> |
| #include <zircon/compiler.h> |
| #include <zircon/time.h> |
| |
| #include <utility> |
| |
| #include "trace.h" |
| #include "virtio_gpu.h" |
| |
| #define LOCAL_TRACE 0 |
| |
| namespace virtio { |
| |
| namespace { |
| |
| constexpr uint32_t kRefreshRateHz = 30; |
| constexpr uint64_t kDisplayId = 1; |
| |
| zx_status_t to_zx_status(uint32_t type) { |
| LTRACEF("response type %#x\n", type); |
| if (type != VIRTIO_GPU_RESP_OK_NODATA) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| return ZX_OK; |
| } |
| |
| } // namespace |
| |
| // DDK level ops |
| |
| typedef struct imported_image { |
| uint32_t resource_id; |
| zx::pmt pmt; |
| } imported_image_t; |
| |
| void GpuDevice::virtio_gpu_set_display_controller_interface( |
| void* ctx, const display_controller_interface_protocol_t* intf) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| { |
| fbl::AutoLock al(&gd->flush_lock_); |
| gd->dc_intf_ = *intf; |
| } |
| |
| added_display_args_t args = {}; |
| args.display_id = kDisplayId, |
| args.edid_present = false, |
| args.panel.params = { |
| .width = gd->pmode_.r.width, |
| .height = gd->pmode_.r.height, |
| .refresh_rate_e2 = kRefreshRateHz * 100, |
| }, |
| args.pixel_format_list = &gd->supported_formats_, |
| args.pixel_format_count = 1, |
| display_controller_interface_on_displays_changed(intf, &args, 1, nullptr, 0, |
| nullptr, 0, nullptr); |
| } |
| |
| zx_status_t GpuDevice::virtio_gpu_import_vmo_image(void* ctx, image_t* image, |
| zx_handle_t vmo_in, size_t offset) { |
| zx::vmo vmo(vmo_in); |
| |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| if (image->type != IMAGE_TYPE_SIMPLE) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| fbl::AllocChecker ac; |
| auto import_data = fbl::make_unique_checked<imported_image_t>(&ac); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| unsigned pixel_size = ZX_PIXEL_FORMAT_BYTES(image->pixel_format); |
| unsigned size = ROUNDUP(image->width * image->height * pixel_size, PAGE_SIZE); |
| zx_paddr_t paddr; |
| zx_status_t status = gd->bti_.pin(ZX_BTI_PERM_READ | ZX_BTI_CONTIGUOUS, vmo, offset, size, |
| &paddr, 1, &import_data->pmt); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| status = gd->allocate_2d_resource(&import_data->resource_id, image->width, image->height); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: failed to allocate 2d resource\n", gd->tag()); |
| return status; |
| } |
| |
| status = gd->attach_backing(import_data->resource_id, paddr, size); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: failed to attach backing store\n", gd->tag()); |
| return status; |
| } |
| |
| image->handle = reinterpret_cast<uint64_t>(import_data.release()); |
| |
| return ZX_OK; |
| } |
| |
| void GpuDevice::virtio_gpu_release_image(void* ctx, image_t* image) { |
| delete reinterpret_cast<imported_image_t*>(image->handle); |
| } |
| |
| uint32_t GpuDevice::virtio_gpu_check_configuration(void* ctx, |
| const display_config_t** display_configs, |
| size_t display_count, |
| uint32_t** layer_cfg_results, |
| size_t* layer_cfg_result_count) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| if (display_count != 1) { |
| ZX_DEBUG_ASSERT(display_count == 0); |
| return CONFIG_DISPLAY_OK; |
| } |
| ZX_DEBUG_ASSERT(display_configs[0]->display_id == kDisplayId); |
| bool success; |
| if (display_configs[0]->layer_count != 1) { |
| success = display_configs[0]->layer_count == 0; |
| } else { |
| primary_layer_t* layer = &display_configs[0]->layer_list[0]->cfg.primary; |
| frame_t frame = { |
| .x_pos = 0, .y_pos = 0, .width = gd->pmode_.r.width, .height = gd->pmode_.r.height, |
| }; |
| success = display_configs[0]->layer_list[0]->type == LAYER_TYPE_PRIMARY |
| && layer->transform_mode == FRAME_TRANSFORM_IDENTITY |
| && layer->image.width == gd->pmode_.r.width |
| && layer->image.height == gd->pmode_.r.height |
| && memcmp(&layer->dest_frame, &frame, sizeof(frame_t)) == 0 |
| && memcmp(&layer->src_frame, &frame, sizeof(frame_t)) == 0 |
| && display_configs[0]->cc_flags == 0 |
| && layer->alpha_mode == ALPHA_DISABLE; |
| } |
| if (!success) { |
| layer_cfg_results[0][0] = CLIENT_MERGE_BASE; |
| for (unsigned i = 1; i < display_configs[0]->layer_count; i++) { |
| layer_cfg_results[0][i] = CLIENT_MERGE_SRC; |
| } |
| layer_cfg_result_count[0] = display_configs[0]->layer_count; |
| } |
| return CONFIG_DISPLAY_OK; |
| } |
| |
| void GpuDevice::virtio_gpu_apply_configuration(void* ctx, const display_config_t** display_configs, |
| size_t display_count) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| uint64_t handle = display_count == 0 || display_configs[0]->layer_count == 0 |
| ? 0 : display_configs[0]->layer_list[0]->cfg.primary.image.handle; |
| |
| { |
| fbl::AutoLock al(&gd->flush_lock_); |
| gd->current_fb_ = reinterpret_cast<imported_image_t*>(handle); |
| } |
| |
| gd->Flush(); |
| } |
| |
| uint32_t GpuDevice::virtio_gpu_compute_linear_stride(void* ctx, uint32_t width, |
| zx_pixel_format_t format) { |
| return width; |
| } |
| |
| zx_status_t GpuDevice::virtio_gpu_allocate_vmo(void* ctx, uint64_t size, zx_handle_t* vmo_out) { |
| GpuDevice* gd = static_cast<GpuDevice*>(ctx); |
| return zx_vmo_create_contiguous(gd->bti().get(), size, 0, vmo_out); |
| } |
| |
| zx_status_t GpuDevice::virtio_get_sysmem_connection(void* ctx, zx_handle_t handle) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t GpuDevice::virtio_set_buffer_collection_constraints(void* ctx, const image_t* config, |
| zx_unowned_handle_t collection) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t GpuDevice::virtio_get_single_buffer_framebuffer(void* ctx, zx_handle_t* out_vmo, |
| uint32_t* out_stride) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| GpuDevice::GpuDevice(zx_device_t* bus_device, zx::bti bti, fbl::unique_ptr<Backend> backend) |
| : Device(bus_device, std::move(bti), std::move(backend)) { |
| sem_init(&request_sem_, 0, 1); |
| sem_init(&response_sem_, 0, 0); |
| cnd_init(&flush_cond_); |
| |
| memset(&gpu_req_, 0, sizeof(gpu_req_)); |
| } |
| |
| GpuDevice::~GpuDevice() { |
| io_buffer_release(&gpu_req_); |
| |
| // TODO: clean up allocated physical memory |
| sem_destroy(&request_sem_); |
| sem_destroy(&response_sem_); |
| cnd_destroy(&flush_cond_); |
| } |
| |
| template <typename RequestType, typename ResponseType> |
| void GpuDevice::send_command_response(const RequestType* cmd, ResponseType** res) { |
| size_t cmd_len = sizeof(RequestType); |
| size_t res_len = sizeof(ResponseType); |
| LTRACEF("dev %p, cmd %p, cmd_len %zu, res %p, res_len %zu\n", this, cmd, cmd_len, res, res_len); |
| |
| // Keep this single message at a time |
| sem_wait(&request_sem_); |
| fbl::MakeAutoCall([this]() { sem_post(&request_sem_); }); |
| |
| uint16_t i; |
| struct vring_desc* desc = vring_.AllocDescChain(2, &i); |
| ZX_ASSERT(desc); |
| |
| void* gpu_req_base = io_buffer_virt(&gpu_req_); |
| zx_paddr_t gpu_req_pa = io_buffer_phys(&gpu_req_); |
| |
| memcpy(gpu_req_base, cmd, cmd_len); |
| |
| desc->addr = gpu_req_pa; |
| desc->len = static_cast<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); |
| ZX_ASSERT(desc); |
| |
| *res = reinterpret_cast<ResponseType*>(static_cast<uint8_t*>(gpu_req_base) + cmd_len); |
| zx_paddr_t res_phys = gpu_req_pa + cmd_len; |
| memset(*res, 0, res_len); |
| |
| desc->addr = res_phys; |
| desc->len = static_cast<uint32_t>(res_len); |
| desc->flags = VRING_DESC_F_WRITE; |
| |
| // Submit the transfer & wait for the response |
| vring_.SubmitChain(i); |
| vring_.Kick(); |
| sem_wait(&response_sem_); |
| } |
| |
| zx_status_t GpuDevice::get_display_info() { |
| LTRACEF("dev %p\n", this); |
| |
| // 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; |
| send_command_response(&req, &info); |
| if (info->hdr.type != VIRTIO_GPU_RESP_OK_DISPLAY_INFO) { |
| return ZX_ERR_NOT_FOUND; |
| } |
| |
| // We got a response |
| 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 ZX_OK; |
| } |
| |
| zx_status_t GpuDevice::allocate_2d_resource(uint32_t* resource_id, uint32_t width, uint32_t height) { |
| LTRACEF("dev %p\n", this); |
| |
| ZX_ASSERT(resource_id); |
| |
| // 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; |
| send_command_response(&req, &res); |
| |
| return to_zx_status(res->type); |
| } |
| |
| zx_status_t GpuDevice::attach_backing(uint32_t resource_id, zx_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); |
| |
| ZX_ASSERT(ptr); |
| |
| // 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; |
| send_command_response(&req, &res); |
| return to_zx_status(res->type); |
| } |
| |
| zx_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); |
| |
| // 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; |
| send_command_response(&req, &res); |
| return to_zx_status(res->type); |
| } |
| |
| zx_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); |
| |
| // 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; |
| send_command_response(&req, &res); |
| return to_zx_status(res->type); |
| } |
| |
| zx_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); |
| |
| // 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; |
| send_command_response(&req, &res); |
| return to_zx_status(res->type); |
| } |
| |
| void GpuDevice::Flush() { |
| fbl::AutoLock al(&flush_lock_); |
| flush_pending_ = true; |
| cnd_signal(&flush_cond_); |
| } |
| |
| void GpuDevice::virtio_gpu_flusher() { |
| LTRACE_ENTRY; |
| zx_time_t next_deadline = zx_clock_get_monotonic(); |
| zx_time_t period = ZX_SEC(1) / kRefreshRateHz; |
| for (;;) { |
| zx_nanosleep(next_deadline); |
| |
| bool fb_change; |
| { |
| fbl::AutoLock al(&flush_lock_); |
| fb_change = displayed_fb_ != current_fb_; |
| displayed_fb_ = current_fb_; |
| } |
| |
| LTRACEF("flushing\n"); |
| |
| if (displayed_fb_) { |
| zx_status_t status = transfer_to_host_2d( |
| displayed_fb_->resource_id, pmode_.r.width, pmode_.r.height); |
| if (status != ZX_OK) { |
| LTRACEF("failed to flush resource\n"); |
| continue; |
| } |
| |
| status = flush_resource(displayed_fb_->resource_id, pmode_.r.width, pmode_.r.height); |
| if (status != ZX_OK) { |
| LTRACEF("failed to flush resource\n"); |
| continue; |
| } |
| } |
| |
| if (fb_change) { |
| uint32_t res_id = displayed_fb_ ? displayed_fb_->resource_id : 0; |
| zx_status_t status = set_scanout(pmode_id_, res_id, pmode_.r.width, pmode_.r.height); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: failed to set scanout\n", tag()); |
| continue; |
| } |
| } |
| |
| { |
| fbl::AutoLock al(&flush_lock_); |
| if (dc_intf_.ops) { |
| uint64_t handles[] = { reinterpret_cast<uint64_t>(displayed_fb_) }; |
| display_controller_interface_on_display_vsync( |
| &dc_intf_, kDisplayId, next_deadline, handles, displayed_fb_ != nullptr); |
| } |
| } |
| next_deadline = zx_time_add_duration(next_deadline, period); |
| } |
| } |
| |
| zx_status_t GpuDevice::virtio_gpu_start() { |
| |
| LTRACEF("dev %p\n", this); |
| |
| // Get the display info and see if we find a valid pmode |
| zx_status_t status = get_display_info(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: failed to get display info\n", tag()); |
| return status; |
| } |
| |
| if (pmode_id_ < 0) { |
| zxlogf(ERROR, "%s: failed to find a pmode, exiting\n", tag()); |
| return ZX_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); |
| |
| // Run a worker thread to shove in flush events |
| auto virtio_gpu_flusher_entry = [](void* arg) { |
| static_cast<GpuDevice*>(arg)->virtio_gpu_flusher(); |
| return 0; |
| }; |
| 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_display_controller_interface = |
| virtio_gpu_set_display_controller_interface; |
| display_proto_ops_.import_vmo_image = virtio_gpu_import_vmo_image; |
| display_proto_ops_.release_image = virtio_gpu_release_image; |
| display_proto_ops_.check_configuration = virtio_gpu_check_configuration; |
| display_proto_ops_.apply_configuration = virtio_gpu_apply_configuration; |
| display_proto_ops_.compute_linear_stride = virtio_gpu_compute_linear_stride; |
| display_proto_ops_.allocate_vmo = virtio_gpu_allocate_vmo; |
| display_proto_ops_.get_sysmem_connection = virtio_get_sysmem_connection; |
| display_proto_ops_.set_buffer_collection_constraints = virtio_set_buffer_collection_constraints; |
| display_proto_ops_.get_single_buffer_framebuffer = virtio_get_single_buffer_framebuffer; |
| |
| // Initialize the zx_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-display"; |
| args.ctx = this; |
| args.ops = &device_ops_; |
| args.proto_id = ZX_PROTOCOL_DISPLAY_CONTROLLER_IMPL; |
| args.proto_ops = &display_proto_ops_; |
| |
| status = device_add(bus_device_, &args, &bus_device_); |
| if (status != ZX_OK) { |
| device_ = nullptr; |
| return status; |
| } |
| |
| LTRACE_EXIT; |
| return ZX_OK; |
| } |
| |
| zx_status_t GpuDevice::Init() { |
| LTRACE_ENTRY; |
| |
| DeviceReset(); |
| |
| struct virtio_gpu_config config; |
| CopyDeviceConfig(&config, sizeof(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); |
| |
| // Ack and set the driver status bit |
| DriverStatusAck(); |
| |
| // XXX check features bits and ack/nak them |
| |
| // Allocate the main vring |
| zx_status_t status = vring_.Init(0, 16); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: failed to allocate vring\n", tag()); |
| return status; |
| } |
| |
| // Allocate a GPU request |
| status = io_buffer_init(&gpu_req_, bti_.get(), PAGE_SIZE, IO_BUFFER_RW | IO_BUFFER_CONTIG); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: cannot alloc gpu_req buffers %d\n", tag(), status); |
| return status; |
| } |
| |
| LTRACEF("allocated gpu request at %p, physical address %#" PRIxPTR "\n", |
| io_buffer_virt(&gpu_req_), io_buffer_phys(&gpu_req_)); |
| |
| StartIrqThread(); |
| DriverStatusOk(); |
| |
| // Start a worker thread that runs through a sequence to finish initializing the GPU |
| auto virtio_gpu_start_entry = [](void* arg) { |
| return static_cast<GpuDevice*>(arg)->virtio_gpu_start(); |
| }; |
| thrd_create_with_name(&start_thread_, virtio_gpu_start_entry, this, "virtio-gpu-starter"); |
| thrd_detach(start_thread_); |
| |
| return ZX_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) { |
| uint16_t i = static_cast<uint16_t>(used_elem->id); |
| struct vring_desc* desc = vring_.DescFromIndex(i); |
| for (;;) { |
| int next; |
| |
| if (desc->flags & VRING_DESC_F_NEXT) { |
| next = desc->next; |
| } else { |
| // End of chain |
| next = -1; |
| } |
| |
| vring_.FreeDesc(i); |
| |
| if (next < 0) { |
| break; |
| } |
| i = static_cast<uint16_t>(next); |
| desc = vring_.DescFromIndex(i); |
| } |
| // Notify the request thread |
| sem_post(&response_sem_); |
| }; |
| |
| // 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 |