| // Copyright (c) 2012 The Chromium 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 "chromeos/display/output_configurator.h" |
| |
| #include <X11/Xlib.h> |
| #include <X11/extensions/Xrandr.h> |
| #include <X11/extensions/XInput2.h> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/sys_info.h" |
| #include "base/time/time.h" |
| #include "chromeos/display/output_util.h" |
| #include "chromeos/display/real_output_configurator_delegate.h" |
| |
| namespace chromeos { |
| |
| namespace { |
| |
| // The delay to perform configuration after RRNotify. See the comment |
| // in |Dispatch()|. |
| const int64 kConfigureDelayMs = 500; |
| |
| // Returns a string describing |state|. |
| std::string DisplayPowerStateToString(DisplayPowerState state) { |
| switch (state) { |
| case DISPLAY_POWER_ALL_ON: |
| return "ALL_ON"; |
| case DISPLAY_POWER_ALL_OFF: |
| return "ALL_OFF"; |
| case DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON: |
| return "INTERNAL_OFF_EXTERNAL_ON"; |
| case DISPLAY_POWER_INTERNAL_ON_EXTERNAL_OFF: |
| return "INTERNAL_ON_EXTERNAL_OFF"; |
| default: |
| return "unknown (" + base::IntToString(state) + ")"; |
| } |
| } |
| |
| // Returns a string describing |state|. |
| std::string OutputStateToString(OutputState state) { |
| switch (state) { |
| case STATE_INVALID: |
| return "INVALID"; |
| case STATE_HEADLESS: |
| return "HEADLESS"; |
| case STATE_SINGLE: |
| return "SINGLE"; |
| case STATE_DUAL_MIRROR: |
| return "DUAL_MIRROR"; |
| case STATE_DUAL_EXTENDED: |
| return "DUAL_EXTENDED"; |
| } |
| NOTREACHED() << "Unknown state " << state; |
| return "INVALID"; |
| } |
| |
| // Returns a string representation of OutputSnapshot. |
| std::string OutputSnapshotToString( |
| const OutputConfigurator::OutputSnapshot* output) { |
| return base::StringPrintf( |
| "[type=%d, output=%ld, crtc=%ld, mode=%ld, dim=%dx%d]", |
| output->type, |
| output->output, |
| output->crtc, |
| output->current_mode, |
| static_cast<int>(output->width_mm), |
| static_cast<int>(output->height_mm)); |
| } |
| |
| // Returns a string representation of ModeInfo. |
| std::string ModeInfoToString(const OutputConfigurator::ModeInfo* mode) { |
| return base::StringPrintf("[%dx%d %srate=%f]", |
| mode->width, |
| mode->height, |
| mode->interlaced ? "interlaced " : "", |
| mode->refresh_rate); |
| |
| } |
| |
| // Returns the number of outputs in |outputs| that should be turned on, per |
| // |state|. If |output_power| is non-NULL, it is updated to contain the |
| // on/off state of each corresponding entry in |outputs|. |
| int GetOutputPower( |
| const std::vector<OutputConfigurator::OutputSnapshot>& outputs, |
| DisplayPowerState state, |
| std::vector<bool>* output_power) { |
| int num_on_outputs = 0; |
| if (output_power) |
| output_power->resize(outputs.size()); |
| |
| for (size_t i = 0; i < outputs.size(); ++i) { |
| bool internal = outputs[i].is_internal; |
| bool on = state == DISPLAY_POWER_ALL_ON || |
| (state == DISPLAY_POWER_INTERNAL_OFF_EXTERNAL_ON && !internal) || |
| (state == DISPLAY_POWER_INTERNAL_ON_EXTERNAL_OFF && internal); |
| if (output_power) |
| (*output_power)[i] = on; |
| if (on) |
| num_on_outputs++; |
| } |
| return num_on_outputs; |
| } |
| |
| // Determine if there is an "internal" output and how many outputs are |
| // connected. |
| bool IsProjecting( |
| const std::vector<OutputConfigurator::OutputSnapshot>& outputs) { |
| bool has_internal_output = false; |
| int connected_output_count = outputs.size(); |
| for (size_t i = 0; i < outputs.size(); ++i) |
| has_internal_output |= outputs[i].is_internal; |
| |
| // "Projecting" is defined as having more than 1 output connected while at |
| // least one of them is an internal output. |
| return has_internal_output && (connected_output_count > 1); |
| } |
| |
| } // namespace |
| |
| OutputConfigurator::ModeInfo::ModeInfo() |
| : width(0), |
| height(0), |
| interlaced(false), |
| refresh_rate(0.0) {} |
| |
| OutputConfigurator::ModeInfo::ModeInfo(int width, |
| int height, |
| bool interlaced, |
| float refresh_rate) |
| : width(width), |
| height(height), |
| interlaced(interlaced), |
| refresh_rate(refresh_rate) {} |
| |
| OutputConfigurator::CoordinateTransformation::CoordinateTransformation() |
| : x_scale(1.0), |
| x_offset(0.0), |
| y_scale(1.0), |
| y_offset(0.0) {} |
| |
| OutputConfigurator::OutputSnapshot::OutputSnapshot() |
| : output(None), |
| crtc(None), |
| current_mode(None), |
| native_mode(None), |
| mirror_mode(None), |
| selected_mode(None), |
| x(0), |
| y(0), |
| width_mm(0), |
| height_mm(0), |
| is_internal(false), |
| is_aspect_preserving_scaling(false), |
| type(OUTPUT_TYPE_UNKNOWN), |
| touch_device_id(0), |
| display_id(0), |
| has_display_id(false), |
| index(0) {} |
| |
| OutputConfigurator::OutputSnapshot::~OutputSnapshot() {} |
| |
| void OutputConfigurator::TestApi::SendScreenChangeEvent() { |
| XRRScreenChangeNotifyEvent event = {0}; |
| event.type = xrandr_event_base_ + RRScreenChangeNotify; |
| configurator_->Dispatch(reinterpret_cast<const base::NativeEvent>(&event)); |
| } |
| |
| void OutputConfigurator::TestApi::SendOutputChangeEvent(RROutput output, |
| RRCrtc crtc, |
| RRMode mode, |
| bool connected) { |
| XRROutputChangeNotifyEvent event = {0}; |
| event.type = xrandr_event_base_ + RRNotify; |
| event.subtype = RRNotify_OutputChange; |
| event.output = output; |
| event.crtc = crtc; |
| event.mode = mode; |
| event.connection = connected ? RR_Connected : RR_Disconnected; |
| configurator_->Dispatch(reinterpret_cast<const base::NativeEvent>(&event)); |
| } |
| |
| bool OutputConfigurator::TestApi::TriggerConfigureTimeout() { |
| if (configurator_->configure_timer_.get() && |
| configurator_->configure_timer_->IsRunning()) { |
| configurator_->configure_timer_.reset(); |
| configurator_->ConfigureOutputs(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| // static |
| const OutputConfigurator::ModeInfo* OutputConfigurator::GetModeInfo( |
| const OutputSnapshot& output, |
| RRMode mode) { |
| if (mode == None) |
| return NULL; |
| |
| ModeInfoMap::const_iterator it = output.mode_infos.find(mode); |
| if (it == output.mode_infos.end()) { |
| LOG(WARNING) << "Unable to find info about mode " << mode |
| << " for output " << output.output; |
| return NULL; |
| } |
| return &it->second; |
| } |
| |
| // static |
| RRMode OutputConfigurator::FindOutputModeMatchingSize( |
| const OutputSnapshot& output, |
| int width, |
| int height) { |
| RRMode found = None; |
| float best_rate = 0; |
| bool non_interlaced_found = false; |
| for (ModeInfoMap::const_iterator it = output.mode_infos.begin(); |
| it != output.mode_infos.end(); ++it) { |
| RRMode mode = it->first; |
| const ModeInfo& info = it->second; |
| |
| if (info.width == width && info.height == height) { |
| if (info.interlaced) { |
| if (non_interlaced_found) |
| continue; |
| } else { |
| // Reset the best rate if the non interlaced is |
| // found the first time. |
| if (!non_interlaced_found) |
| best_rate = info.refresh_rate; |
| non_interlaced_found = true; |
| } |
| if (info.refresh_rate < best_rate) |
| continue; |
| |
| found = mode; |
| best_rate = info.refresh_rate; |
| } |
| } |
| return found; |
| } |
| |
| OutputConfigurator::OutputConfigurator() |
| : state_controller_(NULL), |
| mirroring_controller_(NULL), |
| is_panel_fitting_enabled_(false), |
| configure_display_(base::SysInfo::IsRunningOnChromeOS()), |
| xrandr_event_base_(0), |
| output_state_(STATE_INVALID), |
| power_state_(DISPLAY_POWER_ALL_ON), |
| next_output_protection_client_id_(1) { |
| } |
| |
| OutputConfigurator::~OutputConfigurator() {} |
| |
| void OutputConfigurator::SetDelegateForTesting(scoped_ptr<Delegate> delegate) { |
| delegate_ = delegate.Pass(); |
| configure_display_ = true; |
| } |
| |
| void OutputConfigurator::SetInitialDisplayPower(DisplayPowerState power_state) { |
| DCHECK_EQ(output_state_, STATE_INVALID); |
| power_state_ = power_state; |
| } |
| |
| void OutputConfigurator::Init(bool is_panel_fitting_enabled) { |
| is_panel_fitting_enabled_ = is_panel_fitting_enabled; |
| if (!configure_display_) |
| return; |
| |
| if (!delegate_) |
| delegate_.reset(new RealOutputConfiguratorDelegate()); |
| } |
| |
| void OutputConfigurator::Start(uint32 background_color_argb) { |
| if (!configure_display_) |
| return; |
| |
| delegate_->GrabServer(); |
| delegate_->InitXRandRExtension(&xrandr_event_base_); |
| |
| UpdateCachedOutputs(); |
| if (cached_outputs_.size() > 1 && background_color_argb) |
| delegate_->SetBackgroundColor(background_color_argb); |
| const OutputState new_state = ChooseOutputState(power_state_); |
| const bool success = EnterStateOrFallBackToSoftwareMirroring( |
| new_state, power_state_); |
| |
| // Force the DPMS on chrome startup as the driver doesn't always detect |
| // that all displays are on when signing out. |
| delegate_->ForceDPMSOn(); |
| delegate_->UngrabServer(); |
| delegate_->SendProjectingStateToPowerManager(IsProjecting(cached_outputs_)); |
| NotifyObservers(success, new_state); |
| } |
| |
| bool OutputConfigurator::ApplyProtections(const DisplayProtections& requests) { |
| for (std::vector<OutputSnapshot>::const_iterator it = cached_outputs_.begin(); |
| it != cached_outputs_.end(); ++it) { |
| RROutput this_id = it->output; |
| uint32_t all_desired = 0; |
| DisplayProtections::const_iterator request_it = requests.find( |
| it->display_id); |
| if (request_it != requests.end()) |
| all_desired = request_it->second; |
| switch (it->type) { |
| case OUTPUT_TYPE_UNKNOWN: |
| return false; |
| // DisplayPort, DVI, and HDMI all support HDCP. |
| case OUTPUT_TYPE_DISPLAYPORT: |
| case OUTPUT_TYPE_DVI: |
| case OUTPUT_TYPE_HDMI: { |
| HDCPState new_desired_state = |
| (all_desired & OUTPUT_PROTECTION_METHOD_HDCP) ? |
| HDCP_STATE_DESIRED : HDCP_STATE_UNDESIRED; |
| if (!delegate_->SetHDCPState(this_id, new_desired_state)) |
| return false; |
| break; |
| } |
| case OUTPUT_TYPE_INTERNAL: |
| case OUTPUT_TYPE_VGA: |
| case OUTPUT_TYPE_NETWORK: |
| // No protections for these types. Do nothing. |
| break; |
| case OUTPUT_TYPE_NONE: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| OutputConfigurator::OutputProtectionClientId |
| OutputConfigurator::RegisterOutputProtectionClient() { |
| if (!configure_display_) |
| return kInvalidClientId; |
| |
| return next_output_protection_client_id_++; |
| } |
| |
| void OutputConfigurator::UnregisterOutputProtectionClient( |
| OutputProtectionClientId client_id) { |
| client_protection_requests_.erase(client_id); |
| |
| DisplayProtections protections; |
| for (ProtectionRequests::const_iterator it = |
| client_protection_requests_.begin(); |
| it != client_protection_requests_.end(); |
| ++it) { |
| for (DisplayProtections::const_iterator it2 = it->second.begin(); |
| it2 != it->second.end(); ++it2) { |
| protections[it2->first] |= it2->second; |
| } |
| } |
| |
| ApplyProtections(protections); |
| } |
| |
| bool OutputConfigurator::QueryOutputProtectionStatus( |
| OutputProtectionClientId client_id, |
| int64 display_id, |
| uint32_t* link_mask, |
| uint32_t* protection_mask) { |
| if (!configure_display_) |
| return false; |
| |
| uint32_t enabled = 0; |
| uint32_t unfulfilled = 0; |
| *link_mask = 0; |
| for (std::vector<OutputSnapshot>::const_iterator it = cached_outputs_.begin(); |
| it != cached_outputs_.end(); ++it) { |
| RROutput this_id = it->output; |
| if (it->display_id != display_id) |
| continue; |
| *link_mask |= it->type; |
| switch (it->type) { |
| case OUTPUT_TYPE_UNKNOWN: |
| return false; |
| // DisplayPort, DVI, and HDMI all support HDCP. |
| case OUTPUT_TYPE_DISPLAYPORT: |
| case OUTPUT_TYPE_DVI: |
| case OUTPUT_TYPE_HDMI: { |
| HDCPState state; |
| if (!delegate_->GetHDCPState(this_id, &state)) |
| return false; |
| if (state == HDCP_STATE_ENABLED) |
| enabled |= OUTPUT_PROTECTION_METHOD_HDCP; |
| else |
| unfulfilled |= OUTPUT_PROTECTION_METHOD_HDCP; |
| break; |
| } |
| case OUTPUT_TYPE_INTERNAL: |
| case OUTPUT_TYPE_VGA: |
| case OUTPUT_TYPE_NETWORK: |
| // No protections for these types. Do nothing. |
| break; |
| case OUTPUT_TYPE_NONE: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| // Don't reveal protections requested by other clients. |
| ProtectionRequests::iterator it = client_protection_requests_.find(client_id); |
| if (it != client_protection_requests_.end()) { |
| uint32_t requested_mask = 0; |
| if (it->second.find(display_id) != it->second.end()) |
| requested_mask = it->second[display_id]; |
| *protection_mask = enabled & ~unfulfilled & requested_mask; |
| } else { |
| *protection_mask = 0; |
| } |
| return true; |
| } |
| |
| bool OutputConfigurator::EnableOutputProtection( |
| OutputProtectionClientId client_id, |
| int64 display_id, |
| uint32_t desired_method_mask) { |
| if (!configure_display_) |
| return false; |
| |
| DisplayProtections protections; |
| for (ProtectionRequests::const_iterator it = |
| client_protection_requests_.begin(); |
| it != client_protection_requests_.end(); |
| ++it) { |
| for (DisplayProtections::const_iterator it2 = it->second.begin(); |
| it2 != it->second.end(); ++it2) { |
| if (it->first == client_id && it2->first == display_id) |
| continue; |
| protections[it2->first] |= it2->second; |
| } |
| } |
| protections[display_id] |= desired_method_mask; |
| |
| if (!ApplyProtections(protections)) |
| return false; |
| |
| if (desired_method_mask == OUTPUT_PROTECTION_METHOD_NONE) { |
| if (client_protection_requests_.find(client_id) != |
| client_protection_requests_.end()) { |
| client_protection_requests_[client_id].erase(display_id); |
| if (client_protection_requests_[client_id].size() == 0) |
| client_protection_requests_.erase(client_id); |
| } |
| } else { |
| client_protection_requests_[client_id][display_id] = desired_method_mask; |
| } |
| |
| return true; |
| } |
| |
| void OutputConfigurator::Stop() { |
| configure_display_ = false; |
| } |
| |
| bool OutputConfigurator::SetDisplayPower(DisplayPowerState power_state, |
| int flags) { |
| if (!configure_display_) |
| return false; |
| |
| VLOG(1) << "SetDisplayPower: power_state=" |
| << DisplayPowerStateToString(power_state) << " flags=" << flags |
| << ", configure timer=" |
| << ((configure_timer_.get() && configure_timer_->IsRunning()) ? |
| "Running" : "Stopped"); |
| if (power_state == power_state_ && !(flags & kSetDisplayPowerForceProbe)) |
| return true; |
| |
| delegate_->GrabServer(); |
| UpdateCachedOutputs(); |
| |
| const OutputState new_state = ChooseOutputState(power_state); |
| bool attempted_change = false; |
| bool success = false; |
| |
| bool only_if_single_internal_display = |
| flags & kSetDisplayPowerOnlyIfSingleInternalDisplay; |
| bool single_internal_display = |
| cached_outputs_.size() == 1 && cached_outputs_[0].is_internal; |
| if (single_internal_display || !only_if_single_internal_display) { |
| success = EnterStateOrFallBackToSoftwareMirroring(new_state, power_state); |
| attempted_change = true; |
| |
| // Force the DPMS on since the driver doesn't always detect that it |
| // should turn on. This is needed when coming back from idle suspend. |
| if (success && power_state != DISPLAY_POWER_ALL_OFF) |
| delegate_->ForceDPMSOn(); |
| } |
| |
| delegate_->UngrabServer(); |
| if (attempted_change) |
| NotifyObservers(success, new_state); |
| return true; |
| } |
| |
| bool OutputConfigurator::SetDisplayMode(OutputState new_state) { |
| if (!configure_display_) |
| return false; |
| |
| VLOG(1) << "SetDisplayMode: state=" << OutputStateToString(new_state); |
| if (output_state_ == new_state) { |
| // Cancel software mirroring if the state is moving from |
| // STATE_DUAL_EXTENDED to STATE_DUAL_EXTENDED. |
| if (mirroring_controller_ && new_state == STATE_DUAL_EXTENDED) |
| mirroring_controller_->SetSoftwareMirroring(false); |
| NotifyObservers(true, new_state); |
| return true; |
| } |
| |
| delegate_->GrabServer(); |
| UpdateCachedOutputs(); |
| const bool success = EnterStateOrFallBackToSoftwareMirroring( |
| new_state, power_state_); |
| delegate_->UngrabServer(); |
| |
| NotifyObservers(success, new_state); |
| return success; |
| } |
| |
| bool OutputConfigurator::Dispatch(const base::NativeEvent& event) { |
| if (!configure_display_) |
| return true; |
| |
| if (event->type - xrandr_event_base_ == RRScreenChangeNotify) { |
| VLOG(1) << "Received RRScreenChangeNotify event"; |
| delegate_->UpdateXRandRConfiguration(event); |
| return true; |
| } |
| |
| // Bail out early for everything except RRNotify_OutputChange events |
| // about an output getting connected or disconnected. |
| if (event->type - xrandr_event_base_ != RRNotify) |
| return true; |
| const XRRNotifyEvent* notify_event = reinterpret_cast<XRRNotifyEvent*>(event); |
| if (notify_event->subtype != RRNotify_OutputChange) |
| return true; |
| const XRROutputChangeNotifyEvent* output_change_event = |
| reinterpret_cast<XRROutputChangeNotifyEvent*>(event); |
| const int action = output_change_event->connection; |
| if (action != RR_Connected && action != RR_Disconnected) |
| return true; |
| |
| const bool connected = (action == RR_Connected); |
| VLOG(1) << "Received RRNotify_OutputChange event:" |
| << " output=" << output_change_event->output |
| << " crtc=" << output_change_event->crtc |
| << " mode=" << output_change_event->mode |
| << " action=" << (connected ? "connected" : "disconnected"); |
| |
| bool found_changed_output = false; |
| for (std::vector<OutputSnapshot>::const_iterator it = cached_outputs_.begin(); |
| it != cached_outputs_.end(); ++it) { |
| if (it->output == output_change_event->output) { |
| if (connected && it->crtc == output_change_event->crtc && |
| it->current_mode == output_change_event->mode) { |
| VLOG(1) << "Ignoring event describing already-cached state"; |
| return true; |
| } |
| found_changed_output = true; |
| break; |
| } |
| } |
| |
| if (!connected && !found_changed_output) { |
| VLOG(1) << "Ignoring event describing already-disconnected output"; |
| return true; |
| } |
| |
| // Connecting/disconnecting a display may generate multiple events. Defer |
| // configuring outputs to avoid grabbing X and configuring displays |
| // multiple times. |
| ScheduleConfigureOutputs(); |
| return true; |
| } |
| |
| base::EventStatus OutputConfigurator::WillProcessEvent( |
| const base::NativeEvent& event) { |
| // XI_HierarchyChanged events are special. There is no window associated with |
| // these events. So process them directly from here. |
| if (configure_display_ && event->type == GenericEvent && |
| event->xgeneric.evtype == XI_HierarchyChanged) { |
| VLOG(1) << "Received XI_HierarchyChanged event"; |
| // Defer configuring outputs to not stall event processing. |
| // This also takes care of same event being received twice. |
| ScheduleConfigureOutputs(); |
| } |
| |
| return base::EVENT_CONTINUE; |
| } |
| |
| void OutputConfigurator::DidProcessEvent(const base::NativeEvent& event) { |
| } |
| |
| void OutputConfigurator::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void OutputConfigurator::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void OutputConfigurator::SuspendDisplays() { |
| // If the display is off due to user inactivity and there's only a single |
| // internal display connected, switch to the all-on state before |
| // suspending. This shouldn't be very noticeable to the user since the |
| // backlight is off at this point, and doing this lets us resume directly |
| // into the "on" state, which greatly reduces resume times. |
| if (power_state_ == DISPLAY_POWER_ALL_OFF) { |
| SetDisplayPower(DISPLAY_POWER_ALL_ON, |
| kSetDisplayPowerOnlyIfSingleInternalDisplay); |
| |
| // We need to make sure that the monitor configuration we just did actually |
| // completes before we return, because otherwise the X message could be |
| // racing with the HandleSuspendReadiness message. |
| delegate_->SyncWithServer(); |
| } |
| } |
| |
| void OutputConfigurator::ResumeDisplays() { |
| // Force probing to ensure that we pick up any changes that were made |
| // while the system was suspended. |
| SetDisplayPower(power_state_, kSetDisplayPowerForceProbe); |
| } |
| |
| void OutputConfigurator::ScheduleConfigureOutputs() { |
| if (configure_timer_.get()) { |
| configure_timer_->Reset(); |
| } else { |
| configure_timer_.reset(new base::OneShotTimer<OutputConfigurator>()); |
| configure_timer_->Start( |
| FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kConfigureDelayMs), |
| this, |
| &OutputConfigurator::ConfigureOutputs); |
| } |
| } |
| |
| void OutputConfigurator::UpdateCachedOutputs() { |
| cached_outputs_ = delegate_->GetOutputs(); |
| |
| // Set |selected_mode| fields. |
| for (size_t i = 0; i < cached_outputs_.size(); ++i) { |
| OutputSnapshot* output = &cached_outputs_[i]; |
| if (output->has_display_id) { |
| int width = 0, height = 0; |
| if (state_controller_ && |
| state_controller_->GetResolutionForDisplayId( |
| output->display_id, &width, &height)) { |
| output->selected_mode = |
| FindOutputModeMatchingSize(*output, width, height); |
| } |
| } |
| // Fall back to native mode. |
| if (output->selected_mode == None) |
| output->selected_mode = output->native_mode; |
| } |
| |
| // Set |mirror_mode| fields. |
| if (cached_outputs_.size() == 2) { |
| bool one_is_internal = cached_outputs_[0].is_internal; |
| bool two_is_internal = cached_outputs_[1].is_internal; |
| int internal_outputs = (one_is_internal ? 1 : 0) + |
| (two_is_internal ? 1 : 0); |
| DCHECK_LT(internal_outputs, 2); |
| LOG_IF(WARNING, internal_outputs == 2) |
| << "Two internal outputs detected."; |
| |
| bool can_mirror = false; |
| for (int attempt = 0; !can_mirror && attempt < 2; ++attempt) { |
| // Try preserving external output's aspect ratio on the first attempt. |
| // If that fails, fall back to the highest matching resolution. |
| bool preserve_aspect = attempt == 0; |
| |
| if (internal_outputs == 1) { |
| if (one_is_internal) { |
| can_mirror = FindMirrorMode(&cached_outputs_[0], &cached_outputs_[1], |
| is_panel_fitting_enabled_, preserve_aspect); |
| } else { |
| DCHECK(two_is_internal); |
| can_mirror = FindMirrorMode(&cached_outputs_[1], &cached_outputs_[0], |
| is_panel_fitting_enabled_, preserve_aspect); |
| } |
| } else { // if (internal_outputs == 0) |
| // No panel fitting for external outputs, so fall back to exact match. |
| can_mirror = FindMirrorMode(&cached_outputs_[0], &cached_outputs_[1], |
| false, preserve_aspect); |
| if (!can_mirror && preserve_aspect) { |
| // FindMirrorMode() will try to preserve aspect ratio of what it |
| // thinks is external display, so if it didn't succeed with one, maybe |
| // it will succeed with the other. This way we will have the correct |
| // aspect ratio on at least one of them. |
| can_mirror = FindMirrorMode(&cached_outputs_[1], &cached_outputs_[0], |
| false, preserve_aspect); |
| } |
| } |
| } |
| } |
| } |
| |
| bool OutputConfigurator::FindMirrorMode(OutputSnapshot* internal_output, |
| OutputSnapshot* external_output, |
| bool try_panel_fitting, |
| bool preserve_aspect) { |
| const ModeInfo* internal_native_info = |
| GetModeInfo(*internal_output, internal_output->native_mode); |
| const ModeInfo* external_native_info = |
| GetModeInfo(*external_output, external_output->native_mode); |
| if (!internal_native_info || !external_native_info) |
| return false; |
| |
| // Check if some external output resolution can be mirrored on internal. |
| // Prefer the modes in the order that X sorts them, assuming this is the order |
| // in which they look better on the monitor. |
| for (ModeInfoMap::const_iterator external_it = |
| external_output->mode_infos.begin(); |
| external_it != external_output->mode_infos.end(); ++external_it) { |
| const ModeInfo& external_info = external_it->second; |
| bool is_native_aspect_ratio = |
| external_native_info->width * external_info.height == |
| external_native_info->height * external_info.width; |
| if (preserve_aspect && !is_native_aspect_ratio) |
| continue; // Allow only aspect ratio preserving modes for mirroring. |
| |
| // Try finding an exact match. |
| for (ModeInfoMap::const_iterator internal_it = |
| internal_output->mode_infos.begin(); |
| internal_it != internal_output->mode_infos.end(); ++internal_it) { |
| const ModeInfo& internal_info = internal_it->second; |
| if (internal_info.width == external_info.width && |
| internal_info.height == external_info.height && |
| internal_info.interlaced == external_info.interlaced) { |
| internal_output->mirror_mode = internal_it->first; |
| external_output->mirror_mode = external_it->first; |
| return true; // Mirror mode found. |
| } |
| } |
| |
| // Try to create a matching internal output mode by panel fitting. |
| if (try_panel_fitting) { |
| // We can downscale by 1.125, and upscale indefinitely. Downscaling looks |
| // ugly, so, can fit == can upscale. Also, internal panels don't support |
| // fitting interlaced modes. |
| bool can_fit = |
| internal_native_info->width >= external_info.width && |
| internal_native_info->height >= external_info.height && |
| !external_info.interlaced; |
| if (can_fit) { |
| RRMode mode = external_it->first; |
| delegate_->AddOutputMode(internal_output->output, mode); |
| internal_output->mode_infos.insert(std::make_pair(mode, external_info)); |
| internal_output->mirror_mode = mode; |
| external_output->mirror_mode = mode; |
| return true; // Mirror mode created. |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| void OutputConfigurator::ConfigureOutputs() { |
| configure_timer_.reset(); |
| |
| delegate_->GrabServer(); |
| UpdateCachedOutputs(); |
| const OutputState new_state = ChooseOutputState(power_state_); |
| const bool success = EnterStateOrFallBackToSoftwareMirroring( |
| new_state, power_state_); |
| delegate_->UngrabServer(); |
| |
| NotifyObservers(success, new_state); |
| delegate_->SendProjectingStateToPowerManager(IsProjecting(cached_outputs_)); |
| } |
| |
| void OutputConfigurator::NotifyObservers(bool success, |
| OutputState attempted_state) { |
| if (success) { |
| FOR_EACH_OBSERVER(Observer, observers_, |
| OnDisplayModeChanged(cached_outputs_)); |
| } else { |
| FOR_EACH_OBSERVER(Observer, observers_, |
| OnDisplayModeChangeFailed(attempted_state)); |
| } |
| } |
| |
| bool OutputConfigurator::EnterStateOrFallBackToSoftwareMirroring( |
| OutputState output_state, |
| DisplayPowerState power_state) { |
| bool success = EnterState(output_state, power_state); |
| if (mirroring_controller_) { |
| bool enable_software_mirroring = false; |
| if (!success && output_state == STATE_DUAL_MIRROR) { |
| if (output_state_ != STATE_DUAL_EXTENDED || power_state_ != power_state) |
| EnterState(STATE_DUAL_EXTENDED, power_state); |
| enable_software_mirroring = success = |
| output_state_ == STATE_DUAL_EXTENDED; |
| } |
| mirroring_controller_->SetSoftwareMirroring(enable_software_mirroring); |
| } |
| return success; |
| } |
| |
| bool OutputConfigurator::EnterState( |
| OutputState output_state, |
| DisplayPowerState power_state) { |
| std::vector<bool> output_power; |
| int num_on_outputs = GetOutputPower( |
| cached_outputs_, power_state, &output_power); |
| VLOG(1) << "EnterState: output=" << OutputStateToString(output_state) |
| << " power=" << DisplayPowerStateToString(power_state); |
| |
| // Framebuffer dimensions. |
| int width = 0, height = 0; |
| std::vector<OutputSnapshot> updated_outputs = cached_outputs_; |
| |
| switch (output_state) { |
| case STATE_INVALID: |
| NOTREACHED() << "Ignoring request to enter invalid state with " |
| << updated_outputs.size() << " connected output(s)"; |
| return false; |
| case STATE_HEADLESS: |
| if (updated_outputs.size() != 0) { |
| LOG(WARNING) << "Ignoring request to enter headless mode with " |
| << updated_outputs.size() << " connected output(s)"; |
| return false; |
| } |
| break; |
| case STATE_SINGLE: { |
| // If there are multiple outputs connected, only one should be turned on. |
| if (updated_outputs.size() != 1 && num_on_outputs != 1) { |
| LOG(WARNING) << "Ignoring request to enter single mode with " |
| << updated_outputs.size() << " connected outputs and " |
| << num_on_outputs << " turned on"; |
| return false; |
| } |
| |
| for (size_t i = 0; i < updated_outputs.size(); ++i) { |
| OutputSnapshot* output = &updated_outputs[i]; |
| output->x = 0; |
| output->y = 0; |
| output->current_mode = output_power[i] ? output->selected_mode : None; |
| |
| if (output_power[i] || updated_outputs.size() == 1) { |
| const ModeInfo* mode_info = |
| GetModeInfo(*output, output->selected_mode); |
| if (!mode_info) |
| return false; |
| if (mode_info->width == 1024 && mode_info->height == 768) { |
| VLOG(1) << "Potentially misdetecting display(1024x768):" |
| << " outputs size=" << updated_outputs.size() |
| << ", num_on_outputs=" << num_on_outputs |
| << ", current size:" << width << "x" << height |
| << ", i=" << i |
| << ", output=" << OutputSnapshotToString(output) |
| << ", mode_info=" << ModeInfoToString(mode_info); |
| } |
| width = mode_info->width; |
| height = mode_info->height; |
| } |
| } |
| break; |
| } |
| case STATE_DUAL_MIRROR: { |
| if (updated_outputs.size() != 2 || |
| (num_on_outputs != 0 && num_on_outputs != 2)) { |
| LOG(WARNING) << "Ignoring request to enter mirrored mode with " |
| << updated_outputs.size() << " connected output(s) and " |
| << num_on_outputs << " turned on"; |
| return false; |
| } |
| |
| if (!updated_outputs[0].mirror_mode) |
| return false; |
| const ModeInfo* mode_info = |
| GetModeInfo(updated_outputs[0], updated_outputs[0].mirror_mode); |
| if (!mode_info) |
| return false; |
| width = mode_info->width; |
| height = mode_info->height; |
| |
| for (size_t i = 0; i < updated_outputs.size(); ++i) { |
| OutputSnapshot* output = &updated_outputs[i]; |
| output->x = 0; |
| output->y = 0; |
| output->current_mode = output_power[i] ? output->mirror_mode : None; |
| if (output->touch_device_id) { |
| // CTM needs to be calculated if aspect preserving scaling is used. |
| // Otherwise, assume it is full screen, and use identity CTM. |
| if (output->mirror_mode != output->native_mode && |
| output->is_aspect_preserving_scaling) { |
| output->transform = GetMirrorModeCTM(*output); |
| mirrored_display_area_ratio_map_[output->touch_device_id] = |
| GetMirroredDisplayAreaRatio(*output); |
| } |
| } |
| } |
| break; |
| } |
| case STATE_DUAL_EXTENDED: { |
| if (updated_outputs.size() != 2 || |
| (num_on_outputs != 0 && num_on_outputs != 2)) { |
| LOG(WARNING) << "Ignoring request to enter extended mode with " |
| << updated_outputs.size() << " connected output(s) and " |
| << num_on_outputs << " turned on"; |
| return false; |
| } |
| |
| for (size_t i = 0; i < updated_outputs.size(); ++i) { |
| OutputSnapshot* output = &updated_outputs[i]; |
| output->x = 0; |
| output->y = height ? height + kVerticalGap : 0; |
| output->current_mode = output_power[i] ? output->selected_mode : None; |
| |
| // Retain the full screen size even if all outputs are off so the |
| // same desktop configuration can be restored when the outputs are |
| // turned back on. |
| const ModeInfo* mode_info = |
| GetModeInfo(updated_outputs[i], updated_outputs[i].selected_mode); |
| if (!mode_info) |
| return false; |
| width = std::max<int>(width, mode_info->width); |
| height += (height ? kVerticalGap : 0) + mode_info->height; |
| } |
| |
| for (size_t i = 0; i < updated_outputs.size(); ++i) { |
| OutputSnapshot* output = &updated_outputs[i]; |
| if (output->touch_device_id) |
| output->transform = GetExtendedModeCTM(*output, width, height); |
| } |
| break; |
| } |
| } |
| |
| // Finally, apply the desired changes. |
| DCHECK_EQ(cached_outputs_.size(), updated_outputs.size()); |
| if (!updated_outputs.empty()) { |
| delegate_->CreateFrameBuffer(width, height, updated_outputs); |
| for (size_t i = 0; i < updated_outputs.size(); ++i) { |
| const OutputSnapshot& output = updated_outputs[i]; |
| if (delegate_->ConfigureCrtc(output.crtc, output.current_mode, |
| output.output, output.x, output.y)) { |
| if (output.touch_device_id) |
| delegate_->ConfigureCTM(output.touch_device_id, output.transform); |
| cached_outputs_[i] = updated_outputs[i]; |
| } else { |
| LOG(WARNING) << "Unable to configure CRTC " << output.crtc << ":" |
| << " mode=" << output.current_mode |
| << " output=" << output.output |
| << " x=" << output.x |
| << " y=" << output.y; |
| } |
| } |
| } |
| |
| output_state_ = output_state; |
| power_state_ = power_state; |
| return true; |
| } |
| |
| OutputState OutputConfigurator::ChooseOutputState( |
| DisplayPowerState power_state) const { |
| int num_on_outputs = GetOutputPower(cached_outputs_, power_state, NULL); |
| switch (cached_outputs_.size()) { |
| case 0: |
| return STATE_HEADLESS; |
| case 1: |
| return STATE_SINGLE; |
| case 2: { |
| if (num_on_outputs == 1) { |
| // If only one output is currently turned on, return the "single" |
| // state so that its native mode will be used. |
| return STATE_SINGLE; |
| } else { |
| // With either both outputs on or both outputs off, use one of the |
| // dual modes. |
| std::vector<int64> display_ids; |
| for (size_t i = 0; i < cached_outputs_.size(); ++i) { |
| // If display id isn't available, switch to extended mode. |
| if (!cached_outputs_[i].has_display_id) |
| return STATE_DUAL_EXTENDED; |
| display_ids.push_back(cached_outputs_[i].display_id); |
| } |
| return state_controller_->GetStateForDisplayIds(display_ids); |
| } |
| } |
| default: |
| NOTREACHED(); |
| } |
| return STATE_INVALID; |
| } |
| |
| OutputConfigurator::CoordinateTransformation |
| OutputConfigurator::GetMirrorModeCTM( |
| const OutputConfigurator::OutputSnapshot& output) { |
| CoordinateTransformation ctm; // Default to identity |
| const ModeInfo* native_mode_info = GetModeInfo(output, output.native_mode); |
| const ModeInfo* mirror_mode_info = GetModeInfo(output, output.mirror_mode); |
| |
| if (!native_mode_info || !mirror_mode_info || |
| native_mode_info->height == 0 || mirror_mode_info->height == 0 || |
| native_mode_info->width == 0 || mirror_mode_info->width == 0) |
| return ctm; |
| |
| float native_mode_ar = static_cast<float>(native_mode_info->width) / |
| static_cast<float>(native_mode_info->height); |
| float mirror_mode_ar = static_cast<float>(mirror_mode_info->width) / |
| static_cast<float>(mirror_mode_info->height); |
| |
| if (mirror_mode_ar > native_mode_ar) { // Letterboxing |
| ctm.x_scale = 1.0; |
| ctm.x_offset = 0.0; |
| ctm.y_scale = mirror_mode_ar / native_mode_ar; |
| ctm.y_offset = (native_mode_ar / mirror_mode_ar - 1.0) * 0.5; |
| return ctm; |
| } |
| if (native_mode_ar > mirror_mode_ar) { // Pillarboxing |
| ctm.y_scale = 1.0; |
| ctm.y_offset = 0.0; |
| ctm.x_scale = native_mode_ar / mirror_mode_ar; |
| ctm.x_offset = (mirror_mode_ar / native_mode_ar - 1.0) * 0.5; |
| return ctm; |
| } |
| |
| return ctm; // Same aspect ratio - return identity |
| } |
| |
| OutputConfigurator::CoordinateTransformation |
| OutputConfigurator::GetExtendedModeCTM( |
| const OutputConfigurator::OutputSnapshot& output, |
| int framebuffer_width, |
| int framebuffer_height) { |
| CoordinateTransformation ctm; // Default to identity |
| const ModeInfo* mode_info = GetModeInfo(output, output.selected_mode); |
| DCHECK(mode_info); |
| if (!mode_info) |
| return ctm; |
| // An example of how to calculate the CTM. |
| // Suppose we have 2 monitors, the first one has size 1366 x 768. |
| // The second one has size 2560 x 1600 |
| // The total size of framebuffer is 2560 x 2428 |
| // where 2428 = 768 + 60 (hidden gap) + 1600 |
| // and the sceond monitor is translated to Point (0, 828) in the |
| // framebuffer. |
| // X will first map input event location to [0, 2560) x [0, 2428), |
| // then apply CTM on it. |
| // So to compute CTM, for monitor1, we have |
| // x_scale = (1366 - 1) / (2560 - 1) |
| // x_offset = 0 / (2560 - 1) |
| // y_scale = (768 - 1) / (2428 - 1) |
| // y_offset = 0 / (2428 -1) |
| // For Monitor 2, we have |
| // x_scale = (2560 - 1) / (2560 - 1) |
| // x_offset = 0 / (2560 - 1) |
| // y_scale = (1600 - 1) / (2428 - 1) |
| // y_offset = 828 / (2428 -1) |
| // See the unittest OutputConfiguratorTest.CTMForMultiScreens. |
| ctm.x_scale = |
| static_cast<float>(mode_info->width - 1) / (framebuffer_width - 1); |
| ctm.x_offset = static_cast<float>(output.x) / (framebuffer_width - 1); |
| ctm.y_scale = |
| static_cast<float>(mode_info->height - 1) / (framebuffer_height - 1); |
| ctm.y_offset = static_cast<float>(output.y) / (framebuffer_height - 1); |
| return ctm; |
| } |
| |
| float OutputConfigurator::GetMirroredDisplayAreaRatio( |
| const OutputConfigurator::OutputSnapshot& output) { |
| float area_ratio = 1.0f; |
| const ModeInfo* native_mode_info = GetModeInfo(output, output.native_mode); |
| const ModeInfo* mirror_mode_info = GetModeInfo(output, output.mirror_mode); |
| |
| if (!native_mode_info || !mirror_mode_info || |
| native_mode_info->height == 0 || mirror_mode_info->height == 0 || |
| native_mode_info->width == 0 || mirror_mode_info->width == 0) |
| return area_ratio; |
| |
| float width_ratio = static_cast<float>(mirror_mode_info->width) / |
| static_cast<float>(native_mode_info->width); |
| float height_ratio = static_cast<float>(mirror_mode_info->height) / |
| static_cast<float>(native_mode_info->height); |
| |
| area_ratio = width_ratio * height_ratio; |
| return area_ratio; |
| } |
| |
| } // namespace chromeos |