2023-06-02 19:24:21 +00:00
|
|
|
// Copyright 2023 Dolphin Emulator Project
|
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
|
|
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
2023-06-12 02:51:49 +00:00
|
|
|
|
|
|
|
#include <fmt/std.h>
|
2023-06-02 19:24:21 +00:00
|
|
|
|
|
|
|
#include "Common/FileUtil.h"
|
|
|
|
#include "Common/Logging/Log.h"
|
|
|
|
#include "Common/StringUtil.h"
|
2023-07-03 02:27:24 +00:00
|
|
|
#include "VideoCommon/Assets/MaterialAsset.h"
|
2023-06-29 05:56:06 +00:00
|
|
|
#include "VideoCommon/Assets/ShaderAsset.h"
|
2023-09-06 05:16:26 +00:00
|
|
|
#include "VideoCommon/Assets/TextureAsset.h"
|
2023-06-02 19:24:21 +00:00
|
|
|
|
|
|
|
namespace VideoCommon
|
|
|
|
{
|
|
|
|
namespace
|
|
|
|
{
|
2023-08-13 21:46:37 +00:00
|
|
|
std::chrono::system_clock::time_point FileTimeToSysTime(std::filesystem::file_time_type file_time)
|
|
|
|
{
|
|
|
|
#ifdef _WIN32
|
|
|
|
return std::chrono::clock_cast<std::chrono::system_clock>(file_time);
|
|
|
|
#else
|
|
|
|
// Note: all compilers should switch to chrono::clock_cast
|
|
|
|
// once it is available for use
|
|
|
|
const auto system_time_now = std::chrono::system_clock::now();
|
|
|
|
const auto file_time_now = decltype(file_time)::clock::now();
|
|
|
|
return std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
|
|
|
file_time - file_time_now + system_time_now);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-06-02 19:24:21 +00:00
|
|
|
std::size_t GetAssetSize(const CustomTextureData& data)
|
|
|
|
{
|
|
|
|
std::size_t total = 0;
|
2023-08-13 21:09:45 +00:00
|
|
|
for (const auto& slice : data.m_slices)
|
2023-06-02 19:24:21 +00:00
|
|
|
{
|
2023-08-13 21:09:45 +00:00
|
|
|
for (const auto& level : slice.m_levels)
|
|
|
|
{
|
|
|
|
total += level.data.size();
|
|
|
|
}
|
2023-06-02 19:24:21 +00:00
|
|
|
}
|
|
|
|
return total;
|
|
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CustomAssetLibrary::TimeType
|
|
|
|
DirectFilesystemAssetLibrary::GetLastAssetWriteTime(const AssetID& asset_id) const
|
|
|
|
{
|
2023-06-05 04:01:29 +00:00
|
|
|
std::lock_guard lk(m_lock);
|
2023-06-02 19:24:21 +00:00
|
|
|
if (auto iter = m_assetid_to_asset_map_path.find(asset_id);
|
|
|
|
iter != m_assetid_to_asset_map_path.end())
|
|
|
|
{
|
|
|
|
const auto& asset_map_path = iter->second;
|
|
|
|
CustomAssetLibrary::TimeType max_entry;
|
|
|
|
for (const auto& [key, value] : asset_map_path)
|
|
|
|
{
|
2023-06-09 03:20:52 +00:00
|
|
|
std::error_code ec;
|
|
|
|
const auto tp = std::filesystem::last_write_time(value, ec);
|
|
|
|
if (ec)
|
|
|
|
continue;
|
2023-08-13 21:46:37 +00:00
|
|
|
auto tp_sys = FileTimeToSysTime(tp);
|
|
|
|
if (tp_sys > max_entry)
|
|
|
|
max_entry = tp_sys;
|
2023-06-02 19:24:21 +00:00
|
|
|
}
|
|
|
|
return max_entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2023-06-29 05:56:06 +00:00
|
|
|
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const AssetID& asset_id,
|
|
|
|
PixelShaderData* data)
|
|
|
|
{
|
|
|
|
const auto asset_map = GetAssetMapForID(asset_id);
|
|
|
|
|
|
|
|
// Asset map for a pixel shader is the shader and some metadata
|
|
|
|
if (asset_map.size() != 2)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have two files mapped!", asset_id);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto metadata = asset_map.find("metadata");
|
|
|
|
const auto shader = asset_map.find("shader");
|
|
|
|
if (metadata == asset_map.end())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a metadata entry mapped!", asset_id);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shader == asset_map.end())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a shader entry mapped!", asset_id);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
std::size_t metadata_size;
|
|
|
|
{
|
|
|
|
std::error_code ec;
|
|
|
|
metadata_size = std::filesystem::file_size(metadata->second, ec);
|
|
|
|
if (ec)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - failed to get shader metadata file size with error '{}'!",
|
|
|
|
asset_id, ec);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
std::size_t shader_size;
|
|
|
|
{
|
|
|
|
std::error_code ec;
|
|
|
|
shader_size = std::filesystem::file_size(shader->second, ec);
|
|
|
|
if (ec)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - failed to get shader source file size with error '{}'!",
|
|
|
|
asset_id, ec);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const auto approx_mem_size = metadata_size + shader_size;
|
|
|
|
|
2023-08-16 07:54:24 +00:00
|
|
|
if (!File::ReadFileToString(PathToString(shader->second), data->m_shader_source))
|
2023-06-29 05:56:06 +00:00
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the shader file '{}',", asset_id,
|
2023-08-16 07:54:24 +00:00
|
|
|
PathToString(shader->second));
|
2023-06-29 05:56:06 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string json_data;
|
2023-08-16 07:54:24 +00:00
|
|
|
if (!File::ReadFileToString(PathToString(metadata->second), json_data))
|
2023-06-29 05:56:06 +00:00
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}',", asset_id,
|
2023-08-16 07:54:24 +00:00
|
|
|
PathToString(metadata->second));
|
2023-06-29 05:56:06 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
picojson::value root;
|
|
|
|
const auto error = picojson::parse(root, json_data);
|
|
|
|
|
|
|
|
if (!error.empty())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - failed to load the json file '{}', due to parse error: {}",
|
2023-08-16 07:54:24 +00:00
|
|
|
asset_id, PathToString(metadata->second), error);
|
2023-06-29 05:56:06 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
if (!root.is<picojson::object>())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(
|
|
|
|
VIDEO,
|
|
|
|
"Asset '{}' error - failed to load the json file '{}', due to root not being an object!",
|
2023-08-16 07:54:24 +00:00
|
|
|
asset_id, PathToString(metadata->second));
|
2023-06-29 05:56:06 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto& root_obj = root.get<picojson::object>();
|
|
|
|
|
|
|
|
if (!PixelShaderData::FromJson(asset_id, root_obj, data))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
|
|
|
|
}
|
|
|
|
|
2023-07-03 02:27:24 +00:00
|
|
|
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const AssetID& asset_id,
|
|
|
|
MaterialData* data)
|
|
|
|
{
|
|
|
|
const auto asset_map = GetAssetMapForID(asset_id);
|
|
|
|
|
|
|
|
// Material is expected to have one asset mapped
|
|
|
|
if (asset_map.empty() || asset_map.size() > 1)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material expected to have one file mapped!", asset_id);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
const auto& asset_path = asset_map.begin()->second;
|
|
|
|
|
|
|
|
std::string json_data;
|
2023-08-16 07:54:24 +00:00
|
|
|
if (!File::ReadFileToString(PathToString(asset_path), json_data))
|
2023-07-03 02:27:24 +00:00
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - material failed to load the json file '{}',",
|
2023-08-16 07:54:24 +00:00
|
|
|
asset_id, PathToString(asset_path));
|
2023-07-03 02:27:24 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
picojson::value root;
|
|
|
|
const auto error = picojson::parse(root, json_data);
|
|
|
|
|
|
|
|
if (!error.empty())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(
|
|
|
|
VIDEO,
|
|
|
|
"Asset '{}' error - material failed to load the json file '{}', due to parse error: {}",
|
2023-08-16 07:54:24 +00:00
|
|
|
asset_id, PathToString(asset_path), error);
|
2023-07-03 02:27:24 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
if (!root.is<picojson::object>())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - material failed to load the json file '{}', due to root not "
|
|
|
|
"being an object!",
|
2023-08-16 07:54:24 +00:00
|
|
|
asset_id, PathToString(asset_path));
|
2023-07-03 02:27:24 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto& root_obj = root.get<picojson::object>();
|
|
|
|
|
|
|
|
if (!MaterialData::FromJson(asset_id, root_obj, data))
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - material failed to load the json file '{}', as material "
|
|
|
|
"json could not be parsed!",
|
2023-08-16 07:54:24 +00:00
|
|
|
asset_id, PathToString(asset_path));
|
2023-07-03 02:27:24 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
return LoadInfo{json_data.size(), GetLastAssetWriteTime(asset_id)};
|
|
|
|
}
|
|
|
|
|
2023-06-02 19:24:21 +00:00
|
|
|
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
|
2023-09-06 05:16:26 +00:00
|
|
|
TextureData* data)
|
2023-06-02 19:24:21 +00:00
|
|
|
{
|
2023-06-05 04:01:29 +00:00
|
|
|
const auto asset_map = GetAssetMapForID(asset_id);
|
2023-06-02 19:24:21 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
// Texture can optionally have a metadata file as well
|
|
|
|
if (asset_map.empty() || asset_map.size() > 2)
|
2023-06-09 00:48:45 +00:00
|
|
|
{
|
2023-09-06 05:16:26 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - raw texture expected to have one or two files mapped!",
|
2023-06-09 00:48:45 +00:00
|
|
|
asset_id);
|
2023-06-05 04:01:29 +00:00
|
|
|
return {};
|
2023-06-09 00:48:45 +00:00
|
|
|
}
|
2023-06-02 19:24:21 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
const auto metadata = asset_map.find("metadata");
|
|
|
|
const auto texture_path = asset_map.find("texture");
|
|
|
|
|
|
|
|
if (texture_path == asset_map.end())
|
2023-06-09 03:20:52 +00:00
|
|
|
{
|
2023-09-06 05:16:26 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a texture entry mapped!", asset_id);
|
2023-06-09 03:20:52 +00:00
|
|
|
return {};
|
|
|
|
}
|
2023-09-06 05:16:26 +00:00
|
|
|
|
|
|
|
std::size_t metadata_size = 0;
|
|
|
|
if (metadata != asset_map.end())
|
|
|
|
{
|
|
|
|
std::error_code ec;
|
|
|
|
metadata_size = std::filesystem::file_size(metadata->second, ec);
|
|
|
|
if (ec)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - failed to get texture metadata file size with error '{}'!",
|
|
|
|
asset_id, ec);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string json_data;
|
|
|
|
if (!File::ReadFileToString(PathToString(metadata->second), json_data))
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - failed to load the json file '{}',", asset_id,
|
|
|
|
PathToString(metadata->second));
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
picojson::value root;
|
|
|
|
const auto error = picojson::parse(root, json_data);
|
|
|
|
|
|
|
|
if (!error.empty())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO,
|
|
|
|
"Asset '{}' error - failed to load the json file '{}', due to parse error: {}",
|
|
|
|
asset_id, PathToString(metadata->second), error);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
if (!root.is<picojson::object>())
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(
|
|
|
|
VIDEO,
|
|
|
|
"Asset '{}' error - failed to load the json file '{}', due to root not being an object!",
|
|
|
|
asset_id, PathToString(metadata->second));
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto& root_obj = root.get<picojson::object>();
|
|
|
|
if (!TextureData::FromJson(asset_id, root_obj, data))
|
|
|
|
{
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
data->m_type = TextureData::Type::Type_Texture2D;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto ext = PathToString(texture_path->second.extension());
|
2023-06-05 04:01:29 +00:00
|
|
|
Common::ToLower(&ext);
|
|
|
|
if (ext == ".dds")
|
|
|
|
{
|
2023-09-06 05:16:26 +00:00
|
|
|
if (!LoadDDSTexture(&data->m_texture, PathToString(texture_path->second)))
|
2023-06-09 00:48:45 +00:00
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load dds texture!", asset_id);
|
2023-06-05 04:01:29 +00:00
|
|
|
return {};
|
2023-06-09 00:48:45 +00:00
|
|
|
}
|
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
if (data->m_texture.m_slices.empty()) [[unlikely]]
|
|
|
|
data->m_texture.m_slices.push_back({});
|
2023-08-13 21:09:45 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
if (!LoadMips(texture_path->second, &data->m_texture.m_slices[0]))
|
2023-06-05 04:01:29 +00:00
|
|
|
return {};
|
2023-06-02 19:24:21 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
return LoadInfo{GetAssetSize(data->m_texture) + metadata_size, GetLastAssetWriteTime(asset_id)};
|
2023-06-05 04:01:29 +00:00
|
|
|
}
|
|
|
|
else if (ext == ".png")
|
|
|
|
{
|
2023-09-06 05:16:26 +00:00
|
|
|
// PNG could support more complicated texture types in the future
|
|
|
|
// but for now just error
|
|
|
|
if (data->m_type != TextureData::Type::Type_Texture2D)
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - PNG is not supported for texture type '{}'!",
|
|
|
|
asset_id, data->m_type);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2023-08-13 21:09:45 +00:00
|
|
|
// If we have no slices, create one
|
2023-09-06 05:16:26 +00:00
|
|
|
if (data->m_texture.m_slices.empty())
|
|
|
|
data->m_texture.m_slices.push_back({});
|
2023-08-13 21:09:45 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
auto& slice = data->m_texture.m_slices[0];
|
2023-06-05 04:01:29 +00:00
|
|
|
// If we have no levels, create one to pass into LoadPNGTexture
|
2023-08-13 21:09:45 +00:00
|
|
|
if (slice.m_levels.empty())
|
|
|
|
slice.m_levels.push_back({});
|
2023-06-03 03:02:37 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
if (!LoadPNGTexture(&slice.m_levels[0], PathToString(texture_path->second)))
|
2023-06-09 00:48:45 +00:00
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id);
|
2023-06-05 04:01:29 +00:00
|
|
|
return {};
|
2023-06-09 00:48:45 +00:00
|
|
|
}
|
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
if (!LoadMips(texture_path->second, &slice))
|
2023-06-05 04:01:29 +00:00
|
|
|
return {};
|
2023-06-02 19:24:21 +00:00
|
|
|
|
2023-09-06 05:16:26 +00:00
|
|
|
return LoadInfo{GetAssetSize(data->m_texture) + metadata_size, GetLastAssetWriteTime(asset_id)};
|
2023-06-02 19:24:21 +00:00
|
|
|
}
|
|
|
|
|
2023-06-09 00:48:45 +00:00
|
|
|
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - extension '{}' unknown!", asset_id, ext);
|
2023-06-02 19:24:21 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2023-06-05 04:01:29 +00:00
|
|
|
void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id,
|
|
|
|
AssetMap asset_path_map)
|
2023-06-02 19:24:21 +00:00
|
|
|
{
|
2023-06-05 04:01:29 +00:00
|
|
|
std::lock_guard lk(m_lock);
|
2023-06-02 19:24:21 +00:00
|
|
|
m_assetid_to_asset_map_path[asset_id] = std::move(asset_path_map);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_path,
|
2023-08-13 21:09:45 +00:00
|
|
|
CustomTextureData::ArraySlice* data)
|
2023-06-02 19:24:21 +00:00
|
|
|
{
|
|
|
|
if (!data) [[unlikely]]
|
|
|
|
return false;
|
|
|
|
|
|
|
|
std::string path;
|
|
|
|
std::string filename;
|
|
|
|
std::string extension;
|
2023-08-16 07:54:24 +00:00
|
|
|
SplitPath(PathToString(asset_path), &path, &filename, &extension);
|
2023-06-02 19:24:21 +00:00
|
|
|
|
|
|
|
std::string extension_lower = extension;
|
|
|
|
Common::ToLower(&extension_lower);
|
|
|
|
|
|
|
|
// Load additional mip levels
|
|
|
|
for (u32 mip_level = static_cast<u32>(data->m_levels.size());; mip_level++)
|
|
|
|
{
|
|
|
|
const auto mip_level_filename = filename + fmt::format("_mip{}", mip_level);
|
|
|
|
|
|
|
|
const auto full_path = path + mip_level_filename + extension;
|
|
|
|
if (!File::Exists(full_path))
|
|
|
|
return true;
|
|
|
|
|
2023-08-13 21:09:45 +00:00
|
|
|
VideoCommon::CustomTextureData::ArraySlice::Level level;
|
2023-06-02 19:24:21 +00:00
|
|
|
if (extension_lower == ".dds")
|
|
|
|
{
|
|
|
|
if (!LoadDDSTexture(&level, full_path, mip_level))
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (extension_lower == ".png")
|
|
|
|
{
|
|
|
|
if (!LoadPNGTexture(&level, full_path))
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' has unsupported extension", mip_level_filename);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
data->m_levels.push_back(std::move(level));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2023-06-05 04:01:29 +00:00
|
|
|
|
|
|
|
DirectFilesystemAssetLibrary::AssetMap
|
|
|
|
DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const
|
|
|
|
{
|
|
|
|
std::lock_guard lk(m_lock);
|
|
|
|
if (auto iter = m_assetid_to_asset_map_path.find(asset_id);
|
|
|
|
iter != m_assetid_to_asset_map_path.end())
|
|
|
|
{
|
|
|
|
return iter->second;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
2023-06-02 19:24:21 +00:00
|
|
|
} // namespace VideoCommon
|