blob: b5ea3b260c02671b0f0fa833fdbc3f8730517d2b [file] [log] [blame]
/*
* Copyright 2021 The Chromium OS 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 "features/hdrnet/hdrnet_processor_device_adapter_ipu6.h"
#include <string>
#include <vector>
#include <base/strings/stringprintf.h>
#include "common/embed_file_toc.h"
#include "cros-camera/camera_metadata_utils.h"
#include "cros-camera/common.h"
#include "features/gcam_ae/ae_info.h"
#include "features/hdrnet/embedded_hdrnet_processor_shaders_ipu6_toc.h"
#include "features/hdrnet/ipu6_gamma.h"
#include "features/third_party/intel/intel_vendor_metadata_tags.h"
#include "gpu/embedded_gpu_shaders_toc.h"
#include "gpu/gles/framebuffer.h"
#include "gpu/gles/state_guard.h"
#include "gpu/gles/transform.h"
namespace cros {
namespace {
constexpr const char kVertexShaderFilename[] =
"fullscreen_rect_highp_310_es.vert";
constexpr const char kPreprocessorFilename[] = "preprocess_ipu6.frag";
constexpr const char kPostprocessorFilename[] = "postprocess_ipu6.frag";
} // namespace
HdrNetProcessorDeviceAdapterIpu6::HdrNetProcessorDeviceAdapterIpu6(
const camera_metadata_t* static_info,
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: task_runner_(task_runner) {
base::Optional<int32_t> max_curve_points =
GetRoMetadata<int32_t>(static_info, ANDROID_TONEMAP_MAX_CURVE_POINTS);
CHECK(max_curve_points) << ": ANDROID_TONEMAP_MAX_CURVE_POINTS not set";
num_curve_points_ = *max_curve_points;
}
bool HdrNetProcessorDeviceAdapterIpu6::Initialize() {
DCHECK(task_runner_->BelongsToCurrentThread());
rect_ = std::make_unique<ScreenSpaceRect>();
nearest_clamp_to_edge_ = Sampler(NearestClampToEdge());
linear_clamp_to_edge_ = Sampler(LinearClampToEdge());
EmbeddedFileToc hdrnet_processor_shaders =
GetEmbeddedHdrnetProcessorShadersIpu6Toc();
EmbeddedFileToc gpu_shaders = GetEmbeddedGpuShadersToc();
// Create the vextex shader.
base::span<const char> src = gpu_shaders.Get(kVertexShaderFilename);
Shader vertex_shader(GL_VERTEX_SHADER, std::string(src.data(), src.size()));
if (!vertex_shader.IsValid()) {
LOGF(ERROR) << "Failed to load vertex shader";
return false;
}
{
base::span<const char> src =
hdrnet_processor_shaders.Get(kPreprocessorFilename);
Shader fragment_shader(GL_FRAGMENT_SHADER,
std::string(src.data(), src.size()));
if (!fragment_shader.IsValid()) {
LOGF(ERROR) << "Failed to load preprocess shader";
return false;
}
preprocessor_program_ = ShaderProgram({&vertex_shader, &fragment_shader});
}
{
base::span<const char> src =
hdrnet_processor_shaders.Get(kPostprocessorFilename);
Shader fragment_shader(GL_FRAGMENT_SHADER,
std::string(src.data(), src.size()));
if (!fragment_shader.IsValid()) {
LOGF(ERROR) << "Failed to load postprocess shader";
return false;
}
postprocessor_program_ = ShaderProgram({&vertex_shader, &fragment_shader});
}
gamma_lut_ = intel_ipu6::CreateGammaLutTexture();
inverse_gamma_lut_ = intel_ipu6::CreateInverseGammaLutTexture();
VLOGF(1) << "Created IPU6 HDRnet device processor";
initialized_ = true;
return true;
}
void HdrNetProcessorDeviceAdapterIpu6::TearDown() {
DCHECK(task_runner_->BelongsToCurrentThread());
}
bool HdrNetProcessorDeviceAdapterIpu6::WriteRequestParameters(
Camera3CaptureDescriptor* request, MetadataLogger* metadata_logger) {
DCHECK(task_runner_->BelongsToCurrentThread());
std::array<uint8_t, 1> tonemap_curve_enable = {
INTEL_VENDOR_CAMERA_CALLBACK_TM_CURVE_TRUE};
if (!request->UpdateMetadata<uint8_t>(INTEL_VENDOR_CAMERA_CALLBACK_TM_CURVE,
tonemap_curve_enable)) {
LOGF(ERROR) << "Cannot enable INTEL_VENDOR_CAMERA_CALLBACK_TM_CURVE in "
"request metadta";
return false;
}
return true;
}
void HdrNetProcessorDeviceAdapterIpu6::ProcessResultMetadata(
Camera3CaptureDescriptor* result, MetadataLogger* metadata_logger) {
DCHECK(task_runner_->BelongsToCurrentThread());
// TODO(jcliang): Theoretically metadata can come after the buffer as well.
// Currently the pipeline would break if the metadata come after the buffers.
if (!initialized_) {
LOGF(ERROR) << "HDRnet processor hadn't been initialized";
return;
}
base::span<const float> tonemap_curve =
result->GetMetadata<float>(INTEL_VENDOR_CAMERA_TONE_MAP_CURVE);
if (!tonemap_curve.empty()) {
VLOGF(1) << "Update GTM curve";
CHECK_EQ(tonemap_curve.size(), num_curve_points_ * 2);
gtm_lut_ = CreateGainLutTexture(tonemap_curve, false);
inverse_gtm_lut_ = CreateGainLutTexture(tonemap_curve, true);
if (metadata_logger) {
metadata_logger->Log(result->frame_number(), kTagToneMapCurve,
tonemap_curve);
}
}
}
bool HdrNetProcessorDeviceAdapterIpu6::Preprocess(
const HdrNetConfig::Options& options,
const SharedImage& input_yuv,
const SharedImage& output_rgba) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (!inverse_gtm_lut_.IsValid()) {
LOGF(ERROR) << "Invalid GTM curve textures";
return false;
}
// Intel's GLES implementation always samples the YUV image with narrow range
// color space and it's crushing the shadow areas on the images. Before we
// have a fix in mesa, sample and covert the YUV image to RGB ourselves.
if (!input_yuv.y_texture().IsValid() || !input_yuv.uv_texture().IsValid() ||
!output_rgba.texture().IsValid()) {
LOGF(ERROR) << "Invalid input or output textures";
return false;
}
if ((input_yuv.y_texture().width() / 2 != input_yuv.uv_texture().width()) ||
(input_yuv.y_texture().height() / 2 != input_yuv.uv_texture().height())) {
LOG(ERROR) << "Invalid Y (" << input_yuv.y_texture().width() << ", "
<< input_yuv.y_texture().height() << ") and UV ("
<< input_yuv.uv_texture().width() << ", "
<< input_yuv.uv_texture().height() << ") output dimension";
return false;
}
FramebufferGuard fb_guard;
ViewportGuard viewport_guard;
ProgramGuard program_guard;
VertexArrayGuard va_guard;
rect_->SetAsVertexInput();
constexpr int kYInputBinding = 0;
constexpr int kUvInputBinding = 1;
constexpr int kInverseGammaLutBinding = 2;
constexpr int kInverseGtmLutBinding = 3;
glActiveTexture(GL_TEXTURE0 + kYInputBinding);
input_yuv.y_texture().Bind();
nearest_clamp_to_edge_.Bind(kYInputBinding);
glActiveTexture(GL_TEXTURE0 + kUvInputBinding);
input_yuv.uv_texture().Bind();
nearest_clamp_to_edge_.Bind(kUvInputBinding);
glActiveTexture(GL_TEXTURE0 + kInverseGammaLutBinding);
inverse_gamma_lut_.Bind();
linear_clamp_to_edge_.Bind(kInverseGammaLutBinding);
glActiveTexture(GL_TEXTURE0 + kInverseGtmLutBinding);
inverse_gtm_lut_.Bind();
linear_clamp_to_edge_.Bind(kInverseGtmLutBinding);
preprocessor_program_.UseProgram();
// Set shader uniforms.
std::vector<float> texture_matrix = TextureSpaceFromNdc();
GLint uTextureMatrix =
preprocessor_program_.GetUniformLocation("uTextureMatrix");
glUniformMatrix4fv(uTextureMatrix, 1, false, texture_matrix.data());
Framebuffer fb;
fb.Bind();
glViewport(0, 0, output_rgba.texture().width(),
output_rgba.texture().height());
fb.Attach(GL_COLOR_ATTACHMENT0, output_rgba.texture());
rect_->Draw();
// Clean up.
glActiveTexture(GL_TEXTURE0 + kYInputBinding);
input_yuv.y_texture().Unbind();
Sampler::Unbind(kYInputBinding);
glActiveTexture(GL_TEXTURE0 + kUvInputBinding);
input_yuv.uv_texture().Unbind();
Sampler::Unbind(kUvInputBinding);
glActiveTexture(GL_TEXTURE0 + kInverseGammaLutBinding);
inverse_gamma_lut_.Unbind();
Sampler::Unbind(kInverseGammaLutBinding);
glActiveTexture(GL_TEXTURE0 + kInverseGtmLutBinding);
inverse_gtm_lut_.Unbind();
Sampler::Unbind(kInverseGtmLutBinding);
return true;
}
bool HdrNetProcessorDeviceAdapterIpu6::Postprocess(
const HdrNetConfig::Options& options,
const SharedImage& input_rgba,
const SharedImage& output_nv12) {
DCHECK(task_runner_->BelongsToCurrentThread());
if (!gtm_lut_.IsValid()) {
return false;
}
if (!input_rgba.texture().IsValid() || !output_nv12.y_texture().IsValid() ||
!output_nv12.uv_texture().IsValid()) {
return false;
}
if ((output_nv12.y_texture().width() / 2 !=
output_nv12.uv_texture().width()) ||
(output_nv12.y_texture().height() / 2 !=
output_nv12.uv_texture().height())) {
LOGF(ERROR) << "Invalid Y (" << output_nv12.y_texture().width() << ", "
<< output_nv12.y_texture().height() << ") and UV ("
<< output_nv12.uv_texture().width() << ", "
<< output_nv12.uv_texture().height() << ") output dimension";
return false;
}
FramebufferGuard fb_guard;
ViewportGuard viewport_guard;
ProgramGuard program_guard;
VertexArrayGuard va_guard;
rect_->SetAsVertexInput();
constexpr int kInputBinding = 0;
constexpr int kGammaLutBinding = 1;
constexpr int kGtmLutBinding = 2;
glActiveTexture(GL_TEXTURE0 + kInputBinding);
input_rgba.texture().Bind();
nearest_clamp_to_edge_.Bind(kInputBinding);
glActiveTexture(GL_TEXTURE0 + kGammaLutBinding);
gamma_lut_.Bind();
linear_clamp_to_edge_.Bind(kGammaLutBinding);
glActiveTexture(GL_TEXTURE0 + kGtmLutBinding);
gtm_lut_.Bind();
linear_clamp_to_edge_.Bind(kGtmLutBinding);
postprocessor_program_.UseProgram();
// Set shader uniforms.
std::vector<float> texture_matrix = TextureSpaceFromNdc();
GLint uTextureMatrix =
postprocessor_program_.GetUniformLocation("uTextureMatrix");
glUniformMatrix4fv(uTextureMatrix, 1, false, texture_matrix.data());
GLint uIsYPlane = postprocessor_program_.GetUniformLocation("uIsYPlane");
Framebuffer fb;
fb.Bind();
// Y pass.
{
glUniform1i(uIsYPlane, true);
glViewport(0, 0, output_nv12.y_texture().width(),
output_nv12.y_texture().height());
fb.Attach(GL_COLOR_ATTACHMENT0, output_nv12.y_texture());
rect_->Draw();
}
// UV pass.
{
glUniform1i(uIsYPlane, false);
glViewport(0, 0, output_nv12.uv_texture().width(),
output_nv12.uv_texture().height());
fb.Attach(GL_COLOR_ATTACHMENT0, output_nv12.uv_texture());
rect_->Draw();
}
// Clean up.
glActiveTexture(GL_TEXTURE0 + kInputBinding);
input_rgba.texture().Unbind();
Sampler::Unbind(kInputBinding);
glActiveTexture(GL_TEXTURE0 + kGammaLutBinding);
gamma_lut_.Unbind();
Sampler::Unbind(kGammaLutBinding);
glActiveTexture(GL_TEXTURE0 + kGtmLutBinding);
gtm_lut_.Unbind();
Sampler::Unbind(kGtmLutBinding);
return true;
}
Texture2D HdrNetProcessorDeviceAdapterIpu6::CreateGainLutTexture(
base::span<const float> tonemap_curve, bool inverse) {
auto interpolate = [](float i, float x0, float y0, float x1,
float y1) -> float {
float kEpsilon = 1e-8;
if (std::abs(x1 - x0) < kEpsilon) {
return y0;
}
float slope = (y1 - y0) / (x1 - x0);
return y0 + (i - x0) * slope;
};
if (gtm_lut_buffer_.size() < num_curve_points_) {
gtm_lut_buffer_.resize(num_curve_points_);
}
// |tonemap_curve| is an array of |num_curve_points_| (v, g) pairs of floats,
// with v in [0, 1] and g > 0. Each (v, g) pair specifies the gain `g` to
// apply when the pixel value is `v`. Note that the Intel IPU6 GTM LUT is
// "gain-based" and is different from the plain LUT as defined in [1]. It is
// assumed that v * g is non-decreasing otherwise the LUT cannot be reasonably
// inversed.
//
// For the forward LUT, we build a table with |num_curve_points_| (v, g)
// entries, where `g` is the gain to apply for pre-gain pixel value `v`. This
// is similar to the input |tonemap_curve|.
//
// For the inverse LUT, we build a table with |num_curve_points_| (u, g)
// entries, where `g` is the estimated gain applied on post-gain pixel value
// `u`. The shader would divide `u` by `g` to transform the pixel value back
// to pseudo-linear domain.
//
// [1]:
// https://developer.android.com/reference/android/hardware/camera2/CaptureRequest#TONEMAP_CURVE
int lut_index = 0;
float x0 = 0.0, y0 = 1.0;
for (int i = 0; i < num_curve_points_; ++i) {
int idx = i * 2;
float x1 = tonemap_curve[idx], y1 = tonemap_curve[idx + 1];
if (inverse) {
x1 = x1 * y1; // x-axis is the value with gain applied.
}
const int scaled_x1 = x1 * num_curve_points_;
for (; lut_index <= scaled_x1 && lut_index < num_curve_points_;
++lut_index) {
gtm_lut_buffer_[lut_index] = interpolate(
static_cast<float>(lut_index) / num_curve_points_, x0, y0, x1, y1);
VLOGF(1) << base::StringPrintf("(%5d, %1.10f, %d)", lut_index,
gtm_lut_buffer_[lut_index], inverse);
}
x0 = x1;
y0 = y1;
}
for (; lut_index < num_curve_points_; ++lut_index) {
gtm_lut_buffer_[lut_index] = interpolate(
static_cast<float>(lut_index) / num_curve_points_, x0, y0, 1.0, 1.0);
VLOGF(1) << base::StringPrintf("(%5d, %1.10f, %d)", lut_index,
gtm_lut_buffer_[lut_index], inverse);
}
Texture2D lut_texture(GL_R16F, num_curve_points_, 1);
CHECK(lut_texture.IsValid());
lut_texture.Bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, num_curve_points_, 1, GL_RED,
GL_FLOAT, gtm_lut_buffer_.data());
return lut_texture;
}
} // namespace cros