| // 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 <endian.h> |
| #include <errno.h> |
| #include <fidl/fuchsia.hardware.display.types/cpp/wire.h> |
| #include <fidl/fuchsia.hardware.display/cpp/wire.h> |
| #include <fidl/fuchsia.images2/cpp/wire.h> |
| #include <fidl/fuchsia.sysinfo/cpp/wire.h> |
| #include <fidl/fuchsia.sysmem2/cpp/fidl.h> |
| #include <lib/component/incoming/cpp/protocol.h> |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/stdcompat/span.h> |
| #include <lib/sysmem-version/sysmem-version.h> |
| #include <lib/zircon-internal/align.h> |
| #include <unistd.h> |
| #include <zircon/process.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/types.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <cmath> |
| #include <cstdint> |
| #include <cstdio> |
| #include <cstdlib> |
| #include <cstring> |
| #include <limits> |
| #include <memory> |
| #include <string_view> |
| #include <vector> |
| |
| #include <fbl/string_buffer.h> |
| #include <fbl/vector.h> |
| |
| #include "src/graphics/display/lib/api-types-cpp/buffer-collection-id.h" |
| #include "src/graphics/display/lib/api-types-cpp/display-id.h" |
| #include "src/graphics/display/lib/api-types-cpp/event-id.h" |
| #include "src/graphics/display/lib/api-types-cpp/image-id.h" |
| #include "src/graphics/display/lib/api-types-cpp/layer-id.h" |
| #include "src/graphics/display/testing/client-utils/display.h" |
| #include "src/graphics/display/testing/client-utils/virtual-layer.h" |
| |
| namespace fhd = fuchsia_hardware_display; |
| namespace fhdt = fuchsia_hardware_display_types; |
| namespace images2 = fuchsia_images2; |
| namespace sysmem2 = fuchsia_sysmem2; |
| namespace sysinfo = fuchsia_sysinfo; |
| |
| using display_test::ColorLayer; |
| using display_test::Display; |
| using display_test::PrimaryLayer; |
| using display_test::VirtualLayer; |
| |
| static zx_handle_t device_handle; |
| static fidl::WireSyncClient<fhd::Coordinator> dc; |
| static bool has_ownership; |
| |
| constexpr display::EventId kEventId(13); |
| constexpr display::BufferCollectionId kBufferCollectionId(12); |
| // Use a large ID to avoid conflict with Image IDs allocated by VirtualLayers. |
| constexpr display::ImageId kCaptureImageId(std::numeric_limits<uint64_t>::max()); |
| zx::event client_event_; |
| fidl::SyncClient<sysmem2::BufferCollection> collection_; |
| zx::vmo capture_vmo; |
| |
| enum TestBundle { |
| SIMPLE = 0, // BUNDLE0 |
| FLIP, // BUNDLE1 |
| INTEL, // BUNDLE2 |
| BUNDLE3, |
| BLANK, |
| BUNDLE_COUNT, |
| }; |
| |
| enum Platforms { |
| INTEL_PLATFORM = 0, |
| AMLOGIC_PLATFORM, |
| MEDIATEK_PLATFORM, |
| AEMU_PLATFORM, |
| QEMU_PLATFORM, |
| UNKNOWN_PLATFORM, |
| PLATFORM_COUNT, |
| }; |
| |
| Platforms platform = UNKNOWN_PLATFORM; |
| fbl::StringBuffer<sysinfo::wire::kBoardNameLen> board_name; |
| |
| Platforms GetPlatform(); |
| void Usage(); |
| |
| static bool bind_display(const char* coordinator, fbl::Vector<Display>* displays) { |
| printf("Opening coordinator\n"); |
| zx::result provider = component::Connect<fhd::Provider>(coordinator); |
| if (provider.is_error()) { |
| printf("Failed to open display coordinator (%s)\n", provider.status_string()); |
| return false; |
| } |
| |
| zx::result dc_endpoints = fidl::CreateEndpoints<fhd::Coordinator>(); |
| if (dc_endpoints.is_error()) { |
| printf("Failed to create coordinator channel %d (%s)\n", dc_endpoints.error_value(), |
| dc_endpoints.status_string()); |
| return false; |
| } |
| |
| fidl::WireResult open_response = |
| fidl::WireCall(provider.value())->OpenCoordinatorForPrimary(std::move(dc_endpoints->server)); |
| if (!open_response.ok()) { |
| printf("Failed to call service handle: %s\n", open_response.FormatDescription().c_str()); |
| return false; |
| } |
| if (open_response.value().s != ZX_OK) { |
| printf("Failed to open coordinator %d (%s)\n", open_response.value().s, |
| zx_status_get_string(open_response.value().s)); |
| return false; |
| } |
| |
| dc = fidl::WireSyncClient(std::move(dc_endpoints->client)); |
| |
| class EventHandler : public fidl::WireSyncEventHandler<fhd::Coordinator> { |
| public: |
| EventHandler(fbl::Vector<Display>* displays, bool& has_ownership) |
| : displays_(displays), has_ownership_(has_ownership) {} |
| |
| bool invalid_message() const { return invalid_message_; } |
| |
| void OnDisplaysChanged(fidl::WireEvent<fhd::Coordinator::OnDisplaysChanged>* event) override { |
| for (size_t i = 0; i < event->added.count(); i++) { |
| displays_->push_back(Display(/*info=*/event->added[i])); |
| } |
| } |
| |
| void OnVsync(fidl::WireEvent<fhd::Coordinator::OnVsync>* event) override { |
| invalid_message_ = true; |
| } |
| |
| void OnClientOwnershipChange( |
| fidl::WireEvent<fhd::Coordinator::OnClientOwnershipChange>* event) override { |
| has_ownership_ = event->has_ownership; |
| } |
| |
| private: |
| fbl::Vector<Display>* const displays_; |
| bool& has_ownership_; |
| bool invalid_message_ = false; |
| }; |
| |
| EventHandler event_handler(displays, has_ownership); |
| while (displays->is_empty()) { |
| printf("Waiting for display\n"); |
| if (!dc.HandleOneEvent(event_handler).ok() || event_handler.invalid_message()) { |
| printf("Got unexpected message\n"); |
| return false; |
| } |
| } |
| |
| if (!dc->EnableVsync(true).ok()) { |
| printf("Failed to enable vsync\n"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| Display* find_display(fbl::Vector<Display>& displays, const char* id_str) { |
| errno = 0; |
| uint64_t id_value = strtoul(id_str, nullptr, 10); |
| // strtoul() sets `errno` to non-zero on failure. |
| if (errno != 0) { |
| fprintf(stderr, "Failed to convert display ID \"%s\": %s\n", id_str, strerror(errno)); |
| errno = 0; |
| return nullptr; |
| } |
| display::DisplayId id(id_value); |
| if (id != display::kInvalidDisplayId) { |
| for (auto& d : displays) { |
| if (d.id() == id) { |
| return &d; |
| } |
| } |
| } |
| return nullptr; |
| } |
| |
| bool update_display_layers(const fbl::Vector<std::unique_ptr<VirtualLayer>>& layers, |
| const Display& display, fbl::Vector<display::LayerId>* current_layers) { |
| fbl::Vector<display::LayerId> new_layers; |
| |
| for (auto& layer : layers) { |
| display::LayerId id = layer->id(display.id()); |
| if (id.value() != fhdt::wire::kInvalidDispId) { |
| new_layers.push_back(id); |
| } |
| } |
| |
| bool layer_change = new_layers.size() != current_layers->size(); |
| if (!layer_change) { |
| for (unsigned i = 0; i < new_layers.size(); i++) { |
| if (new_layers[i] != (*current_layers)[i]) { |
| layer_change = true; |
| break; |
| } |
| } |
| } |
| |
| if (layer_change) { |
| current_layers->swap(new_layers); |
| |
| std::vector<fhd::wire::LayerId> current_layers_fidl_id; |
| current_layers_fidl_id.reserve(current_layers->size()); |
| for (const display::LayerId& layer_id : *current_layers) { |
| current_layers_fidl_id.push_back(display::ToFidlLayerId(layer_id)); |
| } |
| if (!dc->SetDisplayLayers( |
| ToFidlDisplayId(display.id()), |
| fidl::VectorView<fhd::wire::LayerId>::FromExternal(current_layers_fidl_id)) |
| .ok()) { |
| printf("Failed to set layers\n"); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| std::optional<fhdt::wire::ConfigStamp> apply_config() { |
| auto result = dc->CheckConfig(false); |
| if (!result.ok()) { |
| printf("Failed to make check call: %s\n", result.FormatDescription().c_str()); |
| return std::nullopt; |
| } |
| |
| if (result.value().res != fhdt::wire::ConfigResult::kOk) { |
| printf("Config not valid (%d)\n", static_cast<uint32_t>(result.value().res)); |
| for (const auto& op : result.value().ops) { |
| printf("Client composition op (display %ld, layer %ld): %hhu\n", op.display_id.value, |
| op.layer_id.value, static_cast<uint8_t>(op.opcode)); |
| } |
| return std::nullopt; |
| } |
| |
| if (!dc->ApplyConfig().ok()) { |
| printf("Apply failed\n"); |
| return std::nullopt; |
| } |
| |
| auto config_stamp_result = dc->GetLatestAppliedConfigStamp(); |
| if (!config_stamp_result.ok()) { |
| printf("GetLatestAppliedConfigStamp failed\n"); |
| return std::nullopt; |
| } |
| |
| return config_stamp_result.value().stamp; |
| } |
| |
| zx_status_t wait_for_vsync(fhdt::wire::ConfigStamp expected_stamp) { |
| class EventHandler : public fidl::WireSyncEventHandler<fhd::Coordinator> { |
| public: |
| explicit EventHandler(fhdt::wire::ConfigStamp expected_stamp) |
| : expected_stamp_(expected_stamp) {} |
| |
| zx_status_t status() const { return status_; } |
| |
| void OnDisplaysChanged(fidl::WireEvent<fhd::Coordinator::OnDisplaysChanged>* event) override { |
| printf("Display disconnected\n"); |
| status_ = ZX_ERR_STOP; |
| } |
| |
| void OnVsync(fidl::WireEvent<fhd::Coordinator::OnVsync>* event) override { |
| // Acknowledge cookie if non-zero |
| if (event->cookie) { |
| // TODO(https://fxbug.dev/42180237) Consider handling the error instead of ignoring it. |
| (void)dc->AcknowledgeVsync(event->cookie); |
| } |
| |
| if (event->applied_config_stamp.value >= expected_stamp_.value) { |
| status_ = ZX_OK; |
| } else { |
| status_ = ZX_ERR_NEXT; |
| } |
| } |
| |
| void OnClientOwnershipChange( |
| fidl::WireEvent<fhd::Coordinator::OnClientOwnershipChange>* event) override { |
| has_ownership = event->has_ownership; |
| status_ = ZX_ERR_NEXT; |
| } |
| |
| private: |
| fhdt::wire::ConfigStamp expected_stamp_; |
| zx_status_t status_ = ZX_OK; |
| }; |
| |
| EventHandler event_handler(expected_stamp); |
| const fidl::Status status = dc.HandleOneEvent(event_handler); |
| if (!status.ok()) { |
| if (status.reason() == fidl::Reason::kUnexpectedMessage) { |
| return ZX_ERR_STOP; |
| } |
| return status.status(); |
| } |
| return event_handler.status(); |
| } |
| |
| zx_status_t set_minimum_rgb(uint8_t min_rgb) { |
| auto resp = dc->SetMinimumRgb(min_rgb); |
| return resp.status(); |
| } |
| |
| zx_status_t capture_setup(Display& display) { |
| // TODO(https://fxbug.dev/42117494): Pull common image setup code into a library |
| |
| // First make sure capture is supported on this platform |
| auto support_resp = dc->IsCaptureSupported(); |
| if (!support_resp.ok()) { |
| printf("%s: %s\n", __func__, support_resp.FormatDescription().c_str()); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| if (!support_resp->value()->supported) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| // Import event used to get notified once capture is completed |
| auto status = zx::event::create(0, &client_event_); |
| if (status != ZX_OK) { |
| printf("Could not create event %d\n", status); |
| return status; |
| } |
| zx::event e2; |
| status = client_event_.duplicate(ZX_RIGHT_SAME_RIGHTS, &e2); |
| if (status != ZX_OK) { |
| printf("Could not duplicate event %d\n", status); |
| return status; |
| } |
| auto event_status = dc->ImportEvent(std::move(e2), display::ToFidlEventId(kEventId)); |
| if (event_status.status() != ZX_OK) { |
| printf("Could not import event: %s\n", event_status.FormatDescription().c_str()); |
| return event_status.status(); |
| } |
| |
| // get connection to sysmem |
| zx::result sysmem_client = component::Connect<sysmem2::Allocator>(); |
| if (sysmem_client.is_error()) { |
| printf("Could not connect to sysmem Allocator %s\n", sysmem_client.status_string()); |
| return sysmem_client.status_value(); |
| } |
| auto sysmem_allocator = fidl::SyncClient(std::move(sysmem_client.value())); |
| |
| // Create and import token |
| zx::result token_endpoints = fidl::CreateEndpoints<sysmem2::BufferCollectionToken>(); |
| if (token_endpoints.is_error()) { |
| printf("Could not create token channel %d\n", token_endpoints.error_value()); |
| return token_endpoints.error_value(); |
| } |
| auto token = fidl::SyncClient(std::move(token_endpoints->client)); |
| |
| // pass token server to sysmem allocator |
| sysmem2::AllocatorAllocateSharedCollectionRequest allocate_shared_request; |
| allocate_shared_request.token_request() = std::move(token_endpoints->server); |
| auto allocate_shared_result = |
| sysmem_allocator->AllocateSharedCollection(std::move(allocate_shared_request)); |
| if (!allocate_shared_result.is_ok()) { |
| printf("Could not pass token to sysmem allocator: %s\n", |
| allocate_shared_result.error_value().FormatDescription().c_str()); |
| return allocate_shared_result.error_value().status(); |
| } |
| |
| // duplicate the token and pass to display driver |
| zx::result token_dup_endpoints = fidl::CreateEndpoints<sysmem2::BufferCollectionToken>(); |
| if (token_dup_endpoints.is_error()) { |
| printf("Could not create duplicate token channel %d\n", token_dup_endpoints.error_value()); |
| return token_dup_endpoints.error_value(); |
| } |
| fidl::SyncClient display_token(std::move(token_dup_endpoints->client)); |
| fuchsia_sysmem2::BufferCollectionTokenDuplicateRequest dup_request; |
| dup_request.rights_attenuation_mask() = ZX_RIGHT_SAME_RIGHTS; |
| dup_request.token_request() = std::move(token_dup_endpoints->server); |
| auto dup_res = token->Duplicate(std::move(dup_request)); |
| if (!dup_res.is_ok()) { |
| printf("Could not duplicate token: %s\n", dup_res.error_value().FormatDescription().c_str()); |
| return dup_res.error_value().status(); |
| } |
| // TODO(https://fxbug.dev/42180237) Consider handling the error instead of ignoring it. |
| (void)token->Sync(); |
| auto import_resp = |
| dc->ImportBufferCollection(display::ToFidlBufferCollectionId(kBufferCollectionId), |
| fidl::ClientEnd<fuchsia_sysmem::BufferCollectionToken>( |
| display_token.TakeClientEnd().TakeChannel())); |
| if (import_resp.status() != ZX_OK) { |
| printf("Could not import token: %s\n", import_resp.FormatDescription().c_str()); |
| return import_resp.status(); |
| } |
| |
| // set buffer constraints |
| fhdt::wire::ImageBufferUsage image_buffer_usage = { |
| .tiling_type = fhdt::wire::kImageTilingTypeCapture, |
| }; |
| auto constraints_resp = dc->SetBufferCollectionConstraints( |
| display::ToFidlBufferCollectionId(kBufferCollectionId), image_buffer_usage); |
| if (constraints_resp.status() != ZX_OK) { |
| printf("Could not set capture constraints %s\n", constraints_resp.FormatDescription().c_str()); |
| return constraints_resp.status(); |
| } |
| |
| // setup our our constraints for buffer to be allocated |
| zx::result collection_endpoints = fidl::CreateEndpoints<sysmem2::BufferCollection>(); |
| if (collection_endpoints.is_error()) { |
| printf("Could not create collection channel %d\n", collection_endpoints.error_value()); |
| return collection_endpoints.error_value(); |
| } |
| // let's return token |
| fuchsia_sysmem2::AllocatorBindSharedCollectionRequest bind_shared_request; |
| bind_shared_request.token() = token.TakeClientEnd(); |
| bind_shared_request.buffer_collection_request() = std::move(collection_endpoints->server); |
| auto bind_resp = sysmem_allocator->BindSharedCollection(std::move(bind_shared_request)); |
| if (!bind_resp.is_ok()) { |
| printf("Could not bind to shared collection: %s\n", |
| bind_resp.error_value().FormatDescription().c_str()); |
| return bind_resp.error_value().status(); |
| } |
| |
| // finally setup our constraints |
| fuchsia_sysmem2::BufferCollectionSetConstraintsRequest set_constraints_request; |
| auto& constraints = set_constraints_request.constraints().emplace(); |
| constraints.usage().emplace().cpu() = sysmem2::kCpuUsageReadOften | sysmem2::kCpuUsageWriteOften; |
| constraints.min_buffer_count_for_camping() = 1; |
| constraints.buffer_memory_constraints().emplace().ram_domain_supported() = true; |
| sysmem2::ImageFormatConstraints& image_constraints = |
| constraints.image_format_constraints().emplace().emplace_back(); |
| if (platform == AMLOGIC_PLATFORM) { |
| image_constraints.pixel_format() = images2::PixelFormat::kB8G8R8; |
| } else { |
| image_constraints.pixel_format() = images2::PixelFormat::kB8G8R8A8; |
| } |
| image_constraints.color_spaces().emplace().emplace_back(images2::ColorSpace::kSrgb); |
| |
| collection_ = fidl::SyncClient(std::move(collection_endpoints->client)); |
| auto set_constraints_result = collection_->SetConstraints(std::move(set_constraints_request)); |
| if (!set_constraints_result.is_ok()) { |
| printf("Could not set buffer constraints: %s\n", |
| set_constraints_result.error_value().FormatDescription().c_str()); |
| return set_constraints_result.error_value().status(); |
| } |
| |
| // wait for allocation |
| auto wait_resp = collection_->WaitForAllBuffersAllocated(); |
| if (!wait_resp.is_ok()) { |
| printf("Wait for buffer allocation failed: %s\n", |
| wait_resp.error_value().FormatDescription().c_str()); |
| zx_status_t status; |
| if (wait_resp.error_value().is_framework_error()) { |
| status = ZX_ERR_INTERNAL; |
| } else { |
| status = sysmem::V1CopyFromV2Error(wait_resp.error_value().domain_error()); |
| } |
| return status; |
| } |
| |
| capture_vmo = |
| std::move(wait_resp.value().buffer_collection_info()->buffers()->at(0).vmo().value()); |
| |
| // import image for capture |
| // TODO(https://fxbug.dev/332521780): Display clients will be required to |
| // pass the captured display's mode information. |
| fhdt::wire::ImageMetadata capture_metadata = { |
| .width = display.mode().horizontal_resolution, |
| .height = display.mode().vertical_resolution, |
| .tiling_type = fhdt::wire::kImageTilingTypeCapture, |
| }; |
| fidl::WireResult import_capture_result = dc->ImportImage( |
| capture_metadata, |
| fhd::wire::BufferId{ |
| .buffer_collection_id = display::ToFidlBufferCollectionId(kBufferCollectionId), |
| .buffer_index = 0, |
| }, |
| display::ToFidlImageId(kCaptureImageId)); |
| if (import_capture_result.status() != ZX_OK) { |
| printf("Failed to start capture: %s\n", import_capture_result.FormatDescription().c_str()); |
| return import_capture_result.status(); |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t capture_start() { |
| // start capture |
| fidl::WireResult start_capture_result = |
| dc->StartCapture(display::ToFidlEventId(kEventId), display::ToFidlImageId(kCaptureImageId)); |
| if (start_capture_result.status() != ZX_OK) { |
| printf("Could not start capture: %s\n", start_capture_result.FormatDescription().c_str()); |
| return start_capture_result.status(); |
| } |
| // wait for capture to complete |
| uint32_t observed; |
| auto event_res = |
| client_event_.wait_one(ZX_EVENT_SIGNALED, zx::deadline_after(zx::sec(1)), &observed); |
| if (event_res == ZX_OK) { |
| client_event_.signal(ZX_EVENT_SIGNALED, 0); |
| } else { |
| printf("capture failed %d\n", event_res); |
| return event_res; |
| } |
| return ZX_OK; |
| } |
| |
| bool AmlogicCompareCapturedImage(cpp20::span<const uint8_t> captured_image, |
| cpp20::span<const uint8_t> input_image, |
| fuchsia_images2::wire::PixelFormat input_image_pixel_format, |
| int height, int width) { |
| assert(input_image_pixel_format == fuchsia_images2::wire::PixelFormat::kB8G8R8A8 || |
| input_image_pixel_format == fuchsia_images2::wire::PixelFormat::kR8G8B8A8); |
| |
| auto expected_image = std::vector<uint8_t>(input_image.begin(), input_image.end()); |
| |
| // Amlogic captured images are always in packed (least-significant) B8G8R8 |
| // (most-siginificant) format. To avoid out-of-order data access, we convert |
| // the endianness of the input image, if they are in (least-significant) |
| // R8G8B8A8 (most-significant) order. |
| if (input_image_pixel_format == fuchsia_images2::wire::PixelFormat::kB8G8R8A8) { |
| for (size_t i = 0; i + 3 < expected_image.size(); i += 4) { |
| std::swap(expected_image[i], expected_image[i + 3]); |
| std::swap(expected_image[i + 1], expected_image[i + 2]); |
| } |
| } |
| |
| // Capture image are of RGB888 formats; each pixel has 3 bytes. |
| constexpr int kCaptureImageBytesPerPixel = 3; |
| const int capture_stride = ZX_ALIGN(width * kCaptureImageBytesPerPixel, 64); |
| // Input image are of R8G8B8A8 or B8R8G8A8 formats; each pixel has 4 bytes. |
| constexpr int kInputImageByetsPerPixel = 4; |
| const int expected_stride = ZX_ALIGN(width * kInputImageByetsPerPixel, 64); |
| |
| // Ignore the first row. It sometimes contains junk (hardware bug). |
| int start_row = 1; |
| int end_row = height; |
| |
| int start_column = 0; |
| // Ignore the last column for Astro only. It contains junk bytes (hardware bug). |
| const bool board_is_astro = |
| std::string_view(board_name.data(), board_name.size()).find("astro") != |
| std::string_view::npos; |
| int end_column = board_is_astro ? (width - 1) : width; |
| |
| for (int row = start_row; row < end_row; row++) { |
| for (int column = start_column; column < end_column; column++) { |
| for (int channel = 0; channel < 3; channel++) { |
| // On expected image, the first byte is alpha channel, so we ignore it. |
| int expected_byte_index = row * expected_stride + column * 4 + 1 + channel; |
| int captured_byte_index = row * capture_stride + column * 3 + channel; |
| |
| constexpr int kColorDifferenceThreshold = 2; |
| if (abs(expected_image[expected_byte_index] - captured_image[captured_byte_index]) > |
| kColorDifferenceThreshold) { |
| printf("Pixel different: (row=%d, col=%d, channel=%d) expected 0x%02x captured 0x%02x\n", |
| row, column, channel, expected_image[expected_byte_index], |
| captured_image[captured_byte_index]); |
| return false; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| |
| bool CompareCapturedImage(cpp20::span<const uint8_t> input_image, |
| fuchsia_images2::wire::PixelFormat input_image_pixel_format, int height, |
| int width) { |
| if (input_image.data() == nullptr) { |
| printf("%s: input image is null\n", __func__); |
| return false; |
| } |
| |
| fzl::VmoMapper mapped_capture_vmo; |
| size_t capture_vmo_size; |
| auto status = capture_vmo.get_size(&capture_vmo_size); |
| if (status != ZX_OK) { |
| printf("capture vmo get size failed %d\n", status); |
| return status; |
| } |
| status = |
| mapped_capture_vmo.Map(capture_vmo, 0, capture_vmo_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE); |
| if (status != ZX_OK) { |
| printf("Could not map capture vmo %d\n", status); |
| return status; |
| } |
| auto* ptr = reinterpret_cast<uint8_t*>(mapped_capture_vmo.start()); |
| zx_cache_flush(ptr, capture_vmo_size, ZX_CACHE_FLUSH_INVALIDATE); |
| |
| if (platform == AMLOGIC_PLATFORM) { |
| return AmlogicCompareCapturedImage( |
| cpp20::span(reinterpret_cast<const uint8_t*>(mapped_capture_vmo.start()), capture_vmo_size), |
| input_image, input_image_pixel_format, height, width); |
| } |
| |
| return !memcmp(input_image.data(), mapped_capture_vmo.start(), capture_vmo_size); |
| } |
| |
| void capture_release() { |
| // TODO(https://fxbug.dev/42180237) Consider handling the error instead of ignoring it. |
| (void)dc->ReleaseImage(display::ToFidlImageId(kCaptureImageId)); |
| // TODO(https://fxbug.dev/42180237) Consider handling the error instead of ignoring it. |
| (void)dc->ReleaseBufferCollection(display::ToFidlBufferCollectionId(kBufferCollectionId)); |
| } |
| |
| void usage(void) { |
| printf( |
| "Usage: display-test [OPTIONS]\n\n" |
| "--controller N : open coordinator N [/dev/class/display-coordinator/N]\n" |
| "--dump : print properties of attached display\n" |
| "--mode-set D N : Set Display D to mode N (use dump option for choices)\n" |
| "--format-set D N : Set Display D to format N (use dump option for choices)\n" |
| "--grayscale : Display images in grayscale mode (default off)\n" |
| "--num-frames N : Run test in N number of frames (default 120)\n" |
| " N can be an integer or 'infinite'\n" |
| "--delay N : Add delay (ms) between Vsync complete and next configuration\n" |
| "--capture : Capture each display frame and verify\n" |
| "--fgcolor 0xaarrggbb : Set foreground color\n" |
| "--bgcolor 0xaarrggbb : Set background color\n" |
| "--preoffsets x,y,z : set preoffsets for color correction\n" |
| "--postoffsets x,y,z : set postoffsets for color correction\n" |
| "--coeff c00,c01,...,,c22 : 3x3 coefficient matrix for color correction\n" |
| "--enable-alpha : Enable per-pixel alpha blending.\n" |
| "--opacity o : Set the opacity of the screen\n" |
| " <o> is a value between [0 1] inclusive\n" |
| "--enable-compression : Enable framebuffer compression.\n" |
| "--apply-config-once : Apply configuration once in single buffer mode.\n" |
| "--clamp-rgb c : Set minimum RGB value [0 255].\n" |
| "--configs-per-vsync n : Number of configs applied per vsync\n" |
| "--pattern pattern : Image pattern to use - 'checkerboard' (default) or 'border'\n" |
| "\nTest Modes:\n\n" |
| "--bundle N : Run test from test bundle N as described below\n\n" |
| " bundle %d: Display a single pattern using single buffer\n" |
| " bundle %d: Flip between two buffers to display a pattern\n" |
| " bundle %d: Run the standard Intel-based display tests. This includes\n" |
| " hardware composition of 1 color layer and 3 primary layers.\n" |
| " The tests include alpha blending, translation, scaling\n" |
| " and rotation\n" |
| " bundle %d: 4 layer hardware composition with alpha blending\n" |
| " and image translation\n" |
| " bundle %d: Blank the screen and sleep for --num-frames.\n" |
| " (default: bundle %d)\n\n" |
| "--help : Show this help message\n", |
| SIMPLE, FLIP, INTEL, BUNDLE3, BLANK, INTEL); |
| } |
| |
| Platforms GetPlatform() { |
| zx::result channel = component::Connect<sysinfo::SysInfo>(); |
| if (channel.is_error()) { |
| return UNKNOWN_PLATFORM; |
| } |
| |
| const fidl::WireResult result = fidl::WireCall(channel.value())->GetBoardName(); |
| if (!result.ok()) { |
| return UNKNOWN_PLATFORM; |
| } |
| const fidl::WireResponse response = result.value(); |
| if (response.status != ZX_OK) { |
| return UNKNOWN_PLATFORM; |
| }; |
| |
| board_name.Clear(); |
| board_name.Append(response.name.get()); |
| |
| printf("Found board %s\n", board_name.c_str()); |
| |
| std::string_view board_name_cmp(board_name); |
| if (board_name_cmp == "x64" || board_name_cmp == "chromebook-x64" || board_name_cmp == "Eve" || |
| board_name_cmp.find("Nocturne") != std::string_view::npos || |
| board_name_cmp.find("NUC") != std::string_view::npos) { |
| return INTEL_PLATFORM; |
| } |
| if (board_name_cmp.find("astro") != std::string_view::npos || |
| board_name_cmp.find("sherlock") != std::string_view::npos || |
| board_name_cmp.find("vim2") != std::string_view::npos || |
| board_name_cmp.find("vim3") != std::string_view::npos || |
| board_name_cmp.find("nelson") != std::string_view::npos || |
| board_name_cmp.find("luis") != std::string_view::npos) { |
| return AMLOGIC_PLATFORM; |
| } |
| if (board_name_cmp.find("cleo") != std::string_view::npos || |
| board_name_cmp.find("mt8167s_ref") != std::string_view::npos) { |
| return MEDIATEK_PLATFORM; |
| } |
| if (board_name_cmp.find("qemu") != std::string_view::npos || |
| board_name_cmp.find("Standard PC (Q35 + ICH9, 2009)") != std::string_view::npos) { |
| return QEMU_PLATFORM; |
| } |
| return UNKNOWN_PLATFORM; |
| } |
| |
| int main(int argc, const char* argv[]) { |
| printf("Running display test\n"); |
| |
| fbl::Vector<Display> displays; |
| fbl::Vector<fbl::Vector<display::LayerId>> display_layers; |
| fbl::Vector<std::unique_ptr<VirtualLayer>> layers; |
| std::optional<int32_t> num_frames = 120; // default to 120 frames. std::nullopt means infinite |
| int32_t delay = 0; |
| bool capture = false; |
| bool verify_capture = false; |
| const char* coordinator = "/dev/class/display-coordinator/000"; |
| |
| platform = GetPlatform(); |
| |
| TestBundle testbundle; |
| switch (platform) { |
| case INTEL_PLATFORM: |
| testbundle = INTEL; |
| break; |
| case AMLOGIC_PLATFORM: |
| testbundle = FLIP; |
| break; |
| case MEDIATEK_PLATFORM: |
| testbundle = BUNDLE3; |
| break; |
| default: |
| testbundle = SIMPLE; |
| } |
| |
| for (int i = 1; i < argc - 1; i++) { |
| if (!strcmp(argv[i], "--controller")) { |
| coordinator = argv[i + 1]; |
| break; |
| } |
| } |
| |
| if (!bind_display(coordinator, &displays)) { |
| usage(); |
| return -1; |
| } |
| |
| if (displays.is_empty()) { |
| printf("No displays available\n"); |
| return 0; |
| } |
| |
| for (unsigned i = 0; i < displays.size(); i++) { |
| display_layers.push_back(fbl::Vector<display::LayerId>()); |
| } |
| |
| argc--; |
| argv++; |
| |
| display_test::Image::Pattern image_pattern = display_test::Image::Pattern::kCheckerboard; |
| uint32_t fgcolor_rgba = 0xffff0000; // red (default) |
| uint32_t bgcolor_rgba = 0xffffffff; // white (default) |
| bool use_color_correction = false; |
| int clamp_rgb = -1; |
| |
| display_test::ColorCorrectionArgs color_correction_args; |
| |
| float alpha_val = std::nanf(""); |
| bool enable_alpha = false; |
| bool enable_compression = false; |
| bool apply_config_once = false; |
| uint32_t configs_per_vsync = 1; |
| |
| while (argc) { |
| if (strcmp(argv[0], "--dump") == 0) { |
| for (auto& display : displays) { |
| display.Dump(); |
| } |
| return 0; |
| } |
| if (strcmp(argv[0], "--mode-set") == 0 || strcmp(argv[0], "--format-set") == 0) { |
| Display* display = find_display(displays, argv[1]); |
| if (!display) { |
| printf("Invalid display \"%s\" for %s\n", argv[1], argv[0]); |
| return -1; |
| } |
| if (strcmp(argv[0], "--mode-set") == 0) { |
| if (!display->set_mode_idx(atoi(argv[2]))) { |
| printf("Invalid mode id\n"); |
| return -1; |
| } |
| } else { |
| if (!display->set_format_idx(atoi(argv[2]))) { |
| printf("Invalid format id\n"); |
| return -1; |
| } |
| } |
| argv += 3; |
| argc -= 3; |
| } else if (strcmp(argv[0], "--grayscale") == 0) { |
| for (auto& d : displays) { |
| d.set_grayscale(true); |
| } |
| argv++; |
| argc--; |
| } else if (strcmp(argv[0], "--num-frames") == 0) { |
| if (strcmp(argv[1], "infinite") == 0) { |
| num_frames = std::nullopt; |
| } else { |
| num_frames = atoi(argv[1]); |
| } |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--controller") == 0) { |
| // We already processed this, skip it. |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--delay") == 0) { |
| delay = atoi(argv[1]); |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--bundle") == 0) { |
| testbundle = static_cast<TestBundle>(atoi(argv[1])); |
| if (testbundle >= BUNDLE_COUNT || testbundle < 0) { |
| printf("Invalid test bundle selected\n"); |
| usage(); |
| return -1; |
| } |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--capture") == 0) { |
| capture = true; |
| verify_capture = true; |
| argv += 1; |
| argc -= 1; |
| } else if (strcmp(argv[0], "--clamp-rgb") == 0) { |
| clamp_rgb = atoi(argv[1]); |
| if (clamp_rgb < 0 || clamp_rgb > 255) { |
| usage(); |
| return -1; |
| } |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--fgcolor") == 0) { |
| fgcolor_rgba = static_cast<uint32_t>(strtoul(argv[1], nullptr, 16)); |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--bgcolor") == 0) { |
| bgcolor_rgba = static_cast<uint32_t>(strtoul(argv[1], nullptr, 16)); |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--preoffsets") == 0) { |
| sscanf(argv[1], "%f,%f,%f", &color_correction_args.preoffsets[0], |
| &color_correction_args.preoffsets[1], &color_correction_args.preoffsets[2]); |
| use_color_correction = true; |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--postoffsets") == 0) { |
| sscanf(argv[1], "%f,%f,%f", &color_correction_args.postoffsets[0], |
| &color_correction_args.postoffsets[1], &color_correction_args.postoffsets[2]); |
| use_color_correction = true; |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--coeff") == 0) { |
| sscanf(argv[1], "%f,%f,%f,%f,%f,%f,%f,%f,%f", &color_correction_args.coeff[0], |
| &color_correction_args.coeff[1], &color_correction_args.coeff[2], |
| &color_correction_args.coeff[3], &color_correction_args.coeff[4], |
| &color_correction_args.coeff[5], &color_correction_args.coeff[6], |
| &color_correction_args.coeff[7], &color_correction_args.coeff[8]); |
| use_color_correction = true; |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--enable-alpha") == 0) { |
| enable_alpha = true; |
| argv += 1; |
| argc -= 1; |
| } else if (strcmp(argv[0], "--opacity") == 0) { |
| enable_alpha = true; |
| alpha_val = std::stof(argv[1]); |
| if (alpha_val < 0 || alpha_val > 1) { |
| printf("Invalid alpha value. Must be between 0 and 1\n"); |
| usage(); |
| return -1; |
| } |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--enable-compression") == 0) { |
| enable_compression = true; |
| argv += 1; |
| argc -= 1; |
| } else if (strcmp(argv[0], "--apply-config-once") == 0) { |
| apply_config_once = true; |
| argv += 1; |
| argc -= 1; |
| } else if (strcmp(argv[0], "--configs-per-vsync") == 0) { |
| configs_per_vsync = atoi(argv[1]); |
| argv += 2; |
| argc -= 2; |
| } else if (strcmp(argv[0], "--pattern") == 0) { |
| if (strcmp(argv[1], "checkerboard") == 0) { |
| image_pattern = display_test::Image::Pattern::kCheckerboard; |
| } else if (strcmp(argv[1], "border") == 0) { |
| image_pattern = display_test::Image::Pattern::kBorder; |
| } else { |
| printf("Invalid image pattern \"%s\".\n", argv[1]); |
| usage(); |
| return 0; |
| } |
| argv += 2; |
| argc -= 2; |
| |
| } else if (strcmp(argv[0], "--help") == 0) { |
| usage(); |
| return 0; |
| } else { |
| printf("Unrecognized argument \"%s\"\n", argv[0]); |
| usage(); |
| return -1; |
| } |
| } |
| |
| // TODO(https://fxbug.dev/42076494): AFBC compression test doesn't work on |
| // amlogic-display; the AFBC encoding format supported by amlogic-display |
| // driver is not compatible with the AFBC buffer formats and generation logic |
| // used by this test. Once amlogic-display supports non-tiled-header formats |
| // and AFBC format switching on the fly, this test will work again and we can |
| // delete the error message below. |
| if (enable_compression) { |
| fprintf(stderr, |
| "AFBC compression test is not working for amlogic-display. " |
| "The --enable-compression option will be re-enabled once " |
| "https://fxbug.dev/42076494 is fixed.\n"); |
| return -1; |
| } |
| |
| if (use_color_correction) { |
| for (auto& d : displays) { |
| d.apply_color_correction(true); |
| } |
| } |
| |
| if (capture && capture_setup(displays[0]) != ZX_OK) { |
| printf("Could not setup capture\n"); |
| capture = false; |
| } |
| |
| if (clamp_rgb != -1) { |
| if (set_minimum_rgb(static_cast<uint8_t>(clamp_rgb)) != ZX_OK) { |
| printf("Warning: RGB Clamping Not Supported!\n"); |
| } |
| } |
| |
| // Call apply_config for each frame by default. |
| std::optional<int32_t> max_apply_configs(num_frames); |
| |
| fbl::AllocChecker ac; |
| if (testbundle == INTEL) { |
| // Intel only supports 90/270 rotation for Y-tiled images, so enable it for testing. |
| constexpr fuchsia_images2::wire::PixelFormatModifier kIntelYTilingModifier = |
| fuchsia_images2::wire::PixelFormatModifier::kIntelI915YTiled; |
| |
| // Color layer which covers all displays |
| std::unique_ptr<ColorLayer> layer0 = fbl::make_unique_checked<ColorLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layers.push_back(std::move(layer0)); |
| |
| // Layer which covers all displays and uses page flipping. |
| std::unique_ptr<PrimaryLayer> layer1 = fbl::make_unique_checked<PrimaryLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layer1->SetLayerFlipping(true); |
| layer1->SetAlpha(true, .75); |
| layer1->SetFormatModifier(kIntelYTilingModifier); |
| layers.push_back(std::move(layer1)); |
| |
| // Layer which covers the left half of the of the first display |
| // and toggles on and off every frame. |
| std::unique_ptr<PrimaryLayer> layer2 = |
| fbl::make_unique_checked<PrimaryLayer>(&ac, &displays[0]); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layer2->SetImageDimens(displays[0].mode().horizontal_resolution / 2, |
| displays[0].mode().vertical_resolution); |
| layer2->SetLayerToggle(true); |
| layer2->SetScaling(true); |
| layer2->SetFormatModifier(kIntelYTilingModifier); |
| layers.push_back(std::move(layer2)); |
| |
| // Layer which is smaller than the display and bigger than its image |
| // and which animates back and forth across all displays and also |
| // its src image and also rotates. |
| std::unique_ptr<PrimaryLayer> layer3 = fbl::make_unique_checked<PrimaryLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| // Width is the larger of disp_width/2, display_height/2, but we also need |
| // to make sure that it's less than the smaller display dimension. |
| uint32_t width = std::min( |
| std::max(displays[0].mode().vertical_resolution / 2, |
| displays[0].mode().horizontal_resolution / 2), |
| std::min(displays[0].mode().vertical_resolution, displays[0].mode().horizontal_resolution)); |
| uint32_t height = std::min(displays[0].mode().vertical_resolution / 2, |
| displays[0].mode().horizontal_resolution / 2); |
| layer3->SetImageDimens(width * 2, height); |
| layer3->SetDestFrame(width, height); |
| layer3->SetSrcFrame(width, height); |
| layer3->SetPanDest(true); |
| layer3->SetPanSrc(true); |
| layer3->SetRotates(true); |
| layer3->SetFormatModifier(kIntelYTilingModifier); |
| layers.push_back(std::move(layer3)); |
| } else if (testbundle == BUNDLE3) { |
| // Mediatek display test |
| uint32_t width = displays[0].mode().horizontal_resolution; |
| uint32_t height = displays[0].mode().vertical_resolution; |
| std::unique_ptr<PrimaryLayer> layer1 = fbl::make_unique_checked<PrimaryLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layer1->SetAlpha(true, (float)0.2); |
| layer1->SetImageDimens(width, height); |
| layer1->SetSrcFrame(width / 2, height / 2); |
| layer1->SetDestFrame(width / 2, height / 2); |
| layer1->SetPanSrc(true); |
| layer1->SetPanDest(true); |
| layers.push_back(std::move(layer1)); |
| |
| // Layer which covers the left half of the of the first display |
| // and toggles on and off every frame. |
| float alpha2 = (float)0.5; |
| std::unique_ptr<PrimaryLayer> layer2 = fbl::make_unique_checked<PrimaryLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layer2->SetLayerFlipping(true); |
| layer2->SetAlpha(true, alpha2); |
| layers.push_back(std::move(layer2)); |
| |
| float alpha3 = (float)0.2; |
| std::unique_ptr<PrimaryLayer> layer3 = fbl::make_unique_checked<PrimaryLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layer3->SetAlpha(true, alpha3); |
| layers.push_back(std::move(layer3)); |
| |
| std::unique_ptr<PrimaryLayer> layer4 = fbl::make_unique_checked<PrimaryLayer>(&ac, displays); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| layer4->SetAlpha(true, (float)0.3); |
| layers.push_back(std::move(layer4)); |
| } else if (testbundle == FLIP) { |
| // Amlogic display test |
| std::unique_ptr<PrimaryLayer> layer1 = fbl::make_unique_checked<PrimaryLayer>( |
| &ac, displays, image_pattern, fgcolor_rgba, bgcolor_rgba); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| if (enable_alpha) { |
| layer1->SetAlpha(true, alpha_val); |
| } |
| layer1->SetLayerFlipping(true); |
| if (enable_compression) { |
| layer1->SetFormatModifier(fuchsia_images2::wire::PixelFormatModifier::kArmAfbc16X16); |
| } |
| layers.push_back(std::move(layer1)); |
| } else if (testbundle == SIMPLE) { |
| // Simple display test |
| bool mirrors = true; |
| std::unique_ptr<PrimaryLayer> layer1 = fbl::make_unique_checked<PrimaryLayer>( |
| &ac, displays, image_pattern, fgcolor_rgba, bgcolor_rgba, mirrors); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| if (enable_compression) { |
| layer1->SetFormatModifier(fuchsia_images2::wire::PixelFormatModifier::kArmAfbc16X16); |
| } |
| if (apply_config_once) { |
| max_apply_configs = 1; |
| } |
| layers.push_back(std::move(layer1)); |
| } else if (testbundle == BLANK) { |
| // 0 layers, applied one time |
| max_apply_configs = 1; |
| } |
| |
| printf("Initializing layers\n"); |
| for (auto& layer : layers) { |
| if (!layer->Init(dc)) { |
| printf("Layer init failed\n"); |
| return -1; |
| } |
| } |
| |
| for (auto& display : displays) { |
| display.Init(dc, color_correction_args); |
| } |
| |
| if (capture && layers.size() != 1) { |
| printf("Capture disabled: verification only works for single-layer display tests\n"); |
| verify_capture = false; |
| } |
| |
| printf("Starting rendering\n"); |
| if (capture) { |
| printf("Capturing every frame. Verification is %s\n", verify_capture ? "enabled" : "disabled"); |
| } |
| bool capture_result = true; |
| for (int i = 0; !num_frames || i < num_frames; i++) { |
| for (auto& layer : layers) { |
| // Step before waiting, since not every layer is used every frame |
| // so we won't necessarily need to wait. |
| layer->StepLayout(i); |
| |
| if (!layer->WaitForReady()) { |
| printf("Buffer failed to become free\n"); |
| return -1; |
| } |
| |
| layer->clear_done(); |
| layer->SendLayout(dc); |
| } |
| |
| for (unsigned i = 0; i < displays.size(); i++) { |
| if (!update_display_layers(layers, displays[i], &display_layers[i])) { |
| return -1; |
| } |
| } |
| |
| // This delay is used to skew the timing between vsync and ApplyConfiguration |
| // in order to observe any tearing effects |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(delay))); |
| |
| fhdt::wire::ConfigStamp expected_stamp = {.value = fhdt::wire::kInvalidConfigStampValue}; |
| if (!max_apply_configs || i < max_apply_configs) { |
| for (uint32_t cpv = 0; cpv < configs_per_vsync; cpv++) { |
| auto maybe_expected_stamp = apply_config(); |
| if (!maybe_expected_stamp.has_value()) { |
| return -1; |
| } else { |
| expected_stamp = *maybe_expected_stamp; |
| } |
| } |
| } |
| |
| for (auto& layer : layers) { |
| layer->Render(i); |
| } |
| |
| zx_status_t status = ZX_OK; |
| while (layers.size() != 0 && (status = wait_for_vsync(expected_stamp)) == ZX_ERR_NEXT) { |
| } |
| ZX_ASSERT(status == ZX_OK); |
| if (capture) { |
| // capture has been requested. |
| status = capture_start(); |
| if (status != ZX_OK) { |
| printf("Capture start failed %d\n", status); |
| capture_release(); |
| capture = false; |
| break; |
| } |
| if (verify_capture && |
| !CompareCapturedImage( |
| cpp20::span(reinterpret_cast<const uint8_t*>(layers[0]->GetCurrentImageBuf()), |
| layers[0]->GetCurrentImageSize()), |
| /*input_image_pixel_format=*/displays[0].format(), |
| /*height=*/displays[0].mode().vertical_resolution, |
| /*width=*/displays[0].mode().horizontal_resolution)) { |
| capture_result = false; |
| break; |
| } |
| } |
| } |
| |
| printf("Done rendering\n"); |
| |
| if (capture) { |
| printf("Capture completed\n"); |
| if (verify_capture) { |
| if (capture_result) { |
| printf("Capture Verification Passed\n"); |
| } else { |
| printf("Capture Verification Failed!\n"); |
| } |
| } |
| capture_release(); |
| } |
| zx_handle_close(device_handle); |
| |
| return 0; |
| } |