| // 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 decoding. |
| // |
| // Author: Skal (pascal.massimino@gmail.com) |
| |
| #include <cassert> |
| #include <cstdlib> |
| #include <cstdint> |
| #include <string> |
| |
| #if !defined(WP2_BITTRACE) |
| #include <cstdio> |
| #endif |
| |
| #include "src/common/filters/rstr_flt_params.h" |
| #include "src/common/global_params.h" |
| #include "src/common/lossy/block.h" |
| #include "src/common/lossy/block_size.h" |
| #include "src/common/lossy/predictor.h" |
| #include "src/common/lossy/segment.h" |
| #include "src/common/vdebug.h" |
| #include "src/dec/filters/block_map_filter.h" |
| #include "src/dec/filters/intratile_filter.h" |
| #include "src/dec/filters/restoration_filter.h" |
| #include "src/dec/wp2_dec_i.h" |
| #include "src/dsp/dsp.h" |
| #include "src/dsp/math.h" |
| #include "src/utils/front_mgr.h" |
| #include "src/utils/plane.h" |
| #include "src/utils/split_iterator.h" |
| #include "src/utils/utils.h" |
| #include "src/wp2/base.h" |
| #include "src/wp2/decode.h" |
| #include "src/wp2/format_constants.h" |
| |
| namespace WP2 { |
| |
| //------------------------------------------------------------------------------ |
| |
| #if defined(WP2_BITTRACE) |
| |
| // Threshold above which ANSDec::GetBitCount() not matching the sum of bit |
| // traces is an issue not linked to floating point precision. |
| static constexpr double kDoubleSumTolerance = 0.0001; |
| |
| // Returns the absolute normalized difference of 'a' and 'b' in [0:2]. |
| static double NormDiff(double a, double b, double reduce_diff_by = 0) { |
| const double diff = std::abs(a - b); |
| if (diff <= reduce_diff_by) return 0.; |
| return (diff - reduce_diff_by) / std::max(std::abs(a), std::abs(b)); |
| } |
| |
| #endif // defined(WP2_BITTRACE) |
| |
| //------------------------------------------------------------------------------ |
| |
| WP2Status LossyDecode(const BitstreamFeatures& features, |
| const DecoderConfig& config, |
| TilesLayout* const tiles_layout, ANSDec* const dec, |
| Tile* const tile) { |
| WP2TransformInit(); |
| WP2QuantizeInit(); |
| PredictionInit(); |
| |
| assert(tile != nullptr); |
| tile->num_decoded_rows = 0; |
| |
| dec->AddDebugPrefix("GlobalHeader"); |
| const bool is_neural = |
| (tiles_layout->gparams->type_ == GlobalParams::GP_BOTH && |
| dec->ReadBool("is_neural")); |
| dec->PopDebugPrefix(); |
| if (is_neural) { |
| assert(!tile->rgb_output.IsEmpty()); |
| tile->output_is_yuv = false; |
| WP2_CHECK_STATUS(NeuralDecode(&tile->rgb_output, dec, features)); |
| WP2_CHECK_STATUS(tile->row_progress.AdvanceBy(tile->rect.height)); |
| return WP2_STATUS_OK; |
| } |
| assert(!tile->yuv_output.IsEmpty()); |
| tile->output_is_yuv = true; |
| |
| const GlobalParams& gparams = *tiles_layout->gparams; |
| SyntaxReader reader(dec, tile->rect); |
| WP2_CHECK_STATUS(reader.ReadHeader(features, gparams, *tile)); |
| |
| tile->block_map.Init(tile->rect, gparams.transf_.GetYuvDepth(), |
| gparams.transf_.GetYUVMin(), gparams.transf_.GetYUVMax(), |
| &tile->yuv_output); |
| if (IsFilterBlockMapNeeded(config, gparams, *tiles_layout)) { |
| WP2_CHECK_STATUS(tile->block_map.Allocate()); |
| } |
| |
| RstrFltParams rstr_params(tile->rect.width, tile->rect.height); |
| WP2_CHECK_STATUS(ReadRestorationFilterParams(dec, &rstr_params)); |
| IntratileFilter filter(config, gparams, tile->block_map, rstr_params); |
| WP2_CHECK_STATUS(filter.Allocate()); |
| |
| #if !defined(WP2_REDUCE_BINARY_SIZE) |
| // VisualDebug |
| ArgbBuffer debug_output; // Tile view of 'config.info->debug_output' |
| Plane16 vd_plane; // Visual debug plane. |
| Plane16 vd_block_view; // View of vd_plane for the current block. |
| if (VDMatch(config, "")) { |
| WP2_CHECK_REDUCED_STATUS( |
| debug_output.SetView(config.info->debug_output, tile->rect)); |
| if (VDMatch(config, "prediction/raw") || VDMatch(config, "residuals") || |
| VDMatch(config, "coeff-method") || VDMatch(config, "has-coeffs") || |
| VDMatch(config, "bits-per-pixel") || |
| VDMatch(config, "chroma-from-luma") || VDMatch(config, "a/lossless") || |
| VDMatch(config, "a/lossy") || VDMatch(config, "a/is_lossy")) { |
| WP2_CHECK_STATUS( |
| vd_plane.Resize(tile->yuv_output.Y.w_, tile->yuv_output.Y.h_)); |
| // Areas that are not overwritten will show as black in the debug UI. |
| // This is only relevant for the alpha prediction/residual/modes where |
| // we only show lossy blocks. |
| vd_plane.Fill(-512); |
| } |
| } |
| if (VDMatch(config, "bits-per-pixel") || |
| (config.info != nullptr && config.info->store_blocks)) { |
| #if !defined(WP2_BITTRACE) |
| static bool warning_printed = false; |
| if (!warning_printed) { |
| printf("Warning! VDEBUG_BITS_PER_PIXEL or store_blocks won't work "); |
| printf("without WP2_BITTRACE compile flag!\n"); |
| warning_printed = true; |
| } |
| if (VDMatch(config, "bits-per-pixel")) vd_plane.Clear(); |
| #endif |
| } |
| #endif // WP2_REDUCE_BINARY_SIZE |
| |
| #if defined(WP2_BITTRACE) |
| double blocks_bit_traces_sum = dec->GetBitCount(); // Start with header. |
| // Mapping from block index to block bit count. |
| VectorNoCtor<double> size_bits; |
| #endif |
| |
| FrontMgrBase* mgr; |
| FrontMgrBase mgr_simple; // Simple FrontMgr when use_splits is true. |
| FrontMgrDefault mgr_double_order; // Full FrontMgr when use_splits is false. |
| |
| SplitIteratorDefault it; |
| if (reader.use_splits()) { |
| WP2_CHECK_STATUS( |
| it.Init(reader.partition_set(), tile->rect.width, tile->rect.height)); |
| WP2_CHECK_STATUS(mgr_simple.InitBase(tile->rect.width, tile->rect.height)); |
| mgr = &mgr_simple; |
| } else { |
| WP2_CHECK_STATUS(mgr_double_order.Init( |
| reader.partition_set(), reader.partition_snapping(), tile->rect.width, |
| tile->rect.height)); |
| mgr = &mgr_double_order; |
| } |
| |
| CodedBlockBase cb; |
| cb.SetRange(gparams.transf_.GetYUVMin(), gparams.transf_.GetYUVMax()); |
| cb.mtx_set_ = &gparams.mtx_set_; |
| ContextCache context_cache; |
| cb.SetContextCache(&context_cache); |
| |
| #if defined(WP2_BITTRACE) |
| // Segments for debugging, with quant initialized for encoding. |
| Vector<Segment> debug_segments; |
| if (config.info != nullptr && config.info->store_blocks) { |
| const uint32_t num_segments = gparams.segments_.size(); |
| WP2_CHECK_ALLOC_OK(debug_segments.resize(num_segments)); |
| for (uint32_t i = 0; i < num_segments; ++i) { |
| debug_segments[i].SetYUVBounds(gparams.transf_.GetYUVMin(), |
| gparams.transf_.GetYUVMax()); |
| for (Channel c : {kYChannel, kUChannel, kVChannel, kAChannel}) { |
| gparams.segments_[i].GetQuantSteps( |
| debug_segments[i].quant_steps_[(uint32_t)c], kYChannel); |
| } |
| WP2_CHECK_STATUS(debug_segments[i].AllocateForEncoder()); |
| debug_segments[i].FinalizeQuant(); |
| } |
| } |
| #endif |
| |
| #if defined(WP2_BITTRACE) |
| uint32_t block_idx = 0; |
| #endif |
| while ((dec->GetStatus() == WP2_STATUS_OK) && !mgr->Done()) { |
| Block block; |
| if (reader.use_splits()) { |
| // Read splits until we get an unsplit block. |
| while (true) { |
| #if defined(WP2_BITTRACE) |
| const double prev_bit_count = dec->GetBitCount(); |
| if (size_bits.size() <= block_idx) { |
| WP2_CHECK_ALLOC_OK(size_bits.resize(block_idx + 1)); |
| // Initialize bit cost to 0. The cost of the splits read before the |
| // next unsplittable block will be added to this cost. |
| size_bits[block_idx] = 0; |
| } |
| #endif |
| if (!it.CurrentBlock().splittable) { |
| block = it.CurrentBlock().block; |
| mgr->Use(block); |
| it.NextBlock(); |
| break; |
| } |
| uint32_t split_idx; |
| WP2_CHECK_STATUS(reader.GetBlockSplit(it, &split_idx)); |
| it.SplitCurrentBlock(split_idx); |
| #if defined(WP2_BITTRACE) |
| const double num_bits = dec->GetBitCount() - prev_bit_count; |
| size_bits[block_idx] += num_bits; |
| #endif |
| } |
| } else { |
| // Read dimensions until we get a full context. |
| while (true) { |
| if (mgr_double_order.UseReady(&block)) break; |
| // Read block sizes until a final one can be used. |
| #if defined(WP2_BITTRACE) |
| const double prev_bit_count = dec->GetBitCount(); |
| #endif |
| const BlockSize dim = reader.GetBlockSize(mgr_double_order); |
| WP2_CHECK_ALLOC_OK(mgr_double_order.UseSize(dim, &block)); |
| #if defined(WP2_BITTRACE) |
| const double num_bits = dec->GetBitCount() - prev_bit_count; |
| WP2_CHECK_ALLOC_OK(size_bits.push_back(num_bits)); |
| #endif |
| } |
| } |
| |
| // When reaching the end of a line, it is certain that all rows above |
| // current block are decoded. |
| if (block.x_pix() + block.w_pix() == tile->yuv_output.Y.w_) { |
| const uint32_t num_decoded_yuv_rows = block.y_pix(); |
| const uint32_t num_decoded_rows = filter.Apply(num_decoded_yuv_rows); |
| if (num_decoded_rows > tile->num_decoded_rows) { |
| WP2_CHECK_STATUS(tile->row_progress.AdvanceBy(num_decoded_rows - |
| tile->num_decoded_rows)); |
| tile->num_decoded_rows = num_decoded_rows; |
| } |
| } |
| |
| cb.SetDim(block, *mgr); |
| assert(cb.y_pix() + cb.h_pix() <= tile->yuv_output.Y.h_); |
| |
| #if !defined(WP2_REDUCE_BINARY_SIZE) |
| if (!vd_plane.IsEmpty()) { |
| WP2_CHECK_STATUS(vd_block_view.SetView(vd_plane, cb.AsRect())); |
| } |
| #endif // WP2_REDUCE_BINARY_SIZE |
| |
| // Estimation of the bits-per-pixel for the FilterBlockMap. |
| const uint32_t num_used_bytes_before = dec->GetNumUsedBytes(); |
| |
| // Read the block's syntax and set the output view. |
| #if defined(WP2_BITTRACE) |
| dec->ClearBitTracesCustom(); |
| // Register the "block_size" bit trace during the decoding of the associated |
| // block. |
| dec->GetBitTracesCustom()["block_size"].bits = size_bits[block_idx]; |
| dec->GetBitTracesCustom()["block_size"].num_occurrences = 1; |
| |
| BlockInfo block_info; |
| const double last_bit_pos = dec->GetBitCount(); |
| WP2_CHECK_STATUS(reader.GetBlock(&cb, &tile->yuv_output, &block_info)); |
| #else |
| WP2_CHECK_STATUS(reader.GetBlock(&cb, &tile->yuv_output, /*info=*/nullptr)); |
| #endif |
| |
| // reconstruct |
| for (Channel c : {kYChannel, kUChannel, kVChannel}) { |
| for (uint32_t tf_i = 0; tf_i < cb.GetNumTransforms(c); ++tf_i) { |
| cb.Reconstruct(c, tf_i); |
| |
| if ((VDMatch(config, "prediction/raw") || |
| VDMatch(config, "prediction/modes")) && |
| !VDMatch(config, "lossless") && c == VDChannel(config)) { |
| const Predictors& preds = gparams.GetPredictors(c); |
| if (VDMatch(config, "prediction/raw")) { |
| cb.StorePredictionModes(config, tile->rect, c, tf_i, preds, |
| &vd_plane, nullptr); |
| } else { |
| cb.StorePredictionModes(config, tile->rect, c, tf_i, preds, nullptr, |
| &debug_output); |
| } |
| } |
| } |
| } |
| if (gparams.has_alpha_) { |
| reader.alpha_reader().Reconstruct(&cb); |
| |
| #if !defined(WP2_REDUCE_BINARY_SIZE) |
| if (VDMatch(config, "a/lossless")) { |
| reader.alpha_reader().ReconstructLossless(cb, &vd_block_view); |
| } else if (VDMatch(config, "a/lossy") && cb.HasLossyAlpha()) { |
| WP2_CHECK_STATUS( |
| vd_block_view.Copy(cb.out_.A, /*resize_if_needed=*/false)); |
| } else if (VDMatch(config, "a/is_lossy")) { |
| vd_block_view.Fill(cb.HasLossyAlpha() ? 255 : 0); |
| } else if (VDMatch(config, "a/prediction/raw") && cb.HasLossyAlpha()) { |
| cb.StorePredictionModes(config, tile->rect, kAChannel, /*tf_i=*/0, |
| gparams.a_preds_, &vd_plane, nullptr); |
| } else if (VDMatch(config, "a/prediction/modes") && cb.HasLossyAlpha()) { |
| cb.StorePredictionModes(config, tile->rect, kAChannel, /*tf_i=*/0, |
| gparams.a_preds_, nullptr, &debug_output); |
| } |
| #endif // WP2_REDUCE_BINARY_SIZE |
| } |
| |
| #if defined(WP2_ENC_DEC_DEEP_MATCH) |
| reader.ReadAndCompareRawPixels(cb, cb.out_, dec); // Debug |
| #endif |
| |
| #if !defined(WP2_REDUCE_BINARY_SIZE) |
| // VisualDebug |
| if (VDMatch(config, "chroma-from-luma/prediction")) { |
| const Channel channel = VDChannel(config); |
| const CodedBlockBase::CodingParams saved = *cb.GetCodingParams(channel); |
| assert(gparams.uv_preds_[0]->DependsOnLuma()); |
| cb.GetCodingParams(kUChannel)->pred = gparams.uv_preds_[0]; |
| cb.StorePredictionModes(config, tile->rect, channel, /*tf_i=*/0, |
| gparams.GetPredictors(channel), &vd_plane, |
| nullptr); |
| *cb.GetCodingParams(channel) = saved; |
| } else if (VDMatch(config, "chroma-from-luma/slope") || |
| VDMatch(config, "chroma-from-luma/intercept")) { |
| const bool selected = |
| VDSelected(tile->rect.x, tile->rect.y, cb.AsRect(), config); |
| std::string* const debug_str = |
| selected ? &config.info->selection_info : nullptr; |
| |
| if (VDMatch(config, "slope")) { |
| cb.StoreCflSlope(VDChannel(config), gparams.transf_.GetYUVMin(), |
| gparams.transf_.GetYUVMax(), &vd_plane, debug_str); |
| } else { |
| cb.StoreCflIntercept(VDChannel(config), gparams.transf_.GetYUVMin(), |
| gparams.transf_.GetYUVMax(), &vd_plane, debug_str); |
| } |
| } else if (VDMatch(config, "transform")) { |
| cb.StoreTransform(config, tile->rect.x, tile->rect.y, &debug_output); |
| } else if (VDMatch(config, "residuals")) { |
| const Channel c = VDChannel(config); |
| if (c != kAChannel) { |
| const Segment& segment = gparams.segments_[cb.id_]; |
| cb.StoreResiduals(config, tile->rect.x, tile->rect.y, |
| segment.GetQuant(c), c, &vd_plane); |
| } else if (cb.HasLossyAlpha()) { |
| cb.StoreResiduals(config, tile->rect.x, tile->rect.y, |
| gparams.segments_[0].quant_a_, kAChannel, &vd_plane); |
| } |
| } else if (VDMatch(config, "original") && !VDMatch(config, "histogram")) { |
| cb.AppendOriginalPixels(config, tile->rect.x, tile->rect.y, |
| gparams.transf_, &debug_output); |
| } else if (VDMatch(config, "compressed")) { |
| cb.AppendCompressedPixels(config, tile->rect.x, tile->rect.y, |
| &debug_output); |
| } else if (VDMatch(config, "coeff-method") && !reader.use_aom_coeffs()) { |
| cb.StoreCoeffMethod(config, &vd_plane); |
| } else if (VDMatch(config, "has-coeffs")) { |
| cb.StoreHasCoeffs(config, &vd_plane); |
| } else if (VDMatch(config, "bits-per-pixel")) { |
| reader.StoreBitCost(config, tile->rect.x, tile->rect.y, cb.blk(), |
| &vd_plane); |
| } |
| #endif // WP2_REDUCE_BINARY_SIZE |
| |
| #if defined(WP2_BITTRACE) |
| const double num_bits = |
| dec->GetBitCount() - last_bit_pos + size_bits[block_idx]; |
| double bt_sum = 0; |
| for (const auto& bt : dec->GetBitTracesCustom()) bt_sum += bt.second.bits; |
| // Check that all reads were registered as custom bit traces. |
| if (NormDiff(bt_sum, num_bits) > kDoubleSumTolerance) assert(false); |
| blocks_bit_traces_sum += bt_sum; |
| if (config.info != nullptr && config.info->store_blocks) { |
| cb.ToBlockInfo(reader.use_aom_coeffs(), &block_info); |
| block_info.rect.x += tile->rect.x; |
| block_info.rect.y += tile->rect.y; |
| block_info.bits = num_bits; |
| block_info.bit_traces.clear(); |
| block_info.bit_traces.insert(dec->GetBitTracesCustom().begin(), |
| dec->GetBitTracesCustom().end()); |
| // Requantize and transform coeffs for the debugging UI. |
| for (Channel channel : {kYChannel, kUChannel, kVChannel, kAChannel}) { |
| if (channel == kAChannel && !cb.HasLossyAlpha()) continue; |
| const Segment& segment = |
| debug_segments[(channel == kAChannel) ? 0 : cb.id_]; |
| const CodedBlockBase::CodingParams& params = |
| *cb.GetCodingParams(channel); |
| const BlockSize split_size = GetSplitSize(cb.dim(), params.split_tf); |
| const uint32_t split_w = BlockWidthPix(split_size); |
| const uint32_t split_h = BlockHeightPix(split_size); |
| for (uint32_t tf_i = 0; tf_i < cb.GetNumTransforms(channel); ++tf_i) { |
| int32_t tf_res[kMaxBlockSizePix2]; |
| const bool reduced_transform = |
| ((channel == kUChannel || channel == kVChannel) && cb.is420_); |
| WP2Transform2D(block_info.coeffs[channel][tf_i], params.tf_x(), |
| params.tf_y(), split_w, split_h, tf_res, |
| reduced_transform); |
| uint32_t unused_num_coeffs; |
| int16_t unused_dequantized[kMaxBlockSizePix2]; |
| segment.GetQuant(channel).Quantize( |
| tf_res, cb.tdim(channel), cb.IsFirstCoeffDC(channel), |
| block_info.coeffs[channel][tf_i], &unused_num_coeffs, |
| unused_dequantized); |
| } |
| } |
| |
| WP2_CHECK_STATUS(tiles_layout->assignment_lock.Acquire()); |
| config.info->blocks.push_back(block_info); |
| tiles_layout->assignment_lock.Release(); |
| } |
| if (config.info != nullptr && config.info->bits_per_pixel != nullptr) { |
| config.info->bits_per_pixel->Fill(cb.AsRect(tile->rect.x, tile->rect.y), |
| num_bits / (cb.w_pix() * cb.h_pix())); |
| } |
| #endif |
| |
| uint32_t min_num_used_bytes = ANSDec::GetMinNumUsedBytesDiff( |
| num_used_bytes_before, dec->GetNumUsedBytes()); |
| if (gparams.has_alpha_) { |
| // Don't include lossless alpha. TODO(maryla): remove lossy alpha as well? |
| min_num_used_bytes = |
| SafeSub(min_num_used_bytes, cb.alpha_lossless_bytes_); |
| } |
| tile->block_map.RegisterBlock(cb, (uint32_t)min_num_used_bytes); |
| |
| #if !defined(WP2_REDUCE_BINARY_SIZE) |
| if (VDMatch(config, "blocks")) { |
| WP2_CHECK_STATUS(cb.Draw(config, *tile, gparams, &debug_output)); |
| } |
| #endif // WP2_REDUCE_BINARY_SIZE |
| #if defined(WP2_BITTRACE) |
| ++block_idx; |
| #endif |
| } |
| |
| WP2_CHECK_STATUS(dec->GetStatus()); |
| // Not using the exact whole chunk is an issue. |
| WP2_CHECK_OK(dec->GetNumUsedBytes() == tile->chunk_size, |
| WP2_STATUS_BITSTREAM_ERROR); |
| |
| #if defined(WP2_BITTRACE) |
| // Check that custom bit traces sum up to the expected bit count. |
| if (NormDiff(blocks_bit_traces_sum, dec->GetBitCount()) > |
| kDoubleSumTolerance) { |
| assert(false); |
| } |
| // Verify that the estimated bit count match the tile size. |
| if (NormDiff(dec->GetBitCount(), tile->chunk_size * 8., |
| /*reduce_diff_by=*/kANSPaddingCost) > |
| ANSDec::kBitCountAccuracy) { |
| assert(false); |
| } |
| #endif |
| |
| const uint32_t num_decoded_rows = filter.Apply(tile->rect.height); |
| assert(num_decoded_rows > tile->num_decoded_rows); |
| WP2_CHECK_STATUS( |
| tile->row_progress.AdvanceBy(num_decoded_rows - tile->num_decoded_rows)); |
| tile->num_decoded_rows = num_decoded_rows; |
| assert(tile->num_decoded_rows == tile->rect.height); |
| |
| #if !defined(WP2_REDUCE_BINARY_SIZE) |
| // VisualDebug |
| if (VDMatch(config, "original") || VDMatch(config, "compressed")) { |
| ApplyVDebugBeforeAfter(config, gparams.transf_, *tile, &debug_output); |
| } else if (VDMatch(config, "filter-block-map")) { |
| tile->block_map.ApplyVDebug(config, &debug_output); |
| } else if (!vd_plane.IsEmpty()) { |
| if (VDMatch(config, "a")) { |
| const bool is_res = VDMatch(config, "residuals"); |
| WP2_CHECK_STATUS(vd_plane.ToGray( |
| &debug_output, |
| /*bit_depth=*/{is_res ? 9u : 8u, /*is_signed=*/is_res})); |
| } else { |
| WP2_CHECK_STATUS(vd_plane.ToGray(&debug_output, |
| /*bit_depth=*/{10, /*is_signed=*/true})); |
| } |
| } |
| #endif // WP2_REDUCE_BINARY_SIZE |
| // Only clear data that is no longer valid. The rest might be used by the |
| // IntertileFilter. |
| tile->block_map.pixels_ = nullptr; |
| return WP2_STATUS_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Empty neural decoding |
| |
| #if !defined(WP2_NEURAL) |
| WP2Status NeuralDecode(ArgbBuffer* const picture, ANSDec* const dec, |
| const BitstreamFeatures& features) { |
| (void)picture; |
| (void)dec; |
| (void)features; |
| return WP2_STATUS_UNSUPPORTED_FEATURE; |
| } |
| #endif // !WP2_NEURAL |
| |
| } // namespace WP2 |