// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <chrome-color-management-client-protocol.h>
#include <drm_fourcc.h>
#include <fcntl.h>
#include <gbm.h>
#include <sys/mman.h>
#include <cstdint>
#include <memory>
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/message_loop/message_pump_type.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/single_thread_task_executor.h"
#include "components/exo/wayland/clients/client_base.h"
#include "components/exo/wayland/clients/client_helper.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPoint.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/core/SkTileMode.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
namespace exo {
namespace wayland {
namespace clients {
namespace {
using DrawFunction = base::OnceCallback<void(const gfx::Size&, SkCanvas*)>;
void DrawToGbm(const gfx::Size& size, DrawFunction draw_func, gbm_bo* bo) {
CHECK_EQ(gbm_bo_get_plane_count(bo), 1);
uint32_t stride_bytes;
void* mapped_data;
void* void_data =
gbm_bo_map(bo, 0, 0, size.width(), size.height(), GBM_BO_TRANSFER_WRITE,
&stride_bytes, &mapped_data);
CHECK_NE(void_data, MAP_FAILED);
CHECK_EQ(stride_bytes % 4, 0u);
auto image_info =
SkImageInfo::Make(size.width(), size.height(), kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType);
auto canvas = SkCanvas::MakeRasterDirect(image_info, void_data, stride_bytes);
canvas->clear(SK_ColorBLACK);
std::move(draw_func).Run(size, canvas.get());
gbm_bo_unmap(bo, mapped_data);
}
void DrawColorGradients(const gfx::Size& size, SkCanvas* canvas) {
constexpr std::array<SkColor, 4> kGradientColors{
SK_ColorRED,
SK_ColorBLUE,
SK_ColorGREEN,
SK_ColorWHITE,
};
int bar_pixels = size.height() / kGradientColors.size();
for (size_t i = 0; i < kGradientColors.size(); i++) {
SkPoint points[2] = {SkPoint::Make(0, 0),
SkPoint::Make(size.width(), bar_pixels)};
SkColor colors[2] = {SK_ColorBLACK, kGradientColors[i]};
SkPaint paint;
paint.setShader(SkGradientShader::MakeLinear(
points, colors, nullptr, 2, SkTileMode::kClamp, 0, nullptr));
canvas->drawRect(
SkRect::MakeXYWH(0, i * bar_pixels, size.width(), bar_pixels), paint);
}
}
void DrawWhiteBox(const gfx::Size& size, SkCanvas* canvas) {
SkRect rect = SkRect::MakeXYWH(size.width() / 4, size.height() / 4,
size.width() / 2, size.height() / 2);
SkPaint paint;
paint.setColor(SK_ColorWHITE);
canvas->drawRect(rect, paint);
}
void FrameCallback(void* data, wl_callback* callback, uint32_t time) {
bool* callback_pending = static_cast<bool*>(data);
*callback_pending = false;
}
std::string ColorSpaceCreationErrorToString(uint32_t error) {
switch (error) {
case ZCR_COLOR_SPACE_CREATOR_V1_CREATION_ERROR_MALFORMED_ICC:
return "malformed ICC profile";
case ZCR_COLOR_SPACE_CREATOR_V1_CREATION_ERROR_BAD_ICC:
return "ICC profile does not meet requirements";
case ZCR_COLOR_SPACE_CREATOR_V1_CREATION_ERROR_BAD_PRIMARIES:
return "bad primaries";
case ZCR_COLOR_SPACE_CREATOR_V1_CREATION_ERROR_BAD_WHITEPOINT:
return "bad whitepoint";
default:
return "<unknown error>";
}
}
const zcr_color_management_output_v1_listener kOutputColorMangerListener = {
.color_space_changed =
[](void* data, struct zcr_color_management_output_v1* color_output) {},
.extended_dynamic_range =
[](void* data,
struct zcr_color_management_output_v1* color_output,
uint32_t value) {}};
const zcr_color_management_surface_v1_listener kSurfaceColorManagerListener = {
.preferred_color_space =
[](void* data,
struct zcr_color_management_surface_v1* color_surface,
struct wl_output* output) {}};
} // namespace
class HdrClient : public ClientBase {
public:
HdrClient() = default;
HdrClient(const HdrClient&) = delete;
HdrClient& operator=(const HdrClient&) = delete;
void InitColorManagement();
void Run(const ClientBase::InitParams& params,
bool test_white_levels,
uint32_t primary1,
uint32_t transfer1,
uint32_t primary2,
uint32_t transfer2);
private:
std::unique_ptr<zcr_color_space_v1> CreateColorSpace(uint32_t color,
uint32_t transfer);
std::unique_ptr<zcr_color_management_output_v1> color_management_output_;
std::unique_ptr<zcr_color_management_surface_v1> color_management_surface_;
};
void HdrClient::InitColorManagement() {
CHECK(globals_.color_manager)
<< "Server doesn't support zcr_color_manager_v1.";
// This is only for the single output scenario.
color_management_output_.reset(
zcr_color_manager_v1_get_color_management_output(
globals_.color_manager.get(), globals_.outputs.back().get()));
CHECK(color_management_output_) << "Can't create color management output.";
zcr_color_management_output_v1_add_listener(
color_management_output_.get(), &kOutputColorMangerListener, this);
color_management_surface_.reset(
zcr_color_manager_v1_get_color_management_surface(
globals_.color_manager.get(), surface_.get()));
CHECK(color_management_surface_) << "Can't create color management surface.";
zcr_color_management_surface_v1_add_listener(
color_management_surface_.get(), &kSurfaceColorManagerListener, this);
}
std::unique_ptr<zcr_color_space_v1> HdrClient::CreateColorSpace(
uint32_t color,
uint32_t transfer) {
std::unique_ptr<zcr_color_space_creator_v1> creator(
zcr_color_manager_v1_create_color_space_from_names(
globals_.color_manager.get(), transfer, color,
ZCR_COLOR_MANAGER_V1_WHITEPOINT_NAMES_D50));
// Since we're doing a wl_display_roundtrip, we can do all this state
// management on the stack a clean it up once we get out of scope.
struct creation_data_t {
std::unique_ptr<zcr_color_space_v1> color_space = nullptr;
uint32_t error = 0;
} creation_data;
zcr_color_space_creator_v1_listener creator_listener = {
.created =
[](void* data,
struct zcr_color_space_creator_v1* zcr_color_space_creator_v1,
struct zcr_color_space_v1* new_color_space) {
static_cast<creation_data_t*>(data)->color_space.reset(
new_color_space);
},
.error =
[](void* data,
struct zcr_color_space_creator_v1* zcr_color_space_creator_v1,
uint32_t error) {
static_cast<creation_data_t*>(data)->error = error;
}};
zcr_color_space_creator_v1_add_listener(creator.get(), &creator_listener,
&creation_data);
wl_display_roundtrip(display_.get());
if (creation_data.error) {
LOG(FATAL) << "Unable to create colorspace for primaries=" << color
<< " transfer=" << transfer << " reason='"
<< ColorSpaceCreationErrorToString(creation_data.error) << "'";
}
return std::move(creation_data.color_space);
}
void HdrClient::Run(const ClientBase::InitParams& params,
bool test_white_levels,
uint32_t primary1,
uint32_t transfer1,
uint32_t primary2,
uint32_t transfer2) {
if (!ClientBase::Init(params))
return;
InitColorManagement();
std::vector<std::unique_ptr<zcr_color_space_v1>> color_spaces;
color_spaces.push_back(CreateColorSpace(primary1, transfer1));
color_spaces.push_back(CreateColorSpace(primary2, transfer2));
bool callback_pending = false;
std::unique_ptr<wl_callback> frame_callback;
wl_callback_listener frame_listener = {FrameCallback};
for (auto& buff : buffers_) {
if (test_white_levels) {
DrawToGbm(size_, base::BindOnce(DrawWhiteBox), buff->bo.get());
} else {
DrawToGbm(size_, base::BindOnce(DrawColorGradients), buff->bo.get());
}
}
constexpr int kFramesPerCycle = 60;
size_t frame_number = 0;
size_t color_space_idx = 0;
do {
if (callback_pending)
continue;
Buffer* buffer = DequeueBuffer();
if (!buffer) {
LOG(ERROR) << "Can't find free buffer";
return;
}
frame_number++;
if (frame_number % kFramesPerCycle == 0) {
color_space_idx = (color_space_idx + 1) % color_spaces.size();
LOG(ERROR) << "Switching to color space "
<< (color_space_idx == 0 ? "PRIMARY" : "SECONDARY");
}
wl_surface_set_buffer_scale(surface_.get(), scale_);
wl_surface_set_buffer_transform(surface_.get(), transform_);
wl_surface_damage(surface_.get(), 0, 0, surface_size_.width(),
surface_size_.height());
wl_surface_attach(surface_.get(), buffer->buffer.get(), 0, 0);
zcr_color_management_surface_v1_set_color_space(
color_management_surface_.get(), color_spaces[color_space_idx].get(),
ZCR_COLOR_MANAGEMENT_SURFACE_V1_RENDER_INTENT_PERCEPTUAL);
frame_callback.reset(wl_surface_frame(surface_.get()));
wl_callback_add_listener(frame_callback.get(), &frame_listener,
&callback_pending);
callback_pending = true;
wl_surface_commit(surface_.get());
wl_display_flush(display_.get());
} while (wl_display_dispatch(display_.get()) != -1);
}
} // namespace clients
} // namespace wayland
} // namespace exo
int main(int argc, char* argv[]) {
base::AtExitManager exit_manager;
base::CommandLine::Init(argc, argv);
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
base::SingleThreadTaskExecutor main_task_executor(base::MessagePumpType::UI);
exo::wayland::clients::ClientBase::InitParams params;
params.num_buffers = 8;
if (!params.FromCommandLine(*command_line))
return 1;
CHECK(params.use_drm) << "Missing --use-drm parameter which is required for "
"gbm buffer allocation";
// sRGB
int32_t primary1 = ZCR_COLOR_MANAGER_V1_CHROMATICITY_NAMES_BT709;
int32_t transfer1 = ZCR_COLOR_MANAGER_V1_EOTF_NAMES_SRGB;
// HDR BT2020, PQ, full range RGB
int32_t primary2 = ZCR_COLOR_MANAGER_V1_CHROMATICITY_NAMES_BT2020;
int32_t transfer2 = ZCR_COLOR_MANAGER_V1_EOTF_NAMES_PQ;
params.bo_usage =
GBM_BO_USE_SCANOUT | GBM_BO_USE_LINEAR | GBM_BO_USE_TEXTURING;
exo::wayland::clients::HdrClient client;
client.Run(params, command_line->HasSwitch("white-levels"), primary1,
transfer1, primary2, transfer2);
return 0;
}