| // Copyright 2018 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 "src/graphics/display/drivers/coordinator/controller.h" |
| |
| #include <fuchsia/hardware/audiotypes/c/banjo.h> |
| #include <fuchsia/hardware/display/controller/cpp/banjo.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/ddk/binding_driver.h> |
| #include <lib/ddk/debug.h> |
| #include <lib/ddk/driver.h> |
| #include <lib/ddk/trace/event.h> |
| #include <lib/fit/function.h> |
| #include <lib/stdcompat/span.h> |
| #include <lib/trace/event.h> |
| #include <lib/zbi-format/graphics.h> |
| #include <lib/zx/channel.h> |
| #include <lib/zx/clock.h> |
| #include <lib/zx/result.h> |
| #include <lib/zx/time.h> |
| #include <threads.h> |
| #include <zircon/assert.h> |
| #include <zircon/errors.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/threads.h> |
| #include <zircon/time.h> |
| #include <zircon/types.h> |
| |
| #include <algorithm> |
| #include <cinttypes> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <cstring> |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include <ddktl/device.h> |
| #include <ddktl/unbind-txn.h> |
| #include <fbl/alloc_checker.h> |
| #include <fbl/array.h> |
| #include <fbl/auto_lock.h> |
| #include <fbl/ref_ptr.h> |
| #include <fbl/vector.h> |
| |
| #include "src/graphics/display/drivers/coordinator/client-id.h" |
| #include "src/graphics/display/drivers/coordinator/client-priority.h" |
| #include "src/graphics/display/drivers/coordinator/client.h" |
| #include "src/graphics/display/drivers/coordinator/display-info.h" |
| #include "src/graphics/display/drivers/coordinator/eld.h" |
| #include "src/graphics/display/drivers/coordinator/image.h" |
| #include "src/graphics/display/drivers/coordinator/layer.h" |
| #include "src/graphics/display/drivers/coordinator/migration-util.h" |
| #include "src/graphics/display/lib/api-types-cpp/config-stamp.h" |
| #include "src/graphics/display/lib/api-types-cpp/display-id.h" |
| #include "src/graphics/display/lib/api-types-cpp/display-timing.h" |
| #include "src/graphics/display/lib/api-types-cpp/driver-buffer-collection-id.h" |
| #include "src/graphics/display/lib/api-types-cpp/driver-capture-image-id.h" |
| #include "src/graphics/display/lib/edid/edid.h" |
| #include "src/graphics/display/lib/edid/timings.h" |
| |
| namespace fidl_display = fuchsia_hardware_display; |
| |
| namespace { |
| |
| // Use the same default watchdog timeout as scenic, which may help ensure watchdog logs/errors |
| // happen close together and can be correlated. |
| constexpr uint64_t kWatchdogWarningIntervalMs = 15000; |
| constexpr uint64_t kWatchdogTimeoutMs = 45000; |
| |
| // vsync delivery is considered to be stalled if at least this amount of time has elapsed since |
| // vsync was last observed. |
| constexpr zx::duration kVsyncStallThreshold = zx::sec(10); |
| constexpr zx::duration kVsyncMonitorInterval = kVsyncStallThreshold / 2; |
| |
| } // namespace |
| |
| namespace display { |
| |
| void Controller::PopulateDisplayMode(const display::DisplayTiming& timing, display_mode_t* mode) { |
| *mode = display::ToBanjoDisplayMode(timing); |
| } |
| |
| void Controller::PopulateDisplayTimings(const fbl::RefPtr<DisplayInfo>& info) { |
| if (!info->edid.has_value()) { |
| return; |
| } |
| |
| // Go through all the display mode timings and record whether or not |
| // a basic layer configuration is acceptable. |
| layer_t test_layer = {}; |
| const layer_t* test_layers[] = {&test_layer}; |
| test_layer.type = LAYER_TYPE_PRIMARY; |
| display_config_t test_config; |
| const display_config_t* test_configs[] = {&test_config}; |
| test_config.display_id = ToBanjoDisplayId(info->id); |
| test_config.layer_count = 1; |
| test_config.layer_list = test_layers; |
| |
| for (auto edid_timing = edid::timing_iterator(&info->edid->base); edid_timing.is_valid(); |
| ++edid_timing) { |
| const display::DisplayTiming& timing = *edid_timing; |
| int32_t width = timing.horizontal_active_px; |
| int32_t height = timing.vertical_active_lines; |
| bool duplicate = false; |
| for (const display::DisplayTiming& existing_timing : info->edid->timings) { |
| if (existing_timing.vertical_field_refresh_rate_millihertz() == |
| timing.vertical_field_refresh_rate_millihertz() && |
| existing_timing.horizontal_active_px == width && |
| existing_timing.vertical_active_lines == height) { |
| duplicate = true; |
| break; |
| } |
| } |
| if (duplicate) { |
| continue; |
| } |
| test_layer.cfg.primary.image_metadata.width = width; |
| test_layer.cfg.primary.image_metadata.height = height; |
| test_layer.cfg.primary.src_frame.width = width; |
| test_layer.cfg.primary.src_frame.height = height; |
| test_layer.cfg.primary.dest_frame.width = width; |
| test_layer.cfg.primary.dest_frame.height = height; |
| test_config.mode = display::ToBanjoDisplayMode(timing); |
| |
| uint32_t display_cfg_result; |
| client_composition_opcode_t layer_result = 0; |
| size_t display_layer_results_count; |
| display_cfg_result = driver_.CheckConfiguration(test_configs, 1, &layer_result, |
| /*client_composition_opcodes_count=*/1, |
| &display_layer_results_count); |
| if (display_cfg_result == CONFIG_CHECK_RESULT_OK) { |
| fbl::AllocChecker ac; |
| info->edid->timings.push_back(timing, &ac); |
| if (!ac.check()) { |
| zxlogf(WARNING, "Edid skip allocation failed"); |
| break; |
| } |
| } |
| } |
| } |
| |
| void Controller::DisplayControllerInterfaceOnDisplaysChanged( |
| const added_display_args_t* displays_added, size_t added_count, |
| const uint64_t* displays_removed, size_t removed_count) { |
| fbl::Vector<fbl::RefPtr<DisplayInfo>> added_display_infos; |
| fbl::Vector<DisplayId> removed_display_ids; |
| std::unique_ptr<async::Task> task; |
| |
| fbl::AllocChecker alloc_checker; |
| if (added_count) { |
| added_display_infos.reserve(added_count, &alloc_checker); |
| if (!alloc_checker.check()) { |
| zxlogf(ERROR, "No memory when processing hotplug"); |
| return; |
| } |
| } |
| if (removed_count) { |
| removed_display_ids.reserve(removed_count, &alloc_checker); |
| if (!alloc_checker.check()) { |
| zxlogf(ERROR, "No memory when processing hotplug"); |
| return; |
| } |
| for (size_t i = 0; i < removed_count; ++i) { |
| removed_display_ids.push_back(ToDisplayId(displays_removed[i])); |
| } |
| } |
| task = fbl::make_unique_checked<async::Task>(&alloc_checker); |
| if (!alloc_checker.check()) { |
| zxlogf(ERROR, "No memory when processing hotplug"); |
| return; |
| } |
| |
| fbl::AutoLock lock(mtx()); |
| |
| for (unsigned i = 0; i < removed_count; i++) { |
| DisplayId removed_display_id(displays_removed[i]); |
| |
| auto target = displays_.erase(removed_display_id); |
| if (target) { |
| while (fbl::RefPtr<Image> image = target->images.pop_front()) { |
| AssertMtxAliasHeld(image->mtx()); |
| image->StartRetire(); |
| image->OnRetire(); |
| } |
| } else { |
| zxlogf(DEBUG, "Unknown display %" PRIu64 " removed", removed_display_id.value()); |
| } |
| } |
| |
| for (unsigned i = 0; i < added_count; i++) { |
| zx::result<fbl::RefPtr<DisplayInfo>> info_result = DisplayInfo::Create(displays_added[i]); |
| if (info_result.is_error()) { |
| zxlogf(INFO, "failed to add display %ld: %s", displays_added[i].display_id, |
| info_result.status_string()); |
| continue; |
| } |
| fbl::RefPtr<DisplayInfo> info = std::move(info_result).value(); |
| if (info->edid.has_value()) { |
| fbl::Array<uint8_t> eld; |
| ComputeEld(info->edid->base, eld); |
| driver_.SetEld(info->id, eld); |
| } |
| |
| if (displays_.insert_or_find(info)) { |
| added_display_infos.push_back(std::move(info)); |
| } else { |
| zxlogf(INFO, "Ignoring duplicate display"); |
| } |
| } |
| |
| task->set_handler([this, added_display_infos = std::move(added_display_infos), |
| removed_display_ids = std::move(removed_display_ids)]( |
| async_dispatcher_t* dispatcher, async::Task* task, zx_status_t status) { |
| if (status == ZX_OK) { |
| for (const fbl::RefPtr<DisplayInfo>& added_display_info : added_display_infos) { |
| if (added_display_info->edid.has_value()) { |
| PopulateDisplayTimings(added_display_info); |
| } |
| } |
| |
| // TODO(b/317914671): Pass parsed display metadata to driver. |
| |
| fbl::AutoLock lock(mtx()); |
| |
| fbl::Vector<DisplayId> added_ids; |
| added_ids.reserve(added_display_infos.size()); |
| for (const fbl::RefPtr<DisplayInfo>& added_display_info : added_display_infos) { |
| // Dropping some add events can result in spurious removes, but |
| // those are filtered out in the clients. |
| if (!added_display_info->edid.has_value() || |
| !added_display_info->edid->timings.is_empty()) { |
| added_ids.push_back(added_display_info->id); |
| added_display_info->init_done = true; |
| added_display_info->InitializeInspect(&root_); |
| } else { |
| zxlogf(WARNING, "Ignoring display with no compatible edid timings"); |
| } |
| } |
| if (virtcon_client_ready_) { |
| ZX_DEBUG_ASSERT(virtcon_client_ != nullptr); |
| virtcon_client_->OnDisplaysChanged(added_ids, removed_display_ids); |
| } |
| if (primary_client_ready_) { |
| ZX_DEBUG_ASSERT(primary_client_ != nullptr); |
| primary_client_->OnDisplaysChanged(added_ids, removed_display_ids); |
| } |
| } else { |
| zxlogf(ERROR, "Failed to dispatch display change task %d", status); |
| } |
| |
| delete task; |
| }); |
| task.release()->Post(loop_.dispatcher()); |
| } |
| |
| void Controller::DisplayControllerInterfaceOnCaptureComplete() { |
| if (!supports_capture_) { |
| zxlogf(ERROR, |
| "OnCaptureComplete(): the display engine doesn't support " |
| "display capture."); |
| return; |
| } |
| |
| std::unique_ptr<async::Task> task = std::make_unique<async::Task>(); |
| fbl::AutoLock lock(mtx()); |
| task->set_handler([this](async_dispatcher_t* dispatcher, async::Task* task, zx_status_t status) { |
| if (status == ZX_OK) { |
| // Free an image that was previously used by the hardware. |
| if (pending_release_capture_image_id_ != kInvalidDriverCaptureImageId) { |
| ReleaseCaptureImage(pending_release_capture_image_id_); |
| pending_release_capture_image_id_ = kInvalidDriverCaptureImageId; |
| } |
| |
| fbl::AutoLock lock(mtx()); |
| if (virtcon_client_ready_) { |
| ZX_DEBUG_ASSERT(virtcon_client_ != nullptr); |
| virtcon_client_->OnCaptureComplete(); |
| } |
| if (primary_client_ready_) { |
| ZX_DEBUG_ASSERT(primary_client_ != nullptr); |
| primary_client_->OnCaptureComplete(); |
| } |
| } else { |
| zxlogf(ERROR, "Failed to dispatch capture complete task %d", status); |
| } |
| delete task; |
| }); |
| task.release()->Post(loop_.dispatcher()); |
| } |
| |
| void Controller::DisplayControllerInterfaceOnDisplayVsync(uint64_t banjo_display_id, |
| zx_time_t timestamp, |
| const config_stamp_t* config_stamp_ptr) { |
| // Emit an event called "VSYNC", which is by convention the event |
| // that Trace Viewer looks for in its "Highlight VSync" feature. |
| TRACE_INSTANT("gfx", "VSYNC", TRACE_SCOPE_THREAD, "display_id", banjo_display_id); |
| TRACE_DURATION("gfx", "Display::Controller::OnDisplayVsync", "display_id", banjo_display_id); |
| |
| const DisplayId display_id(banjo_display_id); |
| |
| last_vsync_ns_property_.Set(timestamp); |
| last_vsync_interval_ns_property_.Set(timestamp - last_vsync_timestamp_.load().get()); |
| last_vsync_timestamp_ = zx::time(timestamp); |
| vsync_stalled_ = false; |
| |
| ConfigStamp controller_config_stamp = |
| config_stamp_ptr ? ToConfigStamp(*config_stamp_ptr) : kInvalidConfigStamp; |
| last_vsync_config_stamp_property_.Set(controller_config_stamp.value()); |
| |
| fbl::AutoLock lock(mtx()); |
| DisplayInfo* info = nullptr; |
| for (auto& display_config : displays_) { |
| if (display_config.id == display_id) { |
| info = &display_config; |
| break; |
| } |
| } |
| |
| if (!info) { |
| zxlogf(ERROR, "No such display %" PRIu64, display_id.value()); |
| return; |
| } |
| |
| // See ::ApplyConfig for more explanation of how vsync image tracking works. |
| // |
| // If there's a pending layer change, don't process any present/retire actions |
| // until the change is complete. |
| if (info->pending_layer_change) { |
| bool done = controller_config_stamp >= info->pending_layer_change_controller_config_stamp; |
| if (done) { |
| info->pending_layer_change = false; |
| info->pending_layer_change_controller_config_stamp = kInvalidConfigStamp; |
| info->switching_client = false; |
| |
| if (active_client_ && info->delayed_apply) { |
| active_client_->ReapplyConfig(); |
| } |
| } |
| } |
| |
| // Determine whether the configuration (associated with Controller |
| // |config_stamp|) comes from primary client, virtcon client, or neither. |
| enum class ConfigStampSource { kPrimary, kVirtcon, kNeither }; |
| ConfigStampSource config_stamp_source = ConfigStampSource::kNeither; |
| |
| struct { |
| ClientProxy* client; |
| ConfigStampSource source; |
| } const kClientInfo[] = { |
| { |
| .client = primary_client_, |
| .source = ConfigStampSource::kPrimary, |
| }, |
| { |
| .client = virtcon_client_, |
| .source = ConfigStampSource::kVirtcon, |
| }, |
| }; |
| |
| for (const auto& [client, source] : kClientInfo) { |
| if (client) { |
| auto pending_stamps = client->pending_applied_config_stamps(); |
| auto it = std::find_if(pending_stamps.begin(), pending_stamps.end(), |
| [controller_config_stamp](const auto& pending_stamp) { |
| return pending_stamp.controller_stamp >= controller_config_stamp; |
| }); |
| if (it != pending_stamps.end() && it->controller_stamp == controller_config_stamp) { |
| config_stamp_source = source; |
| // Obsolete stamps will be removed in |Client::OnDisplayVsync|. |
| break; |
| } |
| } |
| }; |
| |
| if (!info->pending_layer_change) { |
| // Each image in the `info->images` set can fall into one of the following |
| // cases: |
| // - being displayed (its `latest_controller_config_stamp` matches the |
| // incoming `controller_config_stamp` from display driver); |
| // - older than the current displayed image (its |
| // `latest_controller_config_stamp` is less than the incoming |
| // `controller_config_stamp`) and should be retired; |
| // - newer than the current displayed image (its |
| // `latest_controller_config_stamp` is greater than the incoming |
| // `controller_config_stamp`) and yet to be presented. |
| for (auto it = info->images.begin(); it != info->images.end();) { |
| bool should_retire = it->latest_controller_config_stamp() < controller_config_stamp; |
| |
| // Retire any images which are older than whatever is currently in their |
| // layer. |
| if (should_retire) { |
| fbl::RefPtr<Image> image_to_retire = info->images.erase(it++); |
| |
| AssertMtxAliasHeld(image_to_retire->mtx()); |
| image_to_retire->OnRetire(); |
| // Older images may not be presented. Ending their flows here |
| // ensures the correctness of traces. |
| // |
| // NOTE: If changing this flow name or ID, please also do so in the |
| // corresponding FLOW_BEGIN in display_swapchain.cc. |
| TRACE_FLOW_END("gfx", "present_image", image_to_retire->id.value()); |
| } else { |
| it++; |
| } |
| } |
| } |
| |
| // TODO(https://fxbug.dev/42152065): This is a stopgap solution to support existing |
| // OnVsync() DisplayController FIDL events. In the future we'll remove this |
| // logic and only return config seqnos in OnVsync() events instead. |
| |
| if (controller_config_stamp != kInvalidConfigStamp) { |
| auto& config_image_queue = info->config_image_queue; |
| |
| // Evict retired configurations from the queue. |
| while (!config_image_queue.empty() && |
| config_image_queue.front().config_stamp < controller_config_stamp) { |
| config_image_queue.pop(); |
| } |
| |
| // Since the stamps sent from Controller to drivers are in chronological |
| // order, the Vsync signals Controller receives should also be in |
| // chronological order as well. |
| // |
| // Applying empty configs won't create entries in |config_image_queue|. |
| // Otherwise, we'll get the list of images used at ApplyConfig() with |
| // the given |config_stamp|. |
| if (!config_image_queue.empty() && |
| config_image_queue.front().config_stamp == controller_config_stamp) { |
| for (const auto& image : config_image_queue.front().images) { |
| // End of the flow for the image going to be presented. |
| // |
| // NOTE: If changing this flow name or ID, please also do so in the |
| // corresponding FLOW_BEGIN in display_swapchain.cc. |
| TRACE_FLOW_END("gfx", "present_image", image.image_id.value()); |
| } |
| } |
| } |
| |
| switch (config_stamp_source) { |
| case ConfigStampSource::kPrimary: |
| primary_client_->OnDisplayVsync(display_id, timestamp, controller_config_stamp); |
| break; |
| case ConfigStampSource::kVirtcon: |
| virtcon_client_->OnDisplayVsync(display_id, timestamp, controller_config_stamp); |
| break; |
| case ConfigStampSource::kNeither: |
| if (primary_client_) { |
| // A previous client applied a config and then disconnected before the vsync. Don't send |
| // garbage image IDs to the new primary client. |
| if (primary_client_->client_id() != applied_client_id_) { |
| zxlogf(DEBUG, |
| "Dropping vsync. This was meant for client[%" PRIu64 "], but client[%" PRIu64 |
| "] is currently active.\n", |
| applied_client_id_.value(), primary_client_->client_id().value()); |
| } |
| } |
| } |
| } |
| |
| void Controller::ApplyConfig(DisplayConfig* configs[], int32_t count, ConfigStamp config_stamp, |
| uint32_t layer_stamp, ClientId client_id) { |
| zx_time_t timestamp = zx_clock_get_monotonic(); |
| last_valid_apply_config_timestamp_ns_property_.Set(timestamp); |
| last_valid_apply_config_interval_ns_property_.Set(timestamp - last_valid_apply_config_timestamp_); |
| last_valid_apply_config_timestamp_ = timestamp; |
| |
| last_valid_apply_config_config_stamp_property_.Set(config_stamp.value()); |
| |
| // Release the bootloader framebuffer referenced by the kernel. This only |
| // needs to be done once on the first ApplyConfig(). |
| if (!kernel_framebuffer_released_) { |
| zx_framebuffer_set_range(get_framebuffer_resource(parent()), /*vmo=*/ZX_HANDLE_INVALID, |
| /*len=*/0, |
| /*format=*/ZBI_PIXEL_FORMAT_NONE, /*width=*/0, /*height=*/0, |
| /*stride=*/0); |
| kernel_framebuffer_released_ = true; |
| } |
| |
| // TODO(https://fxbug.dev/42080631): Replace VLA with fixed-size array once we have a |
| // limit on the number of connected displays. |
| const int32_t display_configs_size = std::max(1, count); |
| const display_config_t* display_configs[display_configs_size]; |
| uint32_t display_count = 0; |
| |
| // The applied configuration's stamp. |
| // |
| // Populated from `controller_stamp_` while the mutex is held. |
| ConfigStamp applied_config_stamp = {}; |
| |
| { |
| fbl::AutoLock lock(mtx()); |
| bool switching_client = client_id != applied_client_id_; |
| |
| // The fact that there could already be a vsync waiting to be handled when a config |
| // is applied means that a vsync with no handle for a layer could be interpreted as either |
| // nothing in the layer has been presented or everything in the layer can be retired. To |
| // prevent that ambiguity, we don't allow a layer to be disabled until an image from |
| // it has been displayed. |
| // |
| // Since layers can be moved between displays but the implementation only supports |
| // tracking the image in one display's queue, we need to ensure that the old display is |
| // done with a migrated image before the new display is done with it. This means |
| // that the new display can't flip until the configuration change is done. However, we |
| // don't want to completely prohibit flips, as that would add latency if the layer's new |
| // image is being waited for when the configuration is applied. |
| // |
| // To handle both of these cases, we force all layer changes to complete before the client |
| // can apply a new configuration. We allow the client to apply a more complete version of |
| // the configuration, although Client::HandleApplyConfig won't migrate a layer's current |
| // image if there is also a pending image. |
| if (switching_client || applied_layer_stamp_ != layer_stamp) { |
| for (int i = 0; i < count; i++) { |
| DisplayConfig* config = configs[i]; |
| auto display = displays_.find(config->id); |
| if (!display.IsValid()) { |
| continue; |
| } |
| |
| if (display->pending_layer_change) { |
| display->delayed_apply = true; |
| return; |
| } |
| } |
| } |
| |
| // Now we can guarantee that this configuration will be applied to display |
| // controller. Thus increment the controller ApplyConfiguration() counter. |
| ++controller_stamp_; |
| applied_config_stamp = controller_stamp_; |
| |
| for (int i = 0; i < count; i++) { |
| auto* config = configs[i]; |
| auto display = displays_.find(config->id); |
| if (!display.IsValid()) { |
| continue; |
| } |
| |
| auto& config_image_queue = display->config_image_queue; |
| config_image_queue.push({.config_stamp = applied_config_stamp, .images = {}}); |
| |
| display->switching_client = switching_client; |
| display->pending_layer_change = config->apply_layer_change(); |
| if (display->pending_layer_change) { |
| display->pending_layer_change_controller_config_stamp = applied_config_stamp; |
| } |
| display->layer_count = config->current_layer_count(); |
| display->delayed_apply = false; |
| |
| if (display->layer_count == 0) { |
| continue; |
| } |
| |
| display_configs[display_count++] = config->current_config(); |
| |
| for (auto& layer_node : config->get_current_layers()) { |
| Layer* layer = layer_node.layer; |
| fbl::RefPtr<Image> image = layer->current_image(); |
| |
| if (layer->is_skipped() || !image) { |
| continue; |
| } |
| |
| // Set the image controller config stamp so vsync knows what config the |
| // image was used at. |
| AssertMtxAliasHeld(image->mtx()); |
| image->set_latest_controller_config_stamp(applied_config_stamp); |
| image->StartPresent(); |
| |
| // It's possible that the image's layer was moved between displays. The logic around |
| // pending_layer_change guarantees that the old display will be done with the image |
| // before the new display is, so deleting it from the old list is fine. |
| // |
| // Even if we're on the same display, the entry needs to be moved to the end of the |
| // list to ensure that the last config->current.layer_count elements in the queue |
| // are the current images. |
| if (image->InDoublyLinkedList()) { |
| image->RemoveFromDoublyLinkedList(); |
| } |
| display->images.push_back(image); |
| |
| config_image_queue.back().images.push_back({image->id, image->client_id()}); |
| } |
| } |
| |
| applied_layer_stamp_ = layer_stamp; |
| applied_client_id_ = client_id; |
| |
| if (active_client_) { |
| if (switching_client) { |
| active_client_->ReapplySpecialConfigs(); |
| } |
| |
| active_client_->UpdateConfigStampMapping({ |
| .controller_stamp = controller_stamp_, |
| .client_stamp = config_stamp, |
| }); |
| } |
| } |
| |
| const config_stamp_t banjo_config_stamp = ToBanjoConfigStamp(applied_config_stamp); |
| driver_.ApplyConfiguration(display_configs, display_count, &banjo_config_stamp); |
| } |
| |
| void Controller::ReleaseImage(DriverImageId driver_image_id) { |
| driver_.ReleaseImage(driver_image_id); |
| } |
| |
| void Controller::ReleaseCaptureImage(DriverCaptureImageId driver_capture_image_id) { |
| if (!supports_capture_) { |
| return; |
| } |
| if (driver_capture_image_id == kInvalidDriverCaptureImageId) { |
| return; |
| } |
| |
| const zx::result<> result = driver_.ReleaseCapture(driver_capture_image_id); |
| if (result.is_error() && result.error_value() == ZX_ERR_SHOULD_WAIT) { |
| ZX_DEBUG_ASSERT_MSG(pending_release_capture_image_id_ == kInvalidDriverCaptureImageId, |
| "multiple pending releases for capture images"); |
| // Delay the image release until the hardware is done. |
| pending_release_capture_image_id_ = driver_capture_image_id; |
| } |
| } |
| |
| void Controller::SetVirtconMode(fuchsia_hardware_display::wire::VirtconMode virtcon_mode) { |
| fbl::AutoLock lock(mtx()); |
| virtcon_mode_ = virtcon_mode; |
| HandleClientOwnershipChanges(); |
| } |
| |
| void Controller::HandleClientOwnershipChanges() { |
| ClientProxy* new_active; |
| if (virtcon_mode_ == fidl_display::wire::VirtconMode::kForced || |
| (virtcon_mode_ == fidl_display::wire::VirtconMode::kFallback && primary_client_ == nullptr)) { |
| new_active = virtcon_client_; |
| } else { |
| new_active = primary_client_; |
| } |
| |
| if (new_active != active_client_) { |
| if (active_client_) { |
| active_client_->SetOwnership(false); |
| } |
| if (new_active) { |
| new_active->SetOwnership(true); |
| } |
| active_client_ = new_active; |
| } |
| } |
| |
| void Controller::OnClientDead(ClientProxy* client) { |
| zxlogf(DEBUG, "Client %" PRIu64 " dead", client->client_id().value()); |
| fbl::AutoLock lock(mtx()); |
| if (unbinding_) { |
| return; |
| } |
| if (client == virtcon_client_) { |
| virtcon_client_ = nullptr; |
| virtcon_mode_ = fidl_display::wire::VirtconMode::kInactive; |
| virtcon_client_ready_ = false; |
| } else if (client == primary_client_) { |
| primary_client_ = nullptr; |
| primary_client_ready_ = false; |
| } else { |
| ZX_DEBUG_ASSERT_MSG(false, "Dead client is neither vc nor primary\n"); |
| } |
| HandleClientOwnershipChanges(); |
| |
| clients_.remove_if( |
| [client](std::unique_ptr<ClientProxy>& list_client) { return list_client.get() == client; }); |
| } |
| |
| bool Controller::GetPanelConfig(DisplayId display_id, |
| const fbl::Vector<display::DisplayTiming>** timings, |
| const display_mode_t** mode) { |
| ZX_DEBUG_ASSERT(mtx_trylock(&mtx_) == thrd_busy); |
| if (unbinding_) { |
| return false; |
| } |
| for (auto& display : displays_) { |
| if (display.id == display_id) { |
| if (display.edid.has_value()) { |
| *timings = &display.edid->timings; |
| *mode = nullptr; |
| } else { |
| ZX_DEBUG_ASSERT(display.mode.has_value()); |
| *timings = nullptr; |
| *mode = &*display.mode; |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| zx::result<fbl::Array<CoordinatorPixelFormat>> Controller::GetSupportedPixelFormats( |
| DisplayId display_id) { |
| ZX_DEBUG_ASSERT(mtx_trylock(&mtx_) == thrd_busy); |
| fbl::Array<CoordinatorPixelFormat> formats_out; |
| for (auto& display : displays_) { |
| if (display.id == display_id) { |
| fbl::AllocChecker alloc_checker; |
| size_t size = display.pixel_formats.size(); |
| formats_out = fbl::Array<CoordinatorPixelFormat>( |
| new (&alloc_checker) CoordinatorPixelFormat[size], size); |
| if (!alloc_checker.check()) { |
| return zx::error(ZX_ERR_NO_MEMORY); |
| } |
| std::copy(display.pixel_formats.begin(), display.pixel_formats.end(), formats_out.begin()); |
| return zx::ok(std::move(formats_out)); |
| } |
| } |
| return zx::error(ZX_ERR_NOT_FOUND); |
| } |
| |
| bool Controller::GetDisplayIdentifiers(DisplayId display_id, const char** manufacturer_name, |
| const char** monitor_name, const char** monitor_serial) { |
| ZX_DEBUG_ASSERT(mtx_trylock(&mtx_) == thrd_busy); |
| for (auto& display : displays_) { |
| if (display.id == display_id) { |
| display.GetIdentifiers(manufacturer_name, monitor_name, monitor_serial); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool Controller::GetDisplayPhysicalDimensions(DisplayId display_id, uint32_t* horizontal_size_mm, |
| uint32_t* vertical_size_mm) { |
| ZX_DEBUG_ASSERT(mtx_trylock(&mtx_) == thrd_busy); |
| for (DisplayInfo& display : displays_) { |
| if (display.id == display_id) { |
| display.GetPhysicalDimensions(horizontal_size_mm, vertical_size_mm); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| namespace { |
| |
| void PrintChannelKoids(ClientPriority client_priority, const zx::channel& channel) { |
| zx_info_handle_basic_t info{}; |
| size_t actual, avail; |
| zx_status_t status = channel.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), &actual, &avail); |
| if (status != ZX_OK || info.type != ZX_OBJ_TYPE_CHANNEL) { |
| zxlogf(DEBUG, "Could not get koids for handle(type=%d): %d", info.type, status); |
| return; |
| } |
| ZX_DEBUG_ASSERT(actual == avail); |
| zxlogf(INFO, "%s client connecting on channel (c=0x%lx, s=0x%lx)", |
| DebugStringFromClientPriority(client_priority), info.related_koid, info.koid); |
| } |
| |
| } // namespace |
| |
| zx_status_t Controller::CreateClient( |
| ClientPriority client_priority, |
| fidl::ServerEnd<fidl_display::Coordinator> coordinator_server_end, |
| fit::function<void()> on_display_client_dead) { |
| PrintChannelKoids(client_priority, coordinator_server_end.channel()); |
| fbl::AllocChecker ac; |
| std::unique_ptr<async::Task> task = fbl::make_unique_checked<async::Task>(&ac); |
| if (!ac.check()) { |
| zxlogf(DEBUG, "Failed to alloc client task"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| fbl::AutoLock lock(mtx()); |
| if (unbinding_) { |
| zxlogf(DEBUG, "Client connected during unbind"); |
| return ZX_ERR_UNAVAILABLE; |
| } |
| |
| if ((client_priority == ClientPriority::kVirtcon && virtcon_client_ != nullptr) || |
| (client_priority == ClientPriority::kPrimary && primary_client_ != nullptr)) { |
| zxlogf(DEBUG, "%s client already bound", DebugStringFromClientPriority(client_priority)); |
| return ZX_ERR_ALREADY_BOUND; |
| } |
| |
| ClientId client_id = next_client_id_; |
| ++next_client_id_; |
| auto client = std::make_unique<ClientProxy>(this, client_priority, client_id, |
| std::move(on_display_client_dead)); |
| |
| zx_status_t status = client->Init(&root_, std::move(coordinator_server_end)); |
| if (status != ZX_OK) { |
| zxlogf(DEBUG, "Failed to init client %d", status); |
| return status; |
| } |
| |
| ClientProxy* client_ptr = client.get(); |
| clients_.push_back(std::move(client)); |
| |
| zxlogf(DEBUG, "New %s client [%" PRIu64 "] connected.", |
| DebugStringFromClientPriority(client_priority), client_ptr->client_id().value()); |
| |
| switch (client_priority) { |
| case ClientPriority::kVirtcon: |
| ZX_DEBUG_ASSERT(virtcon_client_ == nullptr); |
| ZX_DEBUG_ASSERT(!virtcon_client_ready_); |
| virtcon_client_ = client_ptr; |
| break; |
| case ClientPriority::kPrimary: |
| ZX_DEBUG_ASSERT(primary_client_ == nullptr); |
| ZX_DEBUG_ASSERT(!primary_client_ready_); |
| primary_client_ = client_ptr; |
| } |
| HandleClientOwnershipChanges(); |
| |
| task->set_handler( |
| [this, client_ptr](async_dispatcher_t* dispatcher, async::Task* task, zx_status_t status) { |
| if (status == ZX_OK) { |
| fbl::AutoLock lock(mtx()); |
| if (unbinding_) { |
| return; |
| } |
| if (client_ptr == virtcon_client_ || client_ptr == primary_client_) { |
| // Add all existing displays to the client |
| if (displays_.size() > 0) { |
| DisplayId current_displays[displays_.size()]; |
| int initialized_display_count = 0; |
| for (const DisplayInfo& display : displays_) { |
| if (display.init_done) { |
| current_displays[initialized_display_count] = display.id; |
| ++initialized_display_count; |
| } |
| } |
| cpp20::span<DisplayId> removed_display_ids = {}; |
| client_ptr->OnDisplaysChanged( |
| cpp20::span<DisplayId>(current_displays, initialized_display_count), |
| removed_display_ids); |
| } |
| |
| if (virtcon_client_ == client_ptr) { |
| ZX_DEBUG_ASSERT(!virtcon_client_ready_); |
| virtcon_client_ready_ = true; |
| } else { |
| ZX_DEBUG_ASSERT(!primary_client_ready_); |
| primary_client_ready_ = true; |
| } |
| } |
| } |
| delete task; |
| }); |
| |
| return task.release()->Post(loop_.dispatcher()); |
| } |
| |
| display::DriverBufferCollectionId Controller::GetNextDriverBufferCollectionId() { |
| fbl::AutoLock lock(mtx()); |
| return next_driver_buffer_collection_id_++; |
| } |
| |
| void Controller::OpenCoordinatorForVirtcon(OpenCoordinatorForVirtconRequestView request, |
| OpenCoordinatorForVirtconCompleter::Sync& completer) { |
| completer.Reply(CreateClient(ClientPriority::kVirtcon, std::move(request->coordinator))); |
| } |
| |
| void Controller::OpenCoordinatorForPrimary(OpenCoordinatorForPrimaryRequestView request, |
| OpenCoordinatorForPrimaryCompleter::Sync& completer) { |
| completer.Reply(CreateClient(ClientPriority::kPrimary, std::move(request->coordinator))); |
| } |
| |
| void Controller::OnVsyncMonitor() { |
| if (vsync_stalled_) { |
| return; |
| } |
| |
| if ((zx::clock::get_monotonic() - last_vsync_timestamp_.load()) > kVsyncStallThreshold) { |
| vsync_stalled_ = true; |
| vsync_stalls_detected_.Add(1); |
| } |
| |
| zx_status_t status = vsync_monitor_.PostDelayed(loop_.dispatcher(), kVsyncMonitorInterval); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to schedule vsync monitor: %s", zx_status_get_string(status)); |
| } |
| } |
| |
| ConfigStamp Controller::TEST_controller_stamp() const { |
| fbl::AutoLock lock(mtx()); |
| return controller_stamp_; |
| } |
| |
| zx_status_t Controller::Bind(std::unique_ptr<display::Controller>* device_ptr) { |
| ZX_DEBUG_ASSERT_MSG(device_ptr && device_ptr->get() == this, "Wrong controller passed to Bind()"); |
| |
| zx_status_t status = driver_.Bind(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to bind driver %d", status); |
| return status; |
| } |
| |
| status = loop_.StartThread("display-client-loop", &loop_thread_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to start loop %d", status); |
| return status; |
| } |
| |
| status = DdkAdd(ddk::DeviceAddArgs("display-coordinator") |
| .set_flags(DEVICE_ADD_NON_BINDABLE) |
| .set_inspect_vmo(inspector_.DuplicateVmo())); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to add display core device %d", status); |
| return status; |
| } |
| |
| // Set the display controller looper thread to use a scheduler role. |
| { |
| const char* role_name = "fuchsia.graphics.display.drivers.display.controller"; |
| status = device_set_profile_by_role(this->zxdev(), thrd_get_zx_handle(loop_thread_), role_name, |
| strlen(role_name)); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "Failed to apply role: %s", zx_status_get_string(status)); |
| } |
| } |
| |
| [[maybe_unused]] auto ptr = device_ptr->release(); |
| |
| driver_.SetDisplayControllerInterface(&display_controller_interface_protocol_ops_); |
| |
| supports_capture_ = driver_.IsCaptureSupported(); |
| zxlogf(INFO, "Display capture is%s supported: %s", supports_capture_ ? "" : " not", |
| zx_status_get_string(status)); |
| |
| status = vsync_monitor_.PostDelayed(loop_.dispatcher(), kVsyncMonitorInterval); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to schedule vsync monitor: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| void Controller::DdkUnbind(ddk::UnbindTxn txn) { |
| zxlogf(INFO, "Controller::DdkUnbind"); |
| |
| fbl::AutoLock lock(mtx()); |
| unbinding_ = true; |
| // Tell each client to start releasing. We know `clients_` will not be |
| // modified here because we are holding the lock. |
| for (auto& client : clients_) { |
| client->CloseOnControllerLoop(); |
| } |
| |
| txn.Reply(); |
| } |
| |
| void Controller::DdkRelease() { |
| vsync_monitor_.Cancel(); |
| // Clients may have active work holding mtx_ in loop_.dispatcher(), so shut it down without mtx_. |
| loop_.Shutdown(); |
| |
| // Set an empty config so that the display driver releases resources. |
| const display_config_t* configs; |
| { |
| fbl::AutoLock lock(mtx()); |
| ++controller_stamp_; |
| const config_stamp_t banjo_config_stamp = ToBanjoConfigStamp(controller_stamp_); |
| driver_.ApplyConfiguration(&configs, 0, &banjo_config_stamp); |
| } |
| driver_.ResetDisplayControllerInterface(); |
| delete this; |
| } |
| |
| Controller::Controller(zx_device_t* parent) : Controller(parent, inspect::Inspector{}) {} |
| |
| Controller::Controller(zx_device_t* parent, inspect::Inspector inspector) |
| : DeviceType(parent), |
| inspector_(std::move(inspector)), |
| loop_(&kAsyncLoopConfigNoAttachToCurrentThread), |
| watchdog_("display-client-loop", kWatchdogWarningIntervalMs, kWatchdogTimeoutMs, |
| loop_.dispatcher()), |
| driver_(Driver(this, parent)) { |
| mtx_init(&mtx_, mtx_plain); |
| root_ = inspector_.GetRoot().CreateChild("display"); |
| last_vsync_ns_property_ = root_.CreateUint("last_vsync_timestamp_ns", 0); |
| last_vsync_interval_ns_property_ = root_.CreateUint("last_vsync_interval_ns", 0); |
| last_vsync_config_stamp_property_ = |
| root_.CreateUint("last_vsync_config_stamp", kInvalidConfigStamp.value()); |
| |
| last_valid_apply_config_timestamp_ns_property_ = |
| root_.CreateUint("last_valid_apply_config_timestamp_ns", 0); |
| last_valid_apply_config_interval_ns_property_ = |
| root_.CreateUint("last_valid_apply_config_interval_ns", 0); |
| last_valid_apply_config_config_stamp_property_ = |
| root_.CreateUint("last_valid_apply_config_stamp", kInvalidConfigStamp.value()); |
| |
| vsync_stalls_detected_ = root_.CreateUint("vsync_stalls", 0); |
| } |
| |
| Controller::~Controller() { zxlogf(INFO, "Controller::~Controller"); } |
| |
| size_t Controller::TEST_imported_images_count() const { |
| fbl::AutoLock lock(mtx()); |
| size_t virtcon_images = virtcon_client_ ? virtcon_client_->TEST_imported_images_count() : 0; |
| size_t primary_images = primary_client_ ? primary_client_->TEST_imported_images_count() : 0; |
| size_t display_images = 0; |
| for (const auto& display : displays_) { |
| display_images += display.images.size_slow(); |
| } |
| return virtcon_images + primary_images + display_images; |
| } |
| |
| // ControllerInstance methods |
| |
| } // namespace display |
| |
| static zx_status_t display_controller_bind(void* ctx, zx_device_t* parent) { |
| fbl::AllocChecker ac; |
| std::unique_ptr<display::Controller> core(new (&ac) display::Controller(parent)); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| return core->Bind(&core); |
| } |
| |
| static constexpr zx_driver_ops_t display_controller_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = display_controller_bind; |
| return ops; |
| }(); |
| |
| ZIRCON_DRIVER(display_controller, display_controller_ops, "zircon", "0.1"); |