blob: 68d9617d0beb2b744bc185c88a7a55237dcb09ba [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// -----------------------------------------------------------------------------
//
// WP2 lossy encoding.
//
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <memory>
#include "src/common/lossy/transforms.h"
#include "src/enc/analysis.h"
#include "src/enc/wp2_enc_i.h"
#include "src/utils/utils.h"
#include "src/wp2/format_constants.h"
namespace WP2 {
WP2Status SyntaxWriter::Init(ANSDictionaries* const dicts,
const EncoderConfig& config,
const GlobalParams& gparams, const YUVPlane& yuv,
ChromaSubsampling chroma_subsampling,
const Rectangle& tile_rect, uint32_t num_blocks,
bool use_aom_coeffs, bool use_splits,
const ProgressRange& alpha_progress) {
config_ = &config;
dicts_ = dicts;
recorded_blocks_ = 0;
tile_rect_ = tile_rect;
gparams_ = &gparams;
assert(gparams_->IsOk());
chroma_subsampling_ = chroma_subsampling;
use_splits_ = use_splits;
num_blocks_ = num_blocks;
// Number of transforms is unknown yet, use a higher bound.
num_transforms_ = std::min(
num_blocks * 4, SizeBlocks(yuv.GetWidth()) * SizeBlocks(yuv.GetHeight()));
// Deal with context.
WP2_CHECK_STATUS(context_.Init(use_aom_coeffs, gparams_->segments_.size(),
gparams_->explicit_segment_ids_,
tile_rect.width));
WP2_CHECK_STATUS(residual_writer_.Init(context_.use_aom(),
gparams_->maybe_use_lossy_alpha_));
if (gparams_->has_alpha_) {
WP2_CHECK_STATUS(alpha_writer_.Init(*config_, gparams, context_, yuv,
tile_rect, alpha_progress));
} else {
WP2_CHECK_STATUS(alpha_progress.AdvanceBy(1.));
}
if (kDebugPrintRate) {
WP2_CHECK_ALLOC_OK(residual_rate_.resize(num_blocks));
}
return WP2_STATUS_OK;
}
WP2Status SyntaxWriter::CopyFrom(const SyntaxWriter& other,
ANSDictionaries* const dicts) {
config_ = other.config_;
WP2_CHECK_OK(dicts != nullptr, WP2_STATUS_NULL_PARAMETER);
WP2_CHECK_OK(dicts != other.dicts_, WP2_STATUS_INVALID_PARAMETER);
dicts_ = dicts;
WP2_CHECK_STATUS(context_.CopyFrom(other.context_));
if (other.gparams_->has_alpha_) {
WP2_CHECK_STATUS(alpha_writer_.CopyFrom(other.alpha_writer_, context_));
}
num_blocks_ = other.num_blocks_;
num_transforms_ = other.num_transforms_;
recorded_blocks_ = other.recorded_blocks_;
tile_rect_ = other.tile_rect_;
chroma_subsampling_ = other.chroma_subsampling_;
gparams_ = other.gparams_;
use_splits_ = other.use_splits_;
WP2_CHECK_STATUS(
symbol_writer_.CopyFrom(other.symbol_writer_, *dicts_, *other.dicts_));
WP2_CHECK_STATUS(symbol_recorder_.CopyFrom(other.symbol_recorder_));
WP2_CHECK_STATUS(counters_.CopyFrom(other.counters_, symbol_recorder_));
residual_writer_.CopyFrom(other.residual_writer_);
WP2_CHECK_ALLOC_OK(residual_rate_.copy_from(other.residual_rate_));
return WP2_STATUS_OK;
}
WP2Status SyntaxWriter::InitPass() {
SymbolsInfo symbols_info;
WP2_CHECK_STATUS(symbols_info.InitLossy(
gparams_->segments_.size(), gparams_->partition_set_,
gparams_->maybe_use_lossy_alpha_, context_.use_aom(), use_splits_));
symbols_info.SetMinMax(kSymbolModeY, /*min=*/0,
/*max=*/gparams_->y_preds_.GetMaxMode());
if (gparams_->maybe_use_lossy_alpha_) {
symbols_info.SetMinMax(kSymbolModeA, /*min=*/0,
/*max=*/gparams_->a_preds_.GetMaxMode());
}
symbols_info.SetMinMax(kSymbolModeUV, /*min=*/0,
/*max=*/gparams_->uv_preds_.GetMaxMode());
const uint32_t num_channels = (gparams_->maybe_use_lossy_alpha_ ? 4 : 3);
for (Channel c : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (c == kAChannel && !gparams_->maybe_use_lossy_alpha_) continue;
WP2_CHECK_STATUS(symbols_info.SetMinMax(
kSymbolDC, ResidualIO::GetCluster(c, num_channels), /*min=*/0,
/*max=*/gparams_->GetMaxDCRange(c)));
}
symbols_info.SetClusters(kSymbolDC, symbols_info.NumClusters(kSymbolDC));
WP2_CHECK_STATUS(symbol_writer_.Init(symbols_info, config_->effort));
WP2_CHECK_STATUS(symbol_writer_.Allocate());
if (pass_number_ == 0) {
WP2_CHECK_STATUS(symbol_recorder_.Allocate(
symbols_info, /*num_records=*/num_blocks_ * kMaxBlockSize2));
} else {
WP2_CHECK_STATUS(symbol_recorder_.MakeBackup());
}
++pass_number_;
WP2_CHECK_STATUS(
counters_.Init(config_->effort, context_.use_aom(), symbol_recorder_));
context_.Reset();
if (recorded_blocks_ > 0) WP2_CHECK_STATUS(ResetRecord());
return WP2_STATUS_OK;
}
WP2Status SyntaxWriter::WriteHeader(ANSEncBase* const enc) {
const uint32_t max_num_blocks =
SizeBlocks(tile_rect_.width) * SizeBlocks(tile_rect_.height);
// The minimum number of blocks is not accurate but it is not worth it to have
// a more complex solution (neglictible pessimization).
const uint32_t min_num_blocks = std::max(1u, max_num_blocks / kMaxBlockSize2);
// This is also used for partitioning with an upper bound of 'num_blocks_'.
assert((num_blocks_ == max_num_blocks) || (recorded_blocks_ == num_blocks_));
ANSDebugPrefix prefix(enc, "GlobalHeader");
// TODO(skal): differential-write of gparams_
enc->PutBool(context_.use_aom(), "use_aom_coeffs");
if (gparams_->partition_snapping_) {
enc->PutBool(use_splits_, "use_splits");
} else {
assert(!use_splits_);
}
// Write dictionaries.
PutLargeRange(num_blocks_, min_num_blocks, max_num_blocks, enc, "num_blocks");
// If there's only one block, we don't need this global value, we'll signal
// the subsampling mode for that block later.
if (chroma_subsampling_ != ChromaSubsampling::kSingleBlock) {
enc->PutRValue((int)chroma_subsampling_,
(int)ChromaSubsampling::kSingleBlock, "chroma_subsampling");
}
if (gparams_->explicit_segment_ids_) {
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(kSymbolSegmentId, num_blocks_,
symbol_recorder_, "segment_id",
enc, dicts_));
}
if (gparams_->use_rnd_mtx_) {
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolRndMtx0, num_blocks_, symbol_recorder_, "rnd_mtx", enc, dicts_));
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolUseRandomMatrix, num_blocks_, symbol_recorder_, "use_rnd_mtx",
enc, dicts_));
}
WP2_CHECK_STATUS(
symbol_writer_.WriteHeader(kSymbolSplitTransform, num_blocks_,
symbol_recorder_, "split_tf", enc, dicts_));
if (chroma_subsampling_ == ChromaSubsampling::kSingleBlock ||
chroma_subsampling_ == ChromaSubsampling::kAdaptive) {
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolYuv420, num_blocks_, symbol_recorder_, "yuv420", enc, dicts_));
}
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(kSymbolTransform, num_blocks_,
symbol_recorder_, "transform",
enc, dicts_));
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolModeY, num_blocks_, symbol_recorder_, "mode", enc, dicts_));
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolModeUV, num_blocks_, symbol_recorder_, "mode", enc, dicts_));
// TODO(maryla): not necessary if the SignalingCflPredictor isn't used.
const uint32_t num_channels_using_cfl =
gparams_->maybe_use_lossy_alpha_ ? 3 : 2;
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolCflSlope, num_channels_using_cfl * num_blocks_, symbol_recorder_,
"cfl", enc, dicts_));
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(kSymbolBlockSize, num_blocks_,
symbol_recorder_, "block_size",
enc, dicts_));
const uint32_t num_coeffs_max =
max_num_blocks * kMinBlockSizePix * kMinBlockSizePix;
const uint32_t num_coeffs_max_uv =
(chroma_subsampling_ == ChromaSubsampling::k420) ? num_coeffs_max / 4
: num_coeffs_max;
// We should always have a UV block.
assert(num_coeffs_max_uv > 0);
WP2_CHECK_STATUS(context_.segment_id_predictor().WriteHeader(enc));
if (gparams_->has_alpha_) {
WP2_CHECK_STATUS(alpha_writer_.WriteHeader(num_coeffs_max, enc));
if (alpha_writer_.GetAlphaMode() == kAlphaModeLossy) {
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolBlockAlphaMode, num_blocks_, symbol_recorder_,
"block_alpha_mode", enc, dicts_));
}
if (alpha_writer_.GetAlphaMode() != kAlphaModeLossless) {
WP2_CHECK_STATUS(symbol_writer_.WriteHeader(
kSymbolModeA, num_blocks_, symbol_recorder_, "mode", enc, dicts_));
}
}
WP2_CHECK_STATUS(residual_writer_.WriteHeader(
num_coeffs_max, num_coeffs_max_uv, num_transforms_,
gparams_->has_alpha_ &&
(alpha_writer_.GetAlphaMode() != kAlphaModeLossless),
symbol_recorder_, dicts_, enc, &symbol_writer_));
return enc->GetStatus();
}
void SyntaxWriter::WriteBlockBeforeCoeffs(const CodedBlock& cb,
SymbolManager* const sm,
ANSEncBase* const enc) {
context_.segment_id_predictor().WriteId(cb, sm, enc);
context_.segment_id_predictor().RecordId(cb);
if (gparams_->has_alpha_) {
alpha_writer_.WriteBlockBeforeCoeffs(cb, sm, enc);
} else if (!gparams_->maybe_use_lossy_alpha_) {
assert(!cb.HasLossyAlpha());
}
for (Channel channel : {kYChannel, kAChannel, kUChannel, kVChannel}) {
if (channel == kAChannel && !cb.HasLossyAlpha()) continue;
WritePredictors(cb, channel, sm, enc);
}
if (chroma_subsampling_ == ChromaSubsampling::kSingleBlock ||
chroma_subsampling_ == ChromaSubsampling::kAdaptive) {
sm->Process(kSymbolYuv420, cb.is420_, "yuv420", enc);
}
for (Channel channel : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (channel == kAChannel && !cb.HasLossyAlpha()) continue;
WriteSplitTransform(cb, channel, sm, enc);
WriteHasCoeffs(cb, channel, sm, enc);
WriteTransform(cb, channel, sm, enc);
}
if (!context_.use_aom()) {
ANSDebugPrefix prefix(enc, "coeff_method");
gparams_->segments_[cb.id_].StoreEncodingMethods(cb, sm, enc);
}
if (cb.mtx_set_ != nullptr) {
#if defined(ENABLE_RND_MTX)
const bool can_use =
gparams_->mtx_set_.IsOk(cb.mtx_[kYChannel], cb.tdim(kYChannel));
if (can_use) {
sm->Process(kSymbolUseRandomMatrix, cb.use_mtx_, "use_rnd_mtx", enc);
if (cb.use_mtx_) {
sm->Process(kSymbolRndMtx0, cb.mtx_[kYChannel], "rnd_mtx_y", enc);
}
} else {
assert(!cb.use_mtx_);
}
#endif
}
}
void SyntaxWriter::WritePredictors(const CodedBlock& cb, Channel channel,
SymbolManager* const sm,
ANSEncBase* const enc) {
ANSDebugPrefix prefix(enc, "pred_modes");
const CodedBlock::CodingParams& params = cb.GetCodingParams(channel);
if (channel == kYChannel) {
if (cb.y_context_is_constant_) return;
sm->Process(kSymbolModeY, params.pred->mode(), "y", enc);
} else if (channel == kUChannel) {
sm->Process(kSymbolModeUV, params.pred->mode(), "uv", enc);
} else if (channel == kVChannel) {
// kVChannel is not written as it is the same as kUChannel.
} else {
sm->Process(kSymbolModeA, params.pred->mode(), "a", enc);
}
/* sub_mode = */params.pred->WriteParams(cb, channel, sm, enc);
}
void SyntaxWriter::WriteSplitTransform(const CodedBlock& cb, Channel channel,
SymbolManager* const sm,
ANSEncBase* const enc) {
if (channel == kYChannel &&
GetSplitSize(cb.dim(), /*split=*/true) != cb.dim()) {
sm->Process(kSymbolSplitTransform, cb.GetCodingParams(channel).split_tf,
"split_tf", enc);
} else {
assert(!cb.GetCodingParams(channel).split_tf);
}
}
void SyntaxWriter::WriteHasCoeffs(const CodedBlock& cb, Channel channel,
SymbolManager* const sm,
ANSEncBase* const enc) {
for (uint32_t tf_i = 0; tf_i < cb.GetNumTransforms(channel); ++tf_i) {
const bool tf_has_coeffs = (cb.num_coeffs_[channel][tf_i] != 0);
const uint32_t cluster = (uint32_t)channel;
sm->Process(kSymbolHasCoeffs, cluster, tf_has_coeffs ? 1 : 0, "has_coeffs",
enc);
}
}
void SyntaxWriter::WriteTransform(const CodedBlock& cb, Channel channel,
SymbolManager* const sm,
ANSEncBase* const enc) {
const CodedBlock::CodingParams& params = cb.GetCodingParams(channel);
if (channel == kUChannel || channel == kVChannel || channel == kAChannel) {
assert(params.tf == kDctDct);
} else if (cb.HasCoeffs(channel)) {
// TODO(maryla): try more ways to signal the transform which might be
// correlated to block size.
sm->Process(kSymbolTransform, params.tf, "transform", enc);
} else {
// No need to signal the transform as there is no coeff.
}
}
void SyntaxWriter::RecordBlockHeader(const CodedBlock& cb) {
ANSEncNoop enc;
WriteBlockBeforeCoeffs(cb, &symbol_recorder_, &enc);
}
WP2Status SyntaxWriter::WriteBlocks(const Vector<CodedBlock>& cblocks,
const Vector_u16& size_order_indices,
FrontMgrDoubleOrderBase* const mgr,
ANSEnc* const enc) {
assert(cblocks.size() == num_blocks_);
assert(cblocks.size() == size_order_indices.size());
// Note: we could call segment_ids_.InitMap(bw, bh) again here.
WP2_CHECK_STATUS(residual_writer_.Init(context_.use_aom(),
gparams_->maybe_use_lossy_alpha_));
context_.Reset();
assert(size_order_indices.size() == num_blocks_);
mgr->Clear();
uint32_t block_idx = 0;
for (uint16_t ind : size_order_indices) {
{
ANSDebugPrefix prefix(enc, "BlockHeader");
const CodedBlock& cb = cblocks[ind];
WriteBlockSize(*mgr, cb.dim(), &symbol_writer_, enc);
Block block_tmp;
WP2_CHECK_ALLOC_OK(mgr->UseSize(cb.dim(), &block_tmp));
assert(block_tmp == cb.blk());
}
while (true) {
Block block;
if (!mgr->UseReady(&block)) break;
const CodedBlock& cb = cblocks[block_idx];
assert(cb.blk() == block);
WP2_CHECK_STATUS(WriteBlock(cb, block_idx, enc));
++block_idx;
}
}
if (kDebugPrintRate) {
float total_estimated = 0.f;
float total_actual = 0.f;
float total_difference = 0.f;
float max_difference = 0.f;
uint32_t num_non_zero = 0;
for (uint32_t i = 0; i < cblocks.size(); ++i) {
const CodedBlock& cb = cblocks[i];
for (Channel c : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (c == kAChannel && !cb.HasLossyAlpha()) continue;
const float estimated_rate = residual_rate_[i][c];
const float actual_rate = residual_rate_[i][4 + c];
total_estimated += estimated_rate;
total_actual += actual_rate;
if (actual_rate == 0 && estimated_rate == 0) continue;
fprintf(stderr,
"block (%3d %3d) channel %d rate: estimated %6.2f vs real "
"%6.2f, %+4.2f",
cb.x(), cb.y(), c, estimated_rate, actual_rate,
actual_rate - estimated_rate);
if (estimated_rate != 0) {
++num_non_zero;
const float relative_difference = actual_rate / estimated_rate - 1.f;
const float abs_difference = std::abs(relative_difference);
total_difference += abs_difference;
if (abs_difference > max_difference) max_difference = abs_difference;
fprintf(stderr, " %+.2f%%\n", relative_difference * 100.f);
} else {
fprintf(stderr, "\n");
}
}
}
fprintf(
stderr,
"Total residual rate estimated %.2f vs real %.2f, "
"%+.2f%%, average difference (for non zero rates) %.2f%% max %.2f%%\n",
total_estimated, total_actual,
(total_estimated / total_actual - 1.f) * 100.f,
total_difference / num_non_zero * 100, max_difference * 100);
}
return enc->GetStatus();
}
WP2Status SyntaxWriter::WriteBlocksUseSplits(const Vector<CodedBlock>& cblocks,
const Vector_u32& splits,
ANSEnc* const enc) {
assert(cblocks.size() == num_blocks_);
// Note: we could call segment_ids_.InitMap(bw, bh) again here.
WP2_CHECK_STATUS(residual_writer_.Init(context_.use_aom(),
gparams_->maybe_use_lossy_alpha_));
context_.Reset();
SplitIteratorDefault it;
WP2_CHECK_STATUS(
it.Init(gparams_->partition_set_, tile_rect_.width, tile_rect_.height));
uint32_t block_idx = 0;
uint32_t split_idx = 0;
while (!it.Done()) {
if (it.CurrentBlock().splittable) {
ANSDebugPrefix prefix(enc, "BlockHeader");
const uint32_t split = splits[split_idx];
WriteBlockSplit(it, split, &symbol_writer_, enc);
it.SplitCurrentBlock(split);
++split_idx;
} else {
const CodedBlock& cb = cblocks[block_idx];
assert(cb.blk() == it.CurrentBlock().block);
WP2_CHECK_STATUS(WriteBlock(cb, block_idx, enc));
it.NextBlock();
++block_idx;
}
}
return enc->GetStatus();
}
//------------------------------------------------------------------------------
void SyntaxWriter::FindBestEncodingMethods(CodedBlock* const cb) {
const uint32_t num_channels = (gparams_->maybe_use_lossy_alpha_ ? 4 : 3);
for (auto c : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (c == kAChannel && !cb->HasLossyAlpha()) continue;
for (uint32_t tf_i = 0; tf_i < cb->GetNumTransforms(c); ++tf_i) {
if (context_.use_aom()) {
ResidualWriter::SetGeometry(cb->num_coeffs_[c][tf_i],
&cb->method_[c][tf_i]);
} else {
residual_writer_.FindBestEncodingMethod(
cb->tdim(c), cb->coeffs_[c][tf_i], cb->num_coeffs_[c][tf_i],
cb->IsFirstCoeffDC(c), c, num_channels, counters_.residuals(),
&cb->method_[c][tf_i]);
}
}
}
}
WP2Status SyntaxWriter::DecideAlpha(CodedBlock* const cb,
const BlockModes& modes,
BlockScorer* const scorer) {
return alpha_writer_.DecideAlpha(cb, modes, residual_writer_, &counters_,
scorer);
}
WP2Status SyntaxWriter::RecordAlpha(const CodedBlock& cb) {
return alpha_writer_.Record(cb);
}
WP2Status SyntaxWriter::SetInitialSegmentIds() {
const Rectangle padded_tile_rect = {tile_rect_.x, tile_rect_.y,
Pad(tile_rect_.width, kPredWidth),
Pad(tile_rect_.height, kPredWidth)};
CodedBlock cb;
cb.SetDimDefault(Block(0, 0, BLK_4x4));
FrontMgr4x4 mgr;
WP2_CHECK_STATUS(
mgr.InitBase(padded_tile_rect.width, padded_tile_rect.height));
for (uint32_t y = 0; y < SizeBlocks(padded_tile_rect.height); ++y) {
for (uint32_t x = 0; x < SizeBlocks(padded_tile_rect.width); ++x) {
cb.SetXY(mgr, x, y);
cb.id_ = AssignSegmentId(*config_, *gparams_, padded_tile_rect, cb.blk());
context_.segment_id_predictor().InitInitialSegmentId(cb, cb.id_);
mgr.Use4x4();
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
void SyntaxWriter::Record(const CodedBlock& cb) {
assert(recorded_blocks_ < num_blocks_);
RecordBlockHeader(cb);
const uint32_t num_channels = (gparams_->maybe_use_lossy_alpha_ ? 4 : 3);
for (Channel channel : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (channel == kAChannel && !cb.HasLossyAlpha()) continue;
if (kDebugPrintRate) {
residual_rate_[recorded_blocks_][channel] = cb.ResidualRate(
context_, channel, num_channels, counters_.residuals());
}
residual_writer_.RecordCoeffs(cb, channel, &symbol_recorder_);
}
++recorded_blocks_;
}
void SyntaxWriter::RecordSize(const FrontMgrDoubleOrderBase& mgr,
BlockSize dim) {
RecordSize(dim, mgr.GetMaxPossibleBlock().dim(), mgr.GetPartitionSet());
}
void SyntaxWriter::RecordSize(BlockSize dim, BlockSize max_possible_size,
PartitionSet partition_set) {
assert(!use_splits_);
ANSEncNoop enc;
WriteBlockSize(dim, max_possible_size, partition_set, &symbol_recorder_,
&enc);
}
WP2Status SyntaxWriter::RecordSplit(const SplitIteratorBase& mgr,
uint32_t split_idx) {
assert(use_splits_);
ANSEncNoop enc;
WriteBlockSplit(mgr, split_idx, &symbol_recorder_, &enc);
return WP2_STATUS_OK;
}
WP2Status SyntaxWriter::ResetRecord() {
recorded_blocks_ = 0;
WP2_CHECK_STATUS(symbol_recorder_.ResetRecord(/*reset_backup=*/false));
if (gparams_->has_alpha_) {
WP2_CHECK_STATUS(alpha_writer_.ResetRecord());
}
return WP2_STATUS_OK;
}
WP2Status SyntaxWriter::WriteBlock(const CodedBlock& cb, uint32_t block_index,
ANSEnc* const enc) {
{
ANSDebugPrefix prefix(enc, "BlockHeader");
WriteBlockBeforeCoeffs(cb, &symbol_writer_, enc);
}
float previous_cost;
if (kDebugPrintRate) {
previous_cost = enc->GetCost();
}
for (Channel c : {kYChannel, kUChannel, kVChannel, kAChannel}) {
if (c == kAChannel && !cb.HasLossyAlpha()) continue;
residual_writer_.WriteCoeffs(cb, c, enc, &symbol_writer_);
if (kDebugPrintRate) {
const float cost = enc->GetCost();
const float actual_rate = cost - previous_cost;
residual_rate_[block_index][4 + c] = actual_rate;
previous_cost = cost;
}
}
if (gparams_->has_alpha_) {
WP2_CHECK_STATUS(alpha_writer_.Write(cb, enc));
}
if (cb.HasLossyAlpha()) assert(gparams_->has_alpha_);
#if defined(WP2_ENC_DEC_DEEP_MATCH)
PutRawPixels(cb, cb.out_, enc); // Debug
#endif
return WP2_STATUS_OK;
}
} // namespace WP2