| // 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 "runner.h" |
| |
| #include <fbl/algorithm.h> |
| #include <fbl/unique_fd.h> |
| #include <fcntl.h> |
| #include <fuchsia/hardware/camera/c/fidl.h> |
| #include <fuchsia/hardware/display/c/fidl.h> |
| #include <lib/fzl/fdio.h> |
| #include <zircon/device/display-controller.h> |
| |
| #include <cmath> |
| |
| namespace display_test { |
| namespace internal { |
| |
| constexpr const char* kDisplayController = "/dev/class/display-controller/000"; |
| constexpr const char* kCameraDir = "/dev/class/camera"; |
| |
| constexpr uint32_t kDisplayRate = 60; |
| constexpr uint32_t kDisplayFormat = ZX_PIXEL_FORMAT_ARGB_8888; |
| |
| bool is_opaque(uint32_t val) { return (val & 0xff000000) == 0xff000000; } |
| |
| // Src is always premultipled |
| uint32_t multiply_component(uint32_t dest, uint32_t src, uint8_t offset) { |
| uint32_t alpha_comp = src >> 24; |
| uint32_t dest_val = |
| ((((dest >> offset) & 0xff) * (255 - alpha_comp)) + 254) / 255; |
| uint32_t ret = dest_val + ((src >> offset) & 0xff); |
| if (ret > 255) { |
| ret = 255; |
| } |
| return ret << offset; |
| } |
| |
| uint32_t multiply(uint32_t dest, uint32_t src) { |
| if (is_opaque(src)) { |
| return src; |
| } |
| |
| return 0xff000000 | multiply_component(dest, src, 16) | |
| multiply_component(dest, src, 8) | multiply_component(dest, src, 0); |
| } |
| |
| uint8_t clip(float in) { |
| return in < 0 ? 0 : (in > 255 ? 255 : static_cast<uint8_t>(in + .5)); |
| } |
| |
| // Takes 4 bytes of YUY2 and writes 8 bytes of RGBA |
| // TODO(stevensd): RGBA -> YUY2 might be more reliable/efficient |
| void yuy2_to_bgra(uint8_t* yuy2, uint32_t* argb1, uint32_t* argb2) { |
| uint8_t* bgra1 = reinterpret_cast<uint8_t*>(argb1); |
| uint8_t* bgra2 = reinterpret_cast<uint8_t*>(argb2); |
| int u = yuy2[1] - 128; |
| int y1 = yuy2[0]; |
| int v = yuy2[3] - 128; |
| int y2 = yuy2[2]; |
| |
| bgra1[0] = clip(y1 + 1.772 * u); |
| bgra1[1] = clip(y1 - .344 * u - .714 * v); |
| bgra1[2] = clip(y1 + 1.402 * v); |
| bgra1[3] = 0xff; |
| |
| bgra2[0] = clip(y2 + 1.772 * u); |
| bgra2[1] = clip(y2 - .344 * u - .714 * v); |
| bgra2[2] = clip(y2 + 1.402 * v); |
| bgra2[3] = 0xff; |
| } |
| |
| static bool compare_component(uint32_t argb1, uint32_t argb2, uint32_t offset) { |
| uint8_t comp1 = (argb1 >> (offset * 8)) & 0xff; |
| uint8_t comp2 = (argb2 >> (offset * 8)) & 0xff; |
| uint8_t diff = comp1 - comp2; |
| // Unfortunately this is *very* permissive, since it's there are a lot of |
| // places where slight rounding/implementation differences can accumulate |
| // (i.e. the display controller blending, rgb->yuv, yuv->rgb, our blending). |
| return static_cast<uint8_t>(diff + 6) < 13; |
| } |
| |
| static bool compare_colors(uint32_t argb1, uint32_t argb2) { |
| return compare_component(argb1, argb2, 0) && |
| compare_component(argb1, argb2, 1) && |
| compare_component(argb1, argb2, 2); |
| } |
| |
| Runner::Runner(async::Loop* loop) : loop_(loop), runner_context_(this) {} |
| |
| zx_pixel_format_t Runner::format() const { return kDisplayFormat; } |
| |
| zx_status_t Runner::Start(const char* display_name) { |
| zx_status_t status; |
| |
| display_name_ = display_name; |
| |
| camera_watcher_ = fsl::DeviceWatcher::Create( |
| kCameraDir, fit::bind_member(this, &Runner::OnCameraAvailable)); |
| camera_stream_.events().OnFrameAvailable = |
| fit::bind_member(this, &Runner::FrameNotifyCallback); |
| |
| status = loop_->Run(); |
| if (!display_id_ || !camera_setup_ || !display_ownership_) { |
| status = ZX_ERR_INTERNAL; |
| } else if (status == ZX_ERR_CANCELED) { |
| status = ZX_OK; |
| } |
| return status; |
| } |
| |
| void Runner::OnCameraAvailable(int dir_fd, std::string filename) { |
| fbl::unique_fd fd{::openat(dir_fd, filename.c_str(), O_RDWR)}; |
| if (!fd.is_valid()) { |
| printf("Failed to open camera %s\n", filename.c_str()); |
| return; |
| } |
| |
| zx::channel local, remote; |
| zx_status_t status = zx::channel::create(0u, &local, &remote); |
| FXL_CHECK(status == ZX_OK) << "Failed to create channel. status " << status; |
| |
| fzl::FdioCaller dev(std::move(fd)); |
| zx_status_t res = fuchsia_hardware_camera_DeviceGetChannel( |
| dev.borrow_channel(), remote.release()); |
| if (res != ZX_OK) { |
| printf("Failed to obtain channel for camera %s\n", filename.c_str()); |
| return; |
| } |
| |
| camera_control_.Bind(std::move(local), loop_->dispatcher()); |
| |
| camera_control_->GetFormats( |
| 0, fit::bind_member(this, &Runner::GetFormatCallback)); |
| } |
| |
| void Runner::GetFormatCallback(::std::vector<fuchsia::camera::VideoFormat> fmts, |
| uint32_t total_count, zx_status_t status) { |
| uint32_t idx; |
| for (idx = 0; idx < fmts.size(); idx++) { |
| auto& format = fmts[idx]; |
| uint32_t capture_fps = round(1.0 * format.rate.frames_per_sec_numerator / |
| format.rate.frames_per_sec_denominator); |
| if (capture_fps != kDisplayRate) { |
| continue; |
| } |
| |
| if (format.format.pixel_format.type == |
| fuchsia::sysmem::PixelFormatType::YUY2) { |
| break; |
| } |
| |
| ZX_ASSERT(format.format.width % 2 == 0); |
| } |
| |
| if (idx >= fmts.size()) { |
| // Technically we should call GetFormat again, but we can handle that |
| // when it comes it, since this is just a test program. |
| printf("Failed to find matching capture format\n"); |
| return; |
| } |
| |
| // We found a camera, so stop watching the dir for new cameras. |
| camera_watcher_.reset(); |
| |
| auto& format = fmts[idx]; |
| camera_stride_ = format.format.planes[0].bytes_per_row; |
| width_ = format.format.width; |
| height_ = format.format.height; |
| |
| fuchsia::sysmem::BufferCollectionInfo buffer_collection; |
| size_t buffer_size = fbl::round_up( |
| format.format.height * format.format.planes[0].bytes_per_row, 4096u); |
| buffer_collection.buffer_count = kMaxFrames; |
| buffer_collection.vmo_size = buffer_size; |
| buffer_collection.format.set_image(std::move(format.format)); |
| |
| for (uint32_t i = 0; i < kMaxFrames; ++i) { |
| zx::vmo vmo; |
| |
| status = zx::vmo::create(buffer_size, 0, &vmo); |
| ZX_ASSERT(status == ZX_OK); |
| |
| status = zx::vmar::root_self()->map( |
| 0, vmo, 0, buffer_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, |
| reinterpret_cast<zx_vaddr_t*>(camera_buffers_ + i)); |
| ZX_ASSERT(status == ZX_OK); |
| |
| buffer_collection.vmos[i] = std::move(vmo); |
| } |
| |
| zx::eventpair driver_token; |
| status = zx::eventpair::create(0, &stream_token_, &driver_token); |
| ZX_ASSERT(status == ZX_OK); |
| |
| camera_control_->CreateStream(std::move(buffer_collection), format.rate, |
| camera_stream_.NewRequest(), |
| std::move(driver_token)); |
| |
| camera_stream_->Start(); |
| camera_setup_ = true; |
| |
| InitDisplay(); |
| } |
| |
| void Runner::InitDisplay() { |
| zx_status_t status; |
| fbl::unique_fd fd(open(kDisplayController, O_RDWR)); |
| if (!fd.is_valid()) { |
| ZX_ASSERT_MSG(false, "Failed to open display controller"); |
| } |
| |
| zx::channel device_server, device_client; |
| status = zx::channel::create(0, &device_server, &device_client); |
| if (status != ZX_OK) { |
| ZX_ASSERT_MSG(false, "Failed to create device channel %d\n", status); |
| } |
| |
| zx::channel dc_server, dc_client; |
| status = zx::channel::create(0, &dc_server, &dc_client); |
| if (status != ZX_OK) { |
| ZX_ASSERT_MSG(false, "Failed to get create controller channel %d\n", |
| status); |
| } |
| |
| fzl::FdioCaller dev(std::move(fd)); |
| zx_status_t fidl_status = fuchsia_hardware_display_ProviderOpenController( |
| dev.borrow_channel(), device_server.release(), dc_server.release(), |
| &status); |
| if (fidl_status != ZX_OK) { |
| ZX_ASSERT_MSG(false, "Failed to call service handle %d\n", status); |
| } |
| if (status != ZX_OK) { |
| ZX_ASSERT_MSG(false, "Failed to open controller %d\n", status); |
| } |
| |
| display_controller_conn_ = std::move(device_client); |
| |
| if ((status = display_controller_.Bind(std::move(dc_client), |
| loop_->dispatcher())) != ZX_OK) { |
| ZX_ASSERT_MSG(false, "Failed to bind to display controller %d\n", status); |
| } |
| display_controller_.events().DisplaysChanged = |
| fit::bind_member(this, &Runner::OnDisplaysChanged); |
| display_controller_.events().ClientOwnershipChange = |
| fit::bind_member(this, &Runner::OnClientOwnershipChange); |
| display_controller_.events().Vsync = fit::bind_member(this, &Runner::OnVsync); |
| display_controller_.set_error_handler([](zx_status_t status) { |
| ZX_ASSERT_MSG(false, "Display controller failure"); |
| }); |
| |
| calibration_image_a_ = runner_context_.CreateImage(width_, height_); |
| calibration_image_b_ = runner_context_.CreateImage(width_, height_); |
| calibration_layer_ = runner_context_.CreatePrimaryLayer(width_, height_); |
| runner_context_.SetLayers(std::vector<Layer*>({calibration_layer_})); |
| } |
| |
| void Runner::OnDisplaysChanged( |
| ::std::vector<fuchsia::hardware::display::Info> added, |
| ::std::vector<uint64_t> removed) { |
| ZX_ASSERT_MSG(!display_id_, "Display change while tests are running"); |
| for (auto& info : added) { |
| if (strcmp(info.monitor_name.c_str(), display_name_)) { |
| continue; |
| } |
| for (auto& mode : info.modes) { |
| if (mode.horizontal_resolution != width_ || |
| mode.vertical_resolution != height_ || |
| mode.refresh_rate_e2 != kDisplayRate * 100) { |
| continue; |
| } |
| display_id_ = info.id; |
| } |
| } |
| ZX_ASSERT_MSG(display_id_, "Failed to find compatible display"); |
| |
| display_controller_->EnableVsync(true); |
| |
| OnResourceReady(); |
| } |
| |
| void Runner::OnClientOwnershipChange(bool is_owner) { |
| if (is_owner) { |
| display_ownership_ = true; |
| OnResourceReady(); |
| } else { |
| ZX_ASSERT_MSG(false, "Lost display ownership"); |
| } |
| } |
| |
| void Runner::OnShutdownCallback() { ZX_ASSERT_MSG(false, "Camera shutdown"); } |
| |
| void Runner::OnResourceReady() { |
| if (!display_id_ || !camera_setup_ || !display_ownership_) { |
| return; |
| } |
| if (!runner_context_.IsReady()) { |
| return; |
| } |
| |
| if (!test_context_) { |
| loop_->Quit(); |
| return; |
| } else if (!test_context_->IsReady()) { |
| return; |
| } |
| test_running_ = true; |
| |
| // We know the first 2 calibration frames are fine, so skip them |
| CheckFrameConfig(2); |
| } |
| |
| void Runner::Stop() { camera_stream_->Stop(); } |
| |
| Context* Runner::StartTest() { |
| test_context_ = std::unique_ptr<Context>(new Context(this)); |
| |
| ZX_ASSERT_MSG(!test_running_, "Test starting while busy"); |
| test_status_ = kTestStatusUnknown; |
| |
| calibration_layer_->SetImage(calibration_image_a_); |
| runner_context_.ApplyConfig(); |
| calibration_layer_->SetImage(calibration_image_b_); |
| runner_context_.ApplyConfig(); |
| |
| return test_context_.get(); |
| } |
| |
| void Runner::FinishTest(int32_t status) { |
| test_status_ = status; |
| test_running_ = false; |
| loop_->Quit(); |
| } |
| |
| int32_t Runner::CleanupTest() { |
| ZX_ASSERT_MSG(!test_running_, "Tried to finish running test"); |
| |
| test_context_.release(); |
| for (auto id : buffer_ids_) { |
| camera_stream_->ReleaseFrame(id); |
| } |
| buffer_ids_.clear(); |
| display_idx_ = 0; |
| capture_idx_ = 0; |
| for (auto frame : frames_) { |
| for (auto layer : frame) { |
| layer.first->DeleteState(layer.second); |
| } |
| } |
| frames_.clear(); |
| |
| return test_status_; |
| } |
| |
| void Runner::ApplyConfig(std::vector<LayerImpl*> layers) { |
| std::vector<std::pair<LayerImpl*, const void*>> info; |
| for (auto l : layers) { |
| info.push_back(std::make_pair(l, l->ApplyState())); |
| } |
| frames_.push_back(std::move(info)); |
| } |
| |
| void Runner::SendFrameConfig(uint32_t frame_idx) { |
| std::vector<uint64_t> layer_ids; |
| auto& frame = frames_[frame_idx]; |
| for (auto layer : frame) { |
| layer_ids.push_back(layer.first->id()); |
| layer.first->SendState(layer.second); |
| } |
| display_controller_->SetDisplayLayers( |
| display_id_, fidl::VectorPtr<uint64_t>(std::move(layer_ids))); |
| } |
| |
| void Runner::CheckFrameConfig(uint32_t frame_idx) { |
| SendFrameConfig(frame_idx); |
| display_controller_->CheckConfig( |
| frame_idx == frames_.size() - 1, |
| [this, frame_idx]( |
| fuchsia::hardware::display::ConfigResult result, |
| ::std::vector<fuchsia::hardware::display::ClientCompositionOp>) { |
| if (result == fuchsia::hardware::display::ConfigResult::OK) { |
| if (frame_idx + 1 < frames_.size()) { |
| CheckFrameConfig(frame_idx + 1); |
| } else { |
| ApplyFrame(0); |
| } |
| } else { |
| FinishTest(kTestDisplayCheckFail); |
| } |
| }); |
| } |
| |
| void Runner::ApplyFrame(uint32_t frame_idx) { |
| SendFrameConfig(frame_idx); |
| display_controller_->ApplyConfig(); |
| } |
| |
| void Runner::OnVsync(uint64_t display_id, uint64_t timestamp, |
| ::std::vector<uint64_t> image_ids) { |
| if (!test_running_ || image_ids.empty()) { |
| return; |
| } |
| |
| auto& frame = frames_[display_idx_]; |
| unsigned image_idx = 0; |
| for (auto layer : frame) { |
| uint64_t expected_image = layer.first->image_id(layer.second); |
| if (expected_image) { |
| if (image_ids[image_idx++] != expected_image) { |
| if (display_idx_ != 0) { |
| FinishTest(kTestVsyncFail); |
| } |
| return; |
| } |
| } |
| } |
| |
| if (capture_idx_ > 0) { |
| if (display_idx_ + 1 < frames_.size()) { |
| ApplyFrame(++display_idx_); |
| } |
| } |
| } |
| |
| void Runner::FrameNotifyCallback( |
| const fuchsia::camera::FrameAvailableEvent& resp) { |
| static int64_t last_timestamp = 0; |
| last_timestamp = resp.metadata.timestamp; |
| if (test_running_) { |
| if (resp.frame_status != fuchsia::camera::FrameStatus::OK) { |
| if (capture_idx_ != 0 || ++bad_capture_count_ > 5) { |
| ZX_ASSERT_MSG(false, "Bad capture"); |
| FinishTest(kTestCaptureFail); |
| } |
| } else if (capture_idx_ < 2) { |
| if (CheckFrame(capture_idx_, camera_buffers_[resp.buffer_id], true)) { |
| capture_idx_++; |
| } |
| } else { |
| buffer_ids_.push_back(resp.buffer_id); |
| capture_idx_++; |
| |
| if (capture_idx_ == frames_.size()) { |
| for (unsigned i = 2; i < frames_.size(); i++) { |
| if (!CheckFrame(i, camera_buffers_[buffer_ids_[i - 2]], false)) { |
| FinishTest(kTestCaptureMismatch); |
| return; |
| } |
| } |
| FinishTest(kTestOk); |
| } |
| |
| // Don't release the frame, since we need to process it later. |
| return; |
| } |
| } |
| camera_stream_->ReleaseFrame(resp.buffer_id); |
| } |
| |
| bool Runner::CheckFrame(uint32_t frame_idx, uint8_t* capture_ptr, bool quick) { |
| static uint32_t quickcheck_coords[][2] = { |
| {0, 0}, |
| {0, height_ - 1}, |
| {width_ - 2, 0}, |
| {width_ - 2, height_ - 1}, |
| {width_ / 2, height_ / 2}, |
| {UINT32_MAX, UINT32_MAX}, |
| }; |
| uint32_t quick_idx = 0; |
| uint32_t x = 0; |
| uint32_t y = 0; |
| |
| while (y < height_ && x < width_) { |
| uint32_t expected_rgb = 0; |
| bool first_layer = true; |
| bool skip = false; |
| for (auto layer : frames_[frame_idx]) { |
| bool layer_skip; |
| uint32_t layer_color; |
| bool has_pixel = |
| layer.first->GetPixel(layer.second, x, y, &layer_color, &layer_skip); |
| if (!has_pixel) { |
| continue; |
| } |
| |
| if (first_layer) { |
| ZX_ASSERT(is_opaque(layer_color)); |
| first_layer = false; |
| } |
| |
| if (layer_skip) { |
| skip = true; |
| } else if (skip && is_opaque(layer_color)) { |
| skip = false; |
| } |
| |
| expected_rgb = multiply(expected_rgb, layer_color); |
| } |
| |
| // There must be some fully opaque pixel |
| ZX_ASSERT(!first_layer); |
| |
| if (!skip) { |
| uint32_t actual_rgb1, actual_rgb2; |
| // 2 bytes per pixel, so offset by x * 2 |
| yuy2_to_bgra(capture_ptr + (y * camera_stride_) + (x * 2), &actual_rgb1, |
| &actual_rgb2); |
| |
| if (!compare_colors(expected_rgb, actual_rgb1) || |
| !compare_colors(expected_rgb, actual_rgb2)) { |
| if (!quick) { |
| printf("Mismatch (%d, %d) %08x=%08x,%08x\n", x, y, expected_rgb, |
| actual_rgb1, actual_rgb2); |
| } |
| return false; |
| } |
| } |
| |
| if (quick) { |
| ++quick_idx; |
| x = quickcheck_coords[quick_idx][0]; |
| y = quickcheck_coords[quick_idx][1]; |
| } else { |
| // Check a macropixel at a time, so += 2 |
| x += 2; |
| if (x == width_) { |
| x = 0; |
| y++; |
| } |
| } |
| } |
| return true; |
| } |
| |
| } // namespace internal |
| } // namespace display_test |