blob: 9f940a211eee3bc38ee958bc66f3abdc5dac7340 [file] [log] [blame]
// 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 "src/graphics/display/drivers/virtio-gpu-display/display-engine.h"
#include <fidl/fuchsia.images2/cpp/wire.h>
#include <fidl/fuchsia.sysmem2/cpp/wire.h>
#include <lib/driver/logging/cpp/logger.h>
#include <lib/stdcompat/span.h>
#include <lib/virtio/driver_utils.h>
#include <lib/zx/bti.h>
#include <lib/zx/result.h>
#include <zircon/assert.h>
#include <zircon/compiler.h>
#include <zircon/errors.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstdint>
#include <cstring>
#include <memory>
#include <utility>
#include <fbl/alloc_checker.h>
#include <fbl/auto_lock.h>
#include <fbl/string_buffer.h>
#include "src/graphics/display/drivers/virtio-gpu-display/imported-image.h"
#include "src/graphics/display/drivers/virtio-gpu-display/virtio-gpu-device.h"
#include "src/graphics/display/drivers/virtio-gpu-display/virtio-pci-device.h"
#include "src/graphics/display/lib/api-protocols/cpp/display-engine-events-interface.h"
#include "src/graphics/display/lib/api-types/cpp/alpha-mode.h"
#include "src/graphics/display/lib/api-types/cpp/config-check-result.h"
#include "src/graphics/display/lib/api-types/cpp/coordinate-transformation.h"
#include "src/graphics/display/lib/api-types/cpp/display-id.h"
#include "src/graphics/display/lib/api-types/cpp/driver-buffer-collection-id.h"
#include "src/graphics/display/lib/api-types/cpp/driver-config-stamp.h"
#include "src/graphics/display/lib/api-types/cpp/driver-image-id.h"
#include "src/graphics/display/lib/api-types/cpp/driver-layer.h"
#include "src/graphics/display/lib/api-types/cpp/engine-info.h"
#include "src/graphics/display/lib/api-types/cpp/image-buffer-usage.h"
#include "src/graphics/display/lib/api-types/cpp/image-metadata.h"
#include "src/graphics/display/lib/api-types/cpp/image-tiling-type.h"
#include "src/graphics/display/lib/api-types/cpp/mode-and-id.h"
#include "src/graphics/display/lib/api-types/cpp/mode-id.h"
#include "src/graphics/display/lib/api-types/cpp/mode.h"
#include "src/graphics/display/lib/api-types/cpp/pixel-format.h"
#include "src/graphics/display/lib/api-types/cpp/power-mode.h"
#include "src/graphics/display/lib/api-types/cpp/rectangle.h"
#include "src/graphics/lib/virtio/virtio-abi.h"
namespace virtio_display {
namespace {
constexpr display::EngineInfo kEngineInfo({
.max_layer_count = 1,
.max_connected_display_count = 1,
.is_capture_supported = false,
});
constexpr display::Mode ToDisplayMode(const display::DisplayTiming& timing) {
int32_t active_width = timing.horizontal_active_px;
int32_t active_height = timing.vertical_active_lines;
int32_t refresh_rate_millihertz = timing.vertical_field_refresh_rate_millihertz();
return display::Mode({
.active_width = active_width,
.active_height = active_height,
.refresh_rate_millihertz = refresh_rate_millihertz,
});
}
constexpr auto kSupportedPixelFormats = std::to_array<display::PixelFormat>(
{display::PixelFormat::kB8G8R8A8, display::PixelFormat::kR8G8B8A8});
constexpr uint32_t kRefreshRateHz = 30;
constexpr display::DisplayId kDisplayId(1);
constexpr display::ModeId kDisplayModeId(1);
} // namespace
display::EngineInfo DisplayEngine::CompleteCoordinatorConnection() {
engine_events_.OnDisplayAdded(kDisplayId, modes_, kSupportedPixelFormats);
return kEngineInfo;
}
zx::result<> DisplayEngine::ImportBufferCollection(
display::DriverBufferCollectionId buffer_collection_id,
fidl::ClientEnd<fuchsia_sysmem2::BufferCollectionToken> buffer_collection_token) {
return imported_images_.ImportBufferCollection(buffer_collection_id,
std::move(buffer_collection_token));
}
zx::result<> DisplayEngine::ReleaseBufferCollection(
display::DriverBufferCollectionId buffer_collection_id) {
return imported_images_.ReleaseBufferCollection(buffer_collection_id);
}
zx::result<display::DriverImageId> DisplayEngine::ImportImage(
const display::ImageMetadata& image_metadata,
display::DriverBufferCollectionId buffer_collection_id, uint32_t buffer_index) {
if (image_metadata.tiling_type() != display::ImageTilingType::kLinear) {
return zx::error(ZX_ERR_INVALID_ARGS);
}
zx::result<display::DriverImageId> image_id_result =
imported_images_.ImportImage(buffer_collection_id, buffer_index);
if (image_id_result.is_error()) {
// ImportImage() already logged the error.
return image_id_result.take_error();
}
const display::DriverImageId image_id = image_id_result.value();
SysmemBufferInfo* sysmem_buffer_info = imported_images_.FindSysmemInfoById(image_id);
ZX_DEBUG_ASSERT(sysmem_buffer_info != nullptr);
ZX_DEBUG_ASSERT_MSG(std::ranges::find(kSupportedPixelFormats, sysmem_buffer_info->pixel_format) !=
kSupportedPixelFormats.end(),
"Sysmem negotiation resulted in unsupported pixel format: %" PRIu32,
sysmem_buffer_info->pixel_format.ValueForLogging());
ZX_DEBUG_ASSERT(sysmem_buffer_info->pixel_format_modifier ==
fuchsia_images2::wire::PixelFormatModifier::kLinear);
size_t image_size = static_cast<size_t>(image_metadata.width()) *
static_cast<size_t>(image_metadata.height()) *
sysmem_buffer_info->pixel_format.EncodingSize();
zx::result<ImportedImage> imported_image_result =
ImportedImage::Create(gpu_device_->bti(), sysmem_buffer_info->image_vmo,
sysmem_buffer_info->image_vmo_offset, image_size);
if (imported_image_result.is_error()) {
// Create() already logged the error.
return imported_image_result.take_error();
}
ImportedImage* imported_image = imported_images_.FindImageById(image_id);
ZX_DEBUG_ASSERT(imported_image != nullptr);
*imported_image = std::move(imported_image_result).value();
zx::result<uint32_t> create_resource_result = gpu_device_->Create2DResource(
image_metadata.width(), image_metadata.height(), sysmem_buffer_info->pixel_format);
if (create_resource_result.is_error()) {
fdf::error("Failed to allocate 2D resource: {}", create_resource_result);
return create_resource_result.take_error();
}
imported_image->set_virtio_resource_id(create_resource_result.value());
zx::result<> attach_result = gpu_device_->AttachResourceBacking(
imported_image->virtio_resource_id(), imported_image->physical_address(), image_size);
if (attach_result.is_error()) {
fdf::error("Failed to attach resource backing store: {}", attach_result);
return attach_result.take_error();
}
return zx::ok(image_id);
}
zx::result<display::DriverCaptureImageId> DisplayEngine::ImportImageForCapture(
display::DriverBufferCollectionId driver_buffer_collection_id, uint32_t index) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
void DisplayEngine::ReleaseImage(display::DriverImageId image_id) {
zx::result result = imported_images_.ReleaseImage(image_id);
if (result.is_error()) {
// ReleaseImage() already logged the error.
// The display coordinator API does not have error reporting for this call.
return;
}
}
display::ConfigCheckResult DisplayEngine::CheckConfiguration(
display::DisplayId display_id, display::ModeId display_mode_id,
cpp20::span<const display::DriverLayer> layers) {
ZX_DEBUG_ASSERT(display_id == kDisplayId);
// TODO(https://fxbug.dev/412450577): Remove the single-layer assumption.
ZX_DEBUG_ASSERT(layers.size() == 1);
// Only the first mode is supported.
if (display_mode_id != kDisplayModeId) {
return display::ConfigCheckResult::kUnsupportedDisplayModes;
}
const display::DriverLayer& layer = layers[0];
const display::Rectangle display_area({
.x = 0,
.y = 0,
.width = static_cast<int32_t>(current_display_.scanout_info.geometry.width),
.height = static_cast<int32_t>(current_display_.scanout_info.geometry.height),
});
if (layer.display_destination() != display_area) {
return display::ConfigCheckResult::kUnsupportedConfig;
}
if (layer.image_source() != layer.display_destination()) {
return display::ConfigCheckResult::kUnsupportedConfig;
}
if (layer.image_metadata().dimensions() != layer.image_source().dimensions()) {
return display::ConfigCheckResult::kUnsupportedConfig;
}
if (layer.alpha_mode() != display::AlphaMode::kDisable) {
return display::ConfigCheckResult::kUnsupportedConfig;
}
if (layer.image_source_transformation() != display::CoordinateTransformation::kIdentity) {
return display::ConfigCheckResult::kUnsupportedConfig;
}
return display::ConfigCheckResult::kOk;
}
void DisplayEngine::ApplyConfiguration(display::DisplayId display_id,
display::ModeId display_mode_id,
cpp20::span<const display::DriverLayer> layers,
display::DriverConfigStamp config_stamp) {
ZX_DEBUG_ASSERT(display_id == kDisplayId);
ZX_DEBUG_ASSERT(display_mode_id == kDisplayModeId);
ZX_DEBUG_ASSERT(layers.size() == 1);
const display::DriverImageId image_id = layers[0].image_id();
const ImportedImage* imported_image = imported_images_.FindImageById(image_id);
if (imported_image == nullptr) {
fdf::error("ApplyConfiguration() used invalid image ID");
return;
}
{
fbl::AutoLock al(&flush_lock_);
latest_framebuffer_resource_id_ = imported_image->virtio_resource_id();
latest_config_stamp_ = config_stamp;
}
}
zx::result<> DisplayEngine::SetBufferCollectionConstraints(
const display::ImageBufferUsage& image_buffer_usage,
display::DriverBufferCollectionId buffer_collection_id) {
ImportedBufferCollection* imported_buffer_collection =
imported_images_.FindBufferCollectionById(buffer_collection_id);
if (imported_buffer_collection == nullptr) {
fdf::warn("Rejected request to set constraints on BufferCollection with unknown ID: {}",
buffer_collection_id.value());
return zx::error(ZX_ERR_NOT_FOUND);
}
// TODO(costan): fidl::Arena may allocate memory and crash. Find a way to get
// control over memory allocation.
fidl::Arena arena;
auto buffer_collection_constraints_builder =
fuchsia_sysmem2::wire::BufferCollectionConstraints::Builder(arena);
buffer_collection_constraints_builder.usage(
fuchsia_sysmem2::wire::BufferUsage::Builder(arena)
.display(fuchsia_sysmem2::wire::kDisplayUsageLayer)
.Build());
buffer_collection_constraints_builder.buffer_memory_constraints(
fuchsia_sysmem2::wire::BufferMemoryConstraints::Builder(arena)
.min_size_bytes(0)
.max_size_bytes(std::numeric_limits<uint32_t>::max())
.physically_contiguous_required(true)
.secure_required(false)
.ram_domain_supported(true)
.cpu_domain_supported(true)
.Build());
static constexpr fuchsia_images2::wire::ColorSpace kColorSpaces[] = {
fuchsia_images2::wire::ColorSpace::kSrgb};
fuchsia_sysmem2::wire::ImageFormatConstraints
image_format_constraints[kSupportedPixelFormats.size()];
for (size_t i = 0; i < kSupportedPixelFormats.size(); ++i) {
image_format_constraints[i] =
fuchsia_sysmem2::wire::ImageFormatConstraints::Builder(arena)
.pixel_format(kSupportedPixelFormats[i].ToFidl())
.pixel_format_modifier(fuchsia_images2::wire::PixelFormatModifier::kLinear)
.color_spaces(kColorSpaces)
.bytes_per_row_divisor(4)
.Build();
}
buffer_collection_constraints_builder.image_format_constraints(image_format_constraints);
fidl::OneWayStatus set_constraints_status =
imported_buffer_collection->sysmem_client()->SetConstraints(
fuchsia_sysmem2::wire::BufferCollectionSetConstraintsRequest::Builder(arena)
.constraints(buffer_collection_constraints_builder.Build())
.Build());
if (!set_constraints_status.ok()) {
fdf::error("SetConstraints() FIDL call failed: {}", set_constraints_status.status_string());
return zx::error(set_constraints_status.status());
}
return zx::ok();
}
zx::result<> DisplayEngine::SetDisplayPowerMode(display::DisplayId display_id,
display::PowerMode power_mode) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
zx::result<> DisplayEngine::StartCapture(display::DriverCaptureImageId capture_image_id) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
zx::result<> DisplayEngine::ReleaseCapture(display::DriverCaptureImageId capture_image_id) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
zx::result<> DisplayEngine::SetMinimumRgb(uint8_t minimum_rgb) {
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
DisplayEngine::DisplayEngine(display::DisplayEngineEventsInterface* engine_events,
fidl::ClientEnd<fuchsia_sysmem2::Allocator> sysmem_client,
std::unique_ptr<VirtioGpuDevice> gpu_device)
: imported_images_(std::move(sysmem_client)),
engine_events_(*engine_events),
gpu_device_(std::move(gpu_device)) {
ZX_DEBUG_ASSERT(engine_events != nullptr);
ZX_DEBUG_ASSERT(gpu_device_);
}
DisplayEngine::~DisplayEngine() = default;
// static
zx::result<std::unique_ptr<DisplayEngine>> DisplayEngine::Create(
fidl::ClientEnd<fuchsia_sysmem2::Allocator> sysmem_client, zx::bti bti,
std::unique_ptr<virtio::Backend> backend,
display::DisplayEngineEventsInterface* engine_events) {
zx::result<std::unique_ptr<VirtioPciDevice>> virtio_device_result =
VirtioPciDevice::Create(std::move(bti), std::move(backend));
if (!virtio_device_result.is_ok()) {
// VirtioPciDevice::Create() logs on error.
return virtio_device_result.take_error();
}
fbl::AllocChecker alloc_checker;
auto gpu_device = fbl::make_unique_checked<VirtioGpuDevice>(
&alloc_checker, std::move(virtio_device_result).value());
if (!alloc_checker.check()) {
fdf::error("Failed to allocate memory for VirtioGpuDevice");
return zx::error(ZX_ERR_NO_MEMORY);
}
auto display_engine = fbl::make_unique_checked<DisplayEngine>(
&alloc_checker, engine_events, std::move(sysmem_client), std::move(gpu_device));
if (!alloc_checker.check()) {
fdf::error("Failed to allocate memory for DisplayEngine");
return zx::error(ZX_ERR_NO_MEMORY);
}
zx_status_t status = display_engine->Init();
if (status != ZX_OK) {
fdf::error("Failed to initialize device");
return zx::error(status);
}
return zx::ok(std::move(display_engine));
}
void DisplayEngine::virtio_gpu_flusher() {
fdf::trace("Entering VirtioGpuFlusher()");
zx_instant_mono_t next_deadline = zx_clock_get_monotonic();
zx_instant_mono_t period = ZX_SEC(1) / kRefreshRateHz;
for (;;) {
zx_nanosleep(next_deadline);
bool fb_change;
{
fbl::AutoLock al(&flush_lock_);
fb_change = displayed_framebuffer_resource_id_ != latest_framebuffer_resource_id_;
displayed_framebuffer_resource_id_ = latest_framebuffer_resource_id_;
displayed_config_stamp_ = latest_config_stamp_;
}
fdf::trace("flushing");
if (fb_change) {
uint32_t resource_id = displayed_framebuffer_resource_id_ ? displayed_framebuffer_resource_id_
: virtio_abi::kInvalidResourceId;
zx::result<> set_scanout_result = gpu_device_->SetScanoutProperties(
current_display_.scanout_id, resource_id, current_display_.scanout_info.geometry.width,
current_display_.scanout_info.geometry.height);
if (set_scanout_result.is_error()) {
fdf::error("Failed to set scanout: {}", set_scanout_result);
continue;
}
}
if (displayed_framebuffer_resource_id_) {
zx::result<> transfer_result = gpu_device_->TransferToHost2D(
displayed_framebuffer_resource_id_, current_display_.scanout_info.geometry.width,
current_display_.scanout_info.geometry.height);
if (transfer_result.is_error()) {
fdf::error("Failed to transfer resource: {}", transfer_result);
continue;
}
zx::result<> flush_result = gpu_device_->FlushResource(
displayed_framebuffer_resource_id_, current_display_.scanout_info.geometry.width,
current_display_.scanout_info.geometry.height);
if (flush_result.is_error()) {
fdf::error("Failed to flush resource: {}", flush_result);
continue;
}
}
{
fbl::AutoLock al(&flush_lock_);
if (displayed_config_stamp_ != display::kInvalidDriverConfigStamp) {
engine_events_.OnDisplayVsync(kDisplayId, zx::time_monotonic(next_deadline),
displayed_config_stamp_);
}
}
next_deadline = zx_time_add_duration(next_deadline, period);
}
}
zx_status_t DisplayEngine::Start() {
fdf::trace("Start()");
// virtio13 5.7.5 "Device Requirements: Device Initialization"
zx::result<fbl::Vector<DisplayInfo>> display_infos_result = gpu_device_->GetDisplayInfo();
if (display_infos_result.is_error()) {
fdf::error("Failed to get display info: {}", display_infos_result);
return display_infos_result.error_value();
}
const DisplayInfo* current_display = FirstValidDisplay(display_infos_result.value());
if (current_display == nullptr) {
fdf::error("Failed to find a usable display");
return ZX_ERR_NOT_FOUND;
}
current_display_ = *current_display;
zx::result<fbl::Vector<uint8_t>> display_edid_bytes_result =
gpu_device_->GetDisplayEdid(current_display->scanout_id);
// EDID support is optional, and the driver can proceed without it.
if (display_edid_bytes_result.is_ok()) {
fbl::Vector<uint8_t> display_edid_bytes = std::move(display_edid_bytes_result).value();
fit::result<const char*, edid::Edid> edid_result = edid::Edid::Create(display_edid_bytes);
if (edid_result.is_error()) {
fdf::warn("Failed to create Edid instance: {}", edid_result.error_value());
} else {
edid_ = std::move(edid_result).value();
}
}
zx::result<fbl::Vector<display::DisplayTiming>> edid_timings_result = DisplayTimingsFromEdid();
zx::result<fbl::Vector<display::ModeAndId>> modes_result =
edid_timings_result.is_ok() ? DisplayModesFromEdidTimings(edid_timings_result.value())
: DisplayModesFromScanoutInfo(current_display_.scanout_info);
if (modes_result.is_error()) {
// Errors have been logged in `DisplayModesFromEdidTimings()` and
// `DisplayModesFromScanoutInfo()` functions above.
return modes_result.error_value();
}
modes_ = std::move(modes_result).value();
fdf::info("Found display at ({}, {}) size {}x{}, flags 0x{:08x}",
current_display_.scanout_info.geometry.x, current_display_.scanout_info.geometry.y,
current_display_.scanout_info.geometry.width,
current_display_.scanout_info.geometry.height, current_display_.scanout_info.flags);
LogEdidBytes();
// Set the mouse cursor position to (0,0); the result is not critical.
zx::result<uint32_t> move_cursor_result =
gpu_device_->SetCursorPosition(current_display_.scanout_id, 0, 0);
if (move_cursor_result.is_error()) {
fdf::warn("Failed to move cursor: {}", move_cursor_result);
}
// Run a worker thread to shove in flush events
auto virtio_gpu_flusher_entry = [](void* arg) {
static_cast<DisplayEngine*>(arg)->virtio_gpu_flusher();
return 0;
};
thrd_create(&flush_thread_, virtio_gpu_flusher_entry, this);
thrd_detach(flush_thread_);
fdf::trace("Start() completed");
return ZX_OK;
}
const DisplayInfo* DisplayEngine::FirstValidDisplay(cpp20::span<const DisplayInfo> display_infos) {
return display_infos.empty() ? nullptr : &display_infos.front();
}
zx_status_t DisplayEngine::Init() {
fdf::trace("Init()");
zx::result<> imported_images_init_result = imported_images_.Initialize();
if (imported_images_init_result.is_error()) {
// Initialize() already logged the error.
return imported_images_init_result.error_value();
}
return ZX_OK;
}
// static
zx::result<fbl::Vector<display::ModeAndId>> DisplayEngine::DisplayModesFromEdidTimings(
std::span<const display::DisplayTiming> edid_timings) {
fbl::Vector<display::ModeAndId> modes;
fbl::AllocChecker alloc_checker;
modes.reserve(edid_timings.size(), &alloc_checker);
if (!alloc_checker.check()) {
fdf::error("Failed to allocate memory for display modes");
return zx::error(ZX_ERR_NO_MEMORY);
}
for (uint16_t i = 0; i < edid_timings.size(); ++i) {
ZX_DEBUG_ASSERT_MSG(modes.size() < edid_timings.size(),
"The push_back() below was not supposed to allocate memory, but it might");
modes.push_back(
display::ModeAndId({.id = display::ModeId(i + 1), .mode = ToDisplayMode(edid_timings[i])}),
&alloc_checker);
ZX_DEBUG_ASSERT_MSG(alloc_checker.check(),
"The push_back() above failed to allocate memory; "
"it was not supposed to allocate at all");
}
return zx::ok(std::move(modes));
}
// static
zx::result<fbl::Vector<display::ModeAndId>> DisplayEngine::DisplayModesFromScanoutInfo(
const virtio_abi::ScanoutInfo& scanout_info) {
fbl::Vector<display::ModeAndId> modes;
fbl::AllocChecker alloc_checker;
modes.push_back(display::ModeAndId({
.id = kDisplayModeId,
.mode = display::Mode({
.active_width = static_cast<int32_t>(scanout_info.geometry.width),
.active_height = static_cast<int32_t>(scanout_info.geometry.height),
.refresh_rate_millihertz = kRefreshRateHz * 1'000,
}),
}),
&alloc_checker);
if (!alloc_checker.check()) {
fdf::error("Failed to allocate memory for display modes");
return zx::error(ZX_ERR_NO_MEMORY);
}
return zx::ok(std::move(modes));
}
zx::result<fbl::Vector<display::DisplayTiming>> DisplayEngine::DisplayTimingsFromEdid() {
if (!edid_.has_value()) {
return zx::error(ZX_ERR_NOT_FOUND);
}
zx::result<fbl::Vector<display::DisplayTiming>> edid_timings_result =
edid_->GetSupportedDisplayTimings();
if (edid_timings_result.is_error()) {
fdf::warn("Failed to get supported display timings from EDID: {}",
edid_timings_result.status_string());
}
return edid_timings_result;
}
void DisplayEngine::LogEdidBytes() {
if (!edid_.has_value()) {
fdf::info("EDID not available");
return;
}
if constexpr (ZX_DEBUG_ASSERT_IMPLEMENTED) {
std::span<const uint8_t> bytes(edid_->edid_bytes(), edid_->edid_length());
// The virtio-gpu implementation in QEmu 9.2 reports a zero-padded EDID that
// takes up the maximum buffer size in the virtio-gpu 1.3 specification.
//
// Trimming the trailing zeros significantly reduces the log output size.
const size_t original_size = bytes.size();
while (!bytes.empty() && bytes.back() == 0) {
bytes = bytes.subspan(0, bytes.size() - 1);
}
fdf::info("--- BEGIN EDID DATA: {} BYTES ---", original_size);
for (size_t line_start = 0; line_start < bytes.size();) {
// The logger truncates lines that exceed 1,024 bytes. We pack the bytes
// as compactly as possible, while meeting the constraint of mapping to
// the C++ initializer syntax used in our unit tests.
static constexpr int kMaxLoggingLineSize = 1020;
// Each byte is logged using 6 characters -- "0xcc, ".
static constexpr int kByteLoggingSize = 6;
static constexpr int kMaxLineBytes = kMaxLoggingLineSize / kByteLoggingSize;
const size_t line_byte_count = std::min<size_t>(kMaxLineBytes, bytes.size() - line_start);
fbl::StringBuffer<1020> line;
for (size_t i = 0; i < line_byte_count; ++i) {
line.AppendPrintf("0x%02x, ", bytes[line_start + i]);
}
fdf::info("{}", line.c_str());
line_start += line_byte_count;
}
fdf::info("--- END EDID DATA: {} BYTES; SKIPPED {} ZERO BYTES ---", original_size,
original_size - bytes.size());
} else {
fdf::info("EDID available, uses {} bytes", edid_->edid_length());
}
}
} // namespace virtio_display