1227 lines
32 KiB
C++
1227 lines
32 KiB
C++
#include "wiHelper.h"
|
|
#include "wiPlatform.h"
|
|
#include "wiBacklog.h"
|
|
#include "wiEventHandler.h"
|
|
#include "wiMath.h"
|
|
|
|
#include "Utility/stb_image_write.h"
|
|
#include "Utility/basis_universal/encoder/basisu_comp.h"
|
|
#include "Utility/basis_universal/encoder/basisu_gpu_texture.h"
|
|
extern basist::etc1_global_selector_codebook g_basis_global_codebook;
|
|
|
|
#include <thread>
|
|
#include <locale>
|
|
#include <chrono>
|
|
#include <iomanip>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <codecvt> // string conversion
|
|
#include <filesystem>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#ifdef PLATFORM_UWP
|
|
#include <winrt/Windows.UI.Popups.h>
|
|
#include <winrt/Windows.Storage.h>
|
|
#include <winrt/Windows.Storage.Pickers.h>
|
|
#include <winrt/Windows.Storage.AccessCache.h>
|
|
#include <winrt/Windows.Storage.Streams.h>
|
|
#include <winrt/Windows.Foundation.h>
|
|
#include <winrt/Windows.Foundation.Collections.h>
|
|
#include <winrt/Windows.System.h>
|
|
#else
|
|
#include <Commdlg.h> // openfile
|
|
#include <WinBase.h>
|
|
#endif // PLATFORM_UWP
|
|
#else
|
|
#include "Utility/portable-file-dialogs.h"
|
|
#endif // _WIN32
|
|
|
|
|
|
namespace wi::helper
|
|
{
|
|
|
|
std::string toUpper(const std::string& s)
|
|
{
|
|
std::string result;
|
|
std::locale loc;
|
|
for (unsigned int i = 0; i < s.length(); ++i)
|
|
{
|
|
result += std::toupper(s.at(i), loc);
|
|
}
|
|
return result;
|
|
}
|
|
std::string toLower(const std::string& s)
|
|
{
|
|
std::string result;
|
|
std::locale loc;
|
|
for (unsigned int i = 0; i < s.length(); ++i)
|
|
{
|
|
result += std::tolower(s.at(i), loc);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void messageBox(const std::string& msg, const std::string& caption)
|
|
{
|
|
#ifdef _WIN32
|
|
#ifndef PLATFORM_UWP
|
|
MessageBoxA(GetActiveWindow(), msg.c_str(), caption.c_str(), 0);
|
|
#else
|
|
std::wstring wmessage, wcaption;
|
|
StringConvert(msg, wmessage);
|
|
StringConvert(caption, wcaption);
|
|
// UWP can only show message box on main thread:
|
|
wi::eventhandler::Subscribe_Once(wi::eventhandler::EVENT_THREAD_SAFE_POINT, [=](uint64_t userdata) {
|
|
winrt::Windows::UI::Popups::MessageDialog(wmessage, wcaption).ShowAsync();
|
|
});
|
|
#endif // PLATFORM_UWP
|
|
#elif SDL2
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, caption.c_str(), msg.c_str(), NULL);
|
|
#endif // _WIN32
|
|
}
|
|
|
|
void screenshot(const wi::graphics::SwapChain& swapchain, const std::string& name)
|
|
{
|
|
std::string directory;
|
|
if (name.empty())
|
|
{
|
|
directory = std::filesystem::current_path().string() + "/screenshots";
|
|
}
|
|
else
|
|
{
|
|
directory = GetDirectoryFromPath(name);
|
|
}
|
|
|
|
if(!directory.empty()) //PE: Crash if only filename is used with no folder.
|
|
DirectoryCreate(directory);
|
|
|
|
std::string filename = name;
|
|
if (filename.empty())
|
|
{
|
|
filename = directory + "/sc_" + getCurrentDateTimeAsString() + ".jpg";
|
|
}
|
|
|
|
bool result = saveTextureToFile(wi::graphics::GetDevice()->GetBackBuffer(&swapchain), filename);
|
|
assert(result);
|
|
|
|
if (result)
|
|
{
|
|
wi::backlog::post("Screenshot saved: " + filename);
|
|
}
|
|
}
|
|
|
|
bool saveTextureToMemory(const wi::graphics::Texture& texture, wi::vector<uint8_t>& texturedata)
|
|
{
|
|
using namespace wi::graphics;
|
|
|
|
GraphicsDevice* device = wi::graphics::GetDevice();
|
|
|
|
TextureDesc desc = texture.GetDesc();
|
|
|
|
Texture stagingTex;
|
|
TextureDesc staging_desc = desc;
|
|
staging_desc.usage = Usage::READBACK;
|
|
staging_desc.layout = ResourceState::COPY_DST;
|
|
staging_desc.bind_flags = BindFlag::NONE;
|
|
staging_desc.misc_flags = ResourceMiscFlag::NONE;
|
|
bool success = device->CreateTexture(&staging_desc, nullptr, &stagingTex);
|
|
assert(success);
|
|
|
|
CommandList cmd = device->BeginCommandList();
|
|
|
|
{
|
|
GPUBarrier barriers[] = {
|
|
GPUBarrier::Image(&texture,texture.desc.layout,ResourceState::COPY_SRC),
|
|
};
|
|
device->Barrier(barriers, arraysize(barriers), cmd);
|
|
}
|
|
|
|
device->CopyResource(&stagingTex, &texture, cmd);
|
|
|
|
{
|
|
GPUBarrier barriers[] = {
|
|
GPUBarrier::Image(&texture,ResourceState::COPY_SRC,texture.desc.layout),
|
|
};
|
|
device->Barrier(barriers, arraysize(barriers), cmd);
|
|
}
|
|
|
|
device->SubmitCommandLists();
|
|
device->WaitForGPU();
|
|
|
|
texturedata.clear();
|
|
|
|
if (stagingTex.mapped_data != nullptr)
|
|
{
|
|
texturedata.resize(stagingTex.mapped_size);
|
|
|
|
const uint32_t data_stride = GetFormatStride(desc.format);
|
|
const uint32_t block_size = GetFormatBlockSize(desc.format);
|
|
const uint32_t num_blocks_x = desc.width / block_size;
|
|
const uint32_t num_blocks_y = desc.height / block_size;
|
|
size_t cpy_offset = 0;
|
|
size_t subresourceIndex = 0;
|
|
for (uint32_t layer = 0; layer < desc.array_size; ++layer)
|
|
{
|
|
uint32_t mip_width = num_blocks_x;
|
|
uint32_t mip_height = num_blocks_y;
|
|
uint32_t mip_depth = desc.depth;
|
|
for (uint32_t mip = 0; mip < desc.mip_levels; ++mip)
|
|
{
|
|
assert(subresourceIndex < stagingTex.mapped_subresource_count);
|
|
const SubresourceData& subresourcedata = stagingTex.mapped_subresources[subresourceIndex++];
|
|
const size_t dst_rowpitch = mip_width * data_stride;
|
|
for (uint32_t z = 0; z < mip_depth; ++z)
|
|
{
|
|
uint8_t* dst_slice = texturedata.data() + cpy_offset;
|
|
uint8_t* src_slice = (uint8_t*)subresourcedata.data_ptr + subresourcedata.slice_pitch * z;
|
|
for (uint32_t i = 0; i < mip_height; ++i)
|
|
{
|
|
std::memcpy(
|
|
dst_slice + i * dst_rowpitch,
|
|
src_slice + i * subresourcedata.row_pitch,
|
|
subresourcedata.row_pitch
|
|
);
|
|
}
|
|
cpy_offset += mip_height * dst_rowpitch;
|
|
}
|
|
|
|
mip_width = std::max(1u, mip_width / 2);
|
|
mip_height = std::max(1u, mip_height / 2);
|
|
mip_depth = std::max(1u, mip_depth / 2);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
assert(0);
|
|
}
|
|
|
|
return stagingTex.mapped_data != nullptr;
|
|
}
|
|
|
|
bool saveTextureToMemoryFile(const wi::graphics::Texture& texture, const std::string& fileExtension, wi::vector<uint8_t>& filedata)
|
|
{
|
|
using namespace wi::graphics;
|
|
TextureDesc desc = texture.GetDesc();
|
|
wi::vector<uint8_t> texturedata;
|
|
if (saveTextureToMemory(texture, texturedata))
|
|
{
|
|
return saveTextureToMemoryFile(texturedata, desc, fileExtension, filedata);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool saveTextureToMemoryFile(const wi::vector<uint8_t>& texturedata, const wi::graphics::TextureDesc& desc, const std::string& fileExtension, wi::vector<uint8_t>& filedata)
|
|
{
|
|
using namespace wi::graphics;
|
|
const uint32_t data_stride = GetFormatStride(desc.format);
|
|
|
|
struct MipDesc
|
|
{
|
|
const uint8_t* address = nullptr;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
uint32_t depth = 0;
|
|
};
|
|
wi::vector<MipDesc> mips;
|
|
mips.reserve(desc.mip_levels);
|
|
|
|
uint32_t data_count = 0;
|
|
uint32_t mip_width = desc.width;
|
|
uint32_t mip_height = desc.height;
|
|
uint32_t mip_depth = desc.depth;
|
|
for (uint32_t mip = 0; mip < desc.mip_levels; ++mip)
|
|
{
|
|
MipDesc& mipdesc = mips.emplace_back();
|
|
mipdesc.address = texturedata.data() + data_count * data_stride;
|
|
data_count += mip_width * mip_height * mip_depth;
|
|
mipdesc.width = mip_width;
|
|
mipdesc.height = mip_height;
|
|
mipdesc.depth = mip_depth;
|
|
mip_width = std::max(1u, mip_width / 2);
|
|
mip_height = std::max(1u, mip_height / 2);
|
|
mip_depth = std::max(1u, mip_depth / 2);
|
|
}
|
|
|
|
std::string extension = wi::helper::toUpper(fileExtension);
|
|
bool basis = !extension.compare("BASIS");
|
|
bool ktx2 = !extension.compare("KTX2");
|
|
basisu::image basis_image;
|
|
basisu::vector<basisu::image> basis_mipmaps;
|
|
|
|
int dst_channel_count = 4;
|
|
if (desc.format == Format::R10G10B10A2_UNORM)
|
|
{
|
|
// This will be converted first to rgba8 before saving to common format:
|
|
uint32_t* data32 = (uint32_t*)texturedata.data();
|
|
|
|
for (uint32_t i = 0; i < data_count; ++i)
|
|
{
|
|
uint32_t pixel = data32[i];
|
|
float r = ((pixel >> 0) & 1023) / 1023.0f;
|
|
float g = ((pixel >> 10) & 1023) / 1023.0f;
|
|
float b = ((pixel >> 20) & 1023) / 1023.0f;
|
|
float a = ((pixel >> 30) & 3) / 3.0f;
|
|
|
|
uint32_t rgba8 = 0;
|
|
rgba8 |= (uint32_t)(r * 255.0f) << 0;
|
|
rgba8 |= (uint32_t)(g * 255.0f) << 8;
|
|
rgba8 |= (uint32_t)(b * 255.0f) << 16;
|
|
rgba8 |= (uint32_t)(a * 255.0f) << 24;
|
|
|
|
data32[i] = rgba8;
|
|
}
|
|
}
|
|
else if (desc.format == Format::R32G32B32A32_FLOAT)
|
|
{
|
|
// This will be converted first to rgba8 before saving to common format:
|
|
XMFLOAT4* dataSrc = (XMFLOAT4*)texturedata.data();
|
|
uint32_t* data32 = (uint32_t*)texturedata.data();
|
|
|
|
for (uint32_t i = 0; i < data_count; ++i)
|
|
{
|
|
XMFLOAT4 pixel = dataSrc[i];
|
|
float r = std::max(0.0f, std::min(pixel.x, 1.0f));
|
|
float g = std::max(0.0f, std::min(pixel.y, 1.0f));
|
|
float b = std::max(0.0f, std::min(pixel.z, 1.0f));
|
|
float a = std::max(0.0f, std::min(pixel.w, 1.0f));
|
|
|
|
uint32_t rgba8 = 0;
|
|
rgba8 |= (uint32_t)(r * 255.0f) << 0;
|
|
rgba8 |= (uint32_t)(g * 255.0f) << 8;
|
|
rgba8 |= (uint32_t)(b * 255.0f) << 16;
|
|
rgba8 |= (uint32_t)(a * 255.0f) << 24;
|
|
|
|
data32[i] = rgba8;
|
|
}
|
|
}
|
|
else if (desc.format == Format::R16G16B16A16_FLOAT)
|
|
{
|
|
// This will be converted first to rgba8 before saving to common format:
|
|
XMHALF4* dataSrc = (XMHALF4*)texturedata.data();
|
|
uint32_t* data32 = (uint32_t*)texturedata.data();
|
|
|
|
for (uint32_t i = 0; i < data_count; ++i)
|
|
{
|
|
XMHALF4 pixel = dataSrc[i];
|
|
float r = std::max(0.0f, std::min(XMConvertHalfToFloat(pixel.x), 1.0f));
|
|
float g = std::max(0.0f, std::min(XMConvertHalfToFloat(pixel.y), 1.0f));
|
|
float b = std::max(0.0f, std::min(XMConvertHalfToFloat(pixel.z), 1.0f));
|
|
float a = std::max(0.0f, std::min(XMConvertHalfToFloat(pixel.w), 1.0f));
|
|
|
|
uint32_t rgba8 = 0;
|
|
rgba8 |= (uint32_t)(r * 255.0f) << 0;
|
|
rgba8 |= (uint32_t)(g * 255.0f) << 8;
|
|
rgba8 |= (uint32_t)(b * 255.0f) << 16;
|
|
rgba8 |= (uint32_t)(a * 255.0f) << 24;
|
|
|
|
data32[i] = rgba8;
|
|
}
|
|
}
|
|
else if (desc.format == Format::R11G11B10_FLOAT)
|
|
{
|
|
// This will be converted first to rgba8 before saving to common format:
|
|
XMFLOAT3PK* dataSrc = (XMFLOAT3PK*)texturedata.data();
|
|
uint32_t* data32 = (uint32_t*)texturedata.data();
|
|
|
|
for (uint32_t i = 0; i < data_count; ++i)
|
|
{
|
|
XMFLOAT3PK pixel = dataSrc[i];
|
|
XMVECTOR V = XMLoadFloat3PK(&pixel);
|
|
XMFLOAT3 pixel3;
|
|
XMStoreFloat3(&pixel3, V);
|
|
float r = std::max(0.0f, std::min(pixel3.x, 1.0f));
|
|
float g = std::max(0.0f, std::min(pixel3.y, 1.0f));
|
|
float b = std::max(0.0f, std::min(pixel3.z, 1.0f));
|
|
float a = 1;
|
|
|
|
uint32_t rgba8 = 0;
|
|
rgba8 |= (uint32_t)(r * 255.0f) << 0;
|
|
rgba8 |= (uint32_t)(g * 255.0f) << 8;
|
|
rgba8 |= (uint32_t)(b * 255.0f) << 16;
|
|
rgba8 |= (uint32_t)(a * 255.0f) << 24;
|
|
|
|
data32[i] = rgba8;
|
|
}
|
|
}
|
|
else if (desc.format == Format::R8_UNORM)
|
|
{
|
|
// This can be saved by reducing target channel count, no conversion needed
|
|
dst_channel_count = 1;
|
|
}
|
|
else if (IsFormatBlockCompressed(desc.format))
|
|
{
|
|
basisu::texture_format fmt;
|
|
switch (desc.format)
|
|
{
|
|
default:
|
|
assert(0);
|
|
return false;
|
|
case Format::BC1_UNORM:
|
|
case Format::BC1_UNORM_SRGB:
|
|
fmt = basisu::texture_format::cBC1;
|
|
break;
|
|
case Format::BC3_UNORM:
|
|
case Format::BC3_UNORM_SRGB:
|
|
fmt = basisu::texture_format::cBC3;
|
|
break;
|
|
case Format::BC4_UNORM:
|
|
fmt = basisu::texture_format::cBC4;
|
|
break;
|
|
case Format::BC5_UNORM:
|
|
fmt = basisu::texture_format::cBC5;
|
|
break;
|
|
case Format::BC7_UNORM:
|
|
case Format::BC7_UNORM_SRGB:
|
|
fmt = basisu::texture_format::cBC7;
|
|
break;
|
|
}
|
|
basisu::gpu_image basis_gpu_image;
|
|
basis_gpu_image.init(fmt, desc.width, desc.height);
|
|
std::memcpy(basis_gpu_image.get_ptr(), texturedata.data(), std::min(texturedata.size(), (size_t)basis_gpu_image.get_size_in_bytes()));
|
|
basis_gpu_image.unpack(basis_image);
|
|
}
|
|
else
|
|
{
|
|
assert(desc.format == Format::R8G8B8A8_UNORM); // If you need to save other texture format, implement data conversion for it
|
|
}
|
|
|
|
if (basis || ktx2)
|
|
{
|
|
if (basis_image.get_total_pixels() == 0)
|
|
{
|
|
basis_image.init(texturedata.data(), desc.width, desc.height, 4);
|
|
if (desc.mip_levels > 1)
|
|
{
|
|
basis_mipmaps.reserve(desc.mip_levels - 1);
|
|
for (uint32_t mip = 1; mip < desc.mip_levels; ++mip)
|
|
{
|
|
basisu::image basis_mip;
|
|
const MipDesc& mipdesc = mips[mip];
|
|
basis_mip.init(mipdesc.address, mipdesc.width, mipdesc.height, 4);
|
|
basis_mipmaps.push_back(basis_mip);
|
|
}
|
|
}
|
|
}
|
|
basisu::basis_compressor_params params;
|
|
params.m_source_images.push_back(basis_image);
|
|
if (desc.mip_levels > 1)
|
|
{
|
|
params.m_source_mipmap_images.push_back(basis_mipmaps);
|
|
}
|
|
if (ktx2)
|
|
{
|
|
params.m_create_ktx2_file = true;
|
|
}
|
|
else
|
|
{
|
|
params.m_create_ktx2_file = false;
|
|
}
|
|
#if 1
|
|
params.m_compression_level = basisu::BASISU_DEFAULT_COMPRESSION_LEVEL;
|
|
#else
|
|
params.m_compression_level = basisu::BASISU_MAX_COMPRESSION_LEVEL;
|
|
#endif
|
|
// Disable CPU mipmap generation:
|
|
// instead we provide mipmap data that was downloaded from the GPU with m_source_mipmap_images.
|
|
// This is better, because engine specific mipgen options will be retained, such as coverage preserving mipmaps
|
|
params.m_mip_gen = false;
|
|
params.m_pSel_codebook = &g_basis_global_codebook;
|
|
params.m_quality_level = basisu::BASISU_QUALITY_MAX;
|
|
params.m_multithreading = true;
|
|
int num_threads = std::max(1u, std::thread::hardware_concurrency());
|
|
basisu::job_pool jpool(num_threads);
|
|
params.m_pJob_pool = &jpool;
|
|
basisu::basis_compressor compressor;
|
|
if (compressor.init(params))
|
|
{
|
|
auto result = compressor.process();
|
|
if (result == basisu::basis_compressor::cECSuccess)
|
|
{
|
|
if (basis)
|
|
{
|
|
const auto& basis_file = compressor.get_output_basis_file();
|
|
filedata.resize(basis_file.size());
|
|
std::memcpy(filedata.data(), basis_file.data(), basis_file.size());
|
|
return true;
|
|
}
|
|
else if (ktx2)
|
|
{
|
|
const auto& ktx2_file = compressor.get_output_ktx2_file();
|
|
filedata.resize(ktx2_file.size());
|
|
std::memcpy(filedata.data(), ktx2_file.data(), ktx2_file.size());
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wi::backlog::post("basisu::basis_compressor::process() failure!", wi::backlog::LogLevel::Error);
|
|
assert(0);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int write_result = 0;
|
|
|
|
filedata.clear();
|
|
stbi_write_func* func = [](void* context, void* data, int size) {
|
|
wi::vector<uint8_t>& filedata = *(wi::vector<uint8_t>*)context;
|
|
for (int i = 0; i < size; ++i)
|
|
{
|
|
filedata.push_back(*((uint8_t*)data + i));
|
|
}
|
|
};
|
|
|
|
const void* src_data = texturedata.data();
|
|
if (basis_image.get_width() > 0 && basis_image.get_ptr() != nullptr)
|
|
{
|
|
src_data = basis_image.get_ptr();
|
|
}
|
|
|
|
static int mip_request = 0; // you can use this while debugging to write specific mip level to file (todo: option param?)
|
|
const MipDesc& mip = mips[mip_request];
|
|
|
|
if (!extension.compare("JPG") || !extension.compare("JPEG"))
|
|
{
|
|
write_result = stbi_write_jpg_to_func(func, &filedata, (int)mip.width, (int)mip.height, dst_channel_count, mip.address, 100);
|
|
}
|
|
else if (!extension.compare("PNG"))
|
|
{
|
|
write_result = stbi_write_png_to_func(func, &filedata, (int)mip.width, (int)mip.height, dst_channel_count, mip.address, 0);
|
|
}
|
|
else if (!extension.compare("TGA"))
|
|
{
|
|
write_result = stbi_write_tga_to_func(func, &filedata, (int)mip.width, (int)mip.height, dst_channel_count, mip.address);
|
|
}
|
|
else if (!extension.compare("BMP"))
|
|
{
|
|
write_result = stbi_write_bmp_to_func(func, &filedata, (int)mip.width, (int)mip.height, dst_channel_count, mip.address);
|
|
}
|
|
else
|
|
{
|
|
assert(0 && "Unsupported extension");
|
|
}
|
|
|
|
return write_result != 0;
|
|
}
|
|
|
|
bool saveTextureToFile(const wi::graphics::Texture& texture, const std::string& fileName)
|
|
{
|
|
using namespace wi::graphics;
|
|
TextureDesc desc = texture.GetDesc();
|
|
wi::vector<uint8_t> data;
|
|
if (saveTextureToMemory(texture, data))
|
|
{
|
|
return saveTextureToFile(data, desc, fileName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool saveTextureToFile(const wi::vector<uint8_t>& texturedata, const wi::graphics::TextureDesc& desc, const std::string& fileName)
|
|
{
|
|
using namespace wi::graphics;
|
|
|
|
std::string ext = GetExtensionFromFileName(fileName);
|
|
wi::vector<uint8_t> filedata;
|
|
if (saveTextureToMemoryFile(texturedata, desc, ext, filedata))
|
|
{
|
|
return FileWrite(fileName, filedata.data(), filedata.size());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
std::string getCurrentDateTimeAsString()
|
|
{
|
|
time_t t = std::time(nullptr);
|
|
struct tm time_info;
|
|
#ifdef _WIN32
|
|
localtime_s(&time_info, &t);
|
|
#else
|
|
localtime(&t);
|
|
#endif
|
|
std::stringstream ss("");
|
|
ss << std::put_time(&time_info, "%d-%m-%Y %H-%M-%S");
|
|
return ss.str();
|
|
}
|
|
|
|
void SplitPath(const std::string& fullPath, std::string& dir, std::string& fileName)
|
|
{
|
|
size_t found;
|
|
found = fullPath.find_last_of("/\\");
|
|
dir = fullPath.substr(0, found + 1);
|
|
fileName = fullPath.substr(found + 1);
|
|
}
|
|
|
|
std::string GetFileNameFromPath(const std::string& fullPath)
|
|
{
|
|
if (fullPath.empty())
|
|
{
|
|
return fullPath;
|
|
}
|
|
|
|
std::string ret, empty;
|
|
SplitPath(fullPath, empty, ret);
|
|
return ret;
|
|
}
|
|
|
|
std::string GetDirectoryFromPath(const std::string& fullPath)
|
|
{
|
|
if (fullPath.empty())
|
|
{
|
|
return fullPath;
|
|
}
|
|
|
|
std::string ret, empty;
|
|
SplitPath(fullPath, ret, empty);
|
|
return ret;
|
|
}
|
|
|
|
std::string GetExtensionFromFileName(const std::string& filename)
|
|
{
|
|
size_t idx = filename.rfind('.');
|
|
|
|
if (idx != std::string::npos)
|
|
{
|
|
std::string extension = filename.substr(idx + 1);
|
|
return extension;
|
|
}
|
|
|
|
// No extension found
|
|
return "";
|
|
}
|
|
|
|
std::string ReplaceExtension(const std::string& filename, const std::string& extension)
|
|
{
|
|
size_t idx = filename.rfind('.');
|
|
|
|
if (idx == std::string::npos)
|
|
{
|
|
// extension not found, append it:
|
|
return filename + "." + extension;
|
|
}
|
|
return filename.substr(0, idx + 1) + extension;
|
|
}
|
|
|
|
std::string ForceExtension(const std::string& filename, const std::string& extension)
|
|
{
|
|
std::string ext = "." + extension;
|
|
if (ext.length() > filename.length())
|
|
return filename + ext;
|
|
|
|
if (filename.substr(filename.length() - ext.length()).compare(ext))
|
|
{
|
|
return filename + ext;
|
|
}
|
|
return filename;
|
|
}
|
|
|
|
std::string RemoveExtension(const std::string& filename)
|
|
{
|
|
size_t idx = filename.rfind('.');
|
|
|
|
if (idx == std::string::npos)
|
|
{
|
|
// extension not found:
|
|
return filename;
|
|
}
|
|
return filename.substr(0, idx);
|
|
}
|
|
|
|
void MakePathRelative(const std::string& rootdir, std::string& path)
|
|
{
|
|
if (rootdir.empty() || path.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
#ifdef PLATFORM_UWP
|
|
|
|
size_t found = path.rfind(rootdir);
|
|
if (found != std::string::npos)
|
|
{
|
|
path = path.substr(found + rootdir.length());
|
|
}
|
|
|
|
#else
|
|
|
|
std::filesystem::path filepath = path;
|
|
if (filepath.is_absolute())
|
|
{
|
|
std::filesystem::path rootpath = rootdir;
|
|
std::filesystem::path relative = std::filesystem::relative(path, rootdir);
|
|
if (!relative.empty())
|
|
{
|
|
path = relative.string();
|
|
}
|
|
}
|
|
|
|
#endif // PLATFORM_UWP
|
|
|
|
}
|
|
|
|
void MakePathAbsolute(std::string& path)
|
|
{
|
|
std::filesystem::path filepath = path;
|
|
std::filesystem::path absolute = std::filesystem::absolute(path);
|
|
if (!absolute.empty())
|
|
{
|
|
path = absolute.string();
|
|
}
|
|
}
|
|
|
|
void DirectoryCreate(const std::string& path)
|
|
{
|
|
std::filesystem::create_directories(path);
|
|
}
|
|
|
|
template<template<typename T, typename A> typename vector_interface>
|
|
bool FileRead_Impl(const std::string& fileName, vector_interface<uint8_t, std::allocator<uint8_t>>& data)
|
|
{
|
|
#ifndef PLATFORM_UWP
|
|
#ifdef SDL_FILESYSTEM_UNIX
|
|
std::string filepath = fileName;
|
|
std::replace(filepath.begin(), filepath.end(), '\\', '/');
|
|
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
|
#else
|
|
std::ifstream file(fileName, std::ios::binary | std::ios::ate);
|
|
#endif // SDL_FILESYSTEM_UNIX
|
|
if (file.is_open())
|
|
{
|
|
size_t dataSize = (size_t)file.tellg();
|
|
file.seekg(0, file.beg);
|
|
data.resize(dataSize);
|
|
file.read((char*)data.data(), dataSize);
|
|
file.close();
|
|
return true;
|
|
}
|
|
#else
|
|
using namespace winrt::Windows::Storage;
|
|
using namespace winrt::Windows::Storage::Streams;
|
|
using namespace winrt::Windows::Foundation;
|
|
std::wstring wstr;
|
|
std::filesystem::path filepath = fileName;
|
|
filepath = std::filesystem::absolute(filepath);
|
|
StringConvert(filepath.string(), wstr);
|
|
bool success = false;
|
|
|
|
auto async_helper = [&]() -> IAsyncAction {
|
|
try
|
|
{
|
|
auto file = co_await StorageFile::GetFileFromPathAsync(wstr);
|
|
auto buffer = co_await FileIO::ReadBufferAsync(file);
|
|
auto reader = DataReader::FromBuffer(buffer);
|
|
auto size = buffer.Length();
|
|
data.resize((size_t)size);
|
|
for (auto& x : data)
|
|
{
|
|
x = reader.ReadByte();
|
|
}
|
|
success = true;
|
|
}
|
|
catch (winrt::hresult_error const& ex)
|
|
{
|
|
switch (ex.code())
|
|
{
|
|
case E_ACCESSDENIED:
|
|
wi::backlog::post("Opening file failed: " + fileName + " | Reason: Permission Denied!");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
if (winrt::impl::is_sta_thread())
|
|
{
|
|
std::thread([&] { async_helper().get(); }).join(); // can't block coroutine from ui thread
|
|
}
|
|
else
|
|
{
|
|
async_helper().get();
|
|
}
|
|
|
|
if (success)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#endif // PLATFORM_UWP
|
|
|
|
wi::backlog::post("File not found: " + fileName, wi::backlog::LogLevel::Warning);
|
|
return false;
|
|
}
|
|
bool FileRead(const std::string& fileName, wi::vector<uint8_t>& data)
|
|
{
|
|
return FileRead_Impl(fileName, data);
|
|
}
|
|
#if WI_VECTOR_TYPE
|
|
bool FileRead(const std::string& fileName, std::vector<uint8_t>& data)
|
|
{
|
|
return FileRead_Impl(fileName, data);
|
|
}
|
|
#endif // WI_VECTOR_TYPE
|
|
|
|
bool FileWrite(const std::string& fileName, const uint8_t* data, size_t size)
|
|
{
|
|
if (size <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#ifndef PLATFORM_UWP
|
|
std::ofstream file(fileName, std::ios::binary | std::ios::trunc);
|
|
if (file.is_open())
|
|
{
|
|
file.write((const char*)data, (std::streamsize)size);
|
|
file.close();
|
|
return true;
|
|
}
|
|
#else
|
|
|
|
using namespace winrt::Windows::Storage;
|
|
using namespace winrt::Windows::Storage::Streams;
|
|
using namespace winrt::Windows::Foundation;
|
|
std::wstring wstr;
|
|
std::filesystem::path filepath = fileName;
|
|
filepath = std::filesystem::absolute(filepath);
|
|
StringConvert(filepath.string(), wstr);
|
|
|
|
CREATEFILE2_EXTENDED_PARAMETERS params = {};
|
|
params.dwSize = (DWORD)size;
|
|
params.dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
|
|
HANDLE filehandle = CreateFile2FromAppW(wstr.c_str(), GENERIC_READ | GENERIC_WRITE, 0, CREATE_ALWAYS, ¶ms);
|
|
assert(filehandle);
|
|
CloseHandle(filehandle);
|
|
|
|
bool success = false;
|
|
auto async_helper = [&]() -> IAsyncAction {
|
|
try
|
|
{
|
|
auto file = co_await StorageFile::GetFileFromPathAsync(wstr);
|
|
winrt::array_view<const uint8_t> dataarray(data, (winrt::array_view<const uint8_t>::size_type)size);
|
|
co_await FileIO::WriteBytesAsync(file, dataarray);
|
|
success = true;
|
|
}
|
|
catch (winrt::hresult_error const& ex)
|
|
{
|
|
switch (ex.code())
|
|
{
|
|
case E_ACCESSDENIED:
|
|
wi::backlog::post("Opening file failed: " + fileName + " | Reason: Permission Denied!");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
if (winrt::impl::is_sta_thread())
|
|
{
|
|
std::thread([&] { async_helper().get(); }).join(); // can't block coroutine from ui thread
|
|
}
|
|
else
|
|
{
|
|
async_helper().get();
|
|
}
|
|
|
|
if (success)
|
|
{
|
|
return true;
|
|
}
|
|
#endif // PLATFORM_UWP
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FileExists(const std::string& fileName)
|
|
{
|
|
#ifndef PLATFORM_UWP
|
|
bool exists = std::filesystem::exists(fileName);
|
|
return exists;
|
|
#else
|
|
using namespace winrt::Windows::Storage;
|
|
using namespace winrt::Windows::Storage::Streams;
|
|
using namespace winrt::Windows::Foundation;
|
|
std::wstring wstr;
|
|
std::filesystem::path filepath = fileName;
|
|
filepath = std::filesystem::absolute(filepath);
|
|
StringConvert(filepath.string(), wstr);
|
|
bool success = false;
|
|
|
|
auto async_helper = [&]() -> IAsyncAction {
|
|
try
|
|
{
|
|
auto file = co_await StorageFile::GetFileFromPathAsync(wstr);
|
|
success = true;
|
|
}
|
|
catch (winrt::hresult_error const& ex)
|
|
{
|
|
switch (ex.code())
|
|
{
|
|
case E_ACCESSDENIED:
|
|
wi::backlog::post("Opening file failed: " + fileName + " | Reason: Permission Denied!");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
if (winrt::impl::is_sta_thread())
|
|
{
|
|
std::thread([&] { async_helper().get(); }).join(); // can't block coroutine from ui thread
|
|
}
|
|
else
|
|
{
|
|
async_helper().get();
|
|
}
|
|
|
|
return success;
|
|
#endif // PLATFORM_UWP
|
|
}
|
|
|
|
std::string GetTempDirectoryPath()
|
|
{
|
|
auto path = std::filesystem::temp_directory_path();
|
|
return path.string();
|
|
}
|
|
|
|
std::string GetCurrentPath()
|
|
{
|
|
auto path = std::filesystem::current_path();
|
|
return path.string();
|
|
}
|
|
|
|
void FileDialog(const FileDialogParams& params, std::function<void(std::string fileName)> onSuccess)
|
|
{
|
|
#ifdef _WIN32
|
|
#ifndef PLATFORM_UWP
|
|
|
|
std::thread([=] {
|
|
|
|
char szFile[256];
|
|
|
|
OPENFILENAMEA ofn;
|
|
ZeroMemory(&ofn, sizeof(ofn));
|
|
ofn.lStructSize = sizeof(ofn);
|
|
ofn.hwndOwner = nullptr;
|
|
ofn.lpstrFile = szFile;
|
|
// Set lpstrFile[0] to '\0' so that GetOpenFileName does not
|
|
// use the contents of szFile to initialize itself.
|
|
ofn.lpstrFile[0] = '\0';
|
|
ofn.nMaxFile = sizeof(szFile);
|
|
ofn.lpstrFileTitle = NULL;
|
|
ofn.nMaxFileTitle = 0;
|
|
ofn.lpstrInitialDir = NULL;
|
|
ofn.nFilterIndex = 1;
|
|
|
|
// Slightly convoluted way to create the filter.
|
|
// First string is description, ended by '\0'
|
|
// Second string is extensions, each separated by ';' and at the end of all, a '\0'
|
|
// Then the whole container string is closed with an other '\0'
|
|
// For example: "model files\0*.model;*.obj;\0" <-- this string literal has "model files" as description and two accepted extensions "model" and "obj"
|
|
wi::vector<char> filter;
|
|
filter.reserve(256);
|
|
{
|
|
for (auto& x : params.description)
|
|
{
|
|
filter.push_back(x);
|
|
}
|
|
filter.push_back(0);
|
|
|
|
for (auto& x : params.extensions)
|
|
{
|
|
filter.push_back('*');
|
|
filter.push_back('.');
|
|
for (auto& y : x)
|
|
{
|
|
filter.push_back(y);
|
|
}
|
|
filter.push_back(';');
|
|
}
|
|
filter.push_back(0);
|
|
filter.push_back(0);
|
|
}
|
|
ofn.lpstrFilter = filter.data();
|
|
|
|
|
|
BOOL ok = FALSE;
|
|
switch (params.type)
|
|
{
|
|
case FileDialogParams::OPEN:
|
|
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
|
|
ofn.Flags |= OFN_NOCHANGEDIR;
|
|
ok = GetOpenFileNameA(&ofn) == TRUE;
|
|
break;
|
|
case FileDialogParams::SAVE:
|
|
ofn.Flags = OFN_OVERWRITEPROMPT;
|
|
ofn.Flags |= OFN_NOCHANGEDIR;
|
|
ok = GetSaveFileNameA(&ofn) == TRUE;
|
|
break;
|
|
}
|
|
|
|
if (ok)
|
|
{
|
|
onSuccess(ofn.lpstrFile);
|
|
}
|
|
|
|
}).detach();
|
|
|
|
#else
|
|
auto filedialoghelper = [](FileDialogParams params, std::function<void(std::string fileName)> onSuccess) -> winrt::fire_and_forget {
|
|
|
|
using namespace winrt::Windows::Storage;
|
|
using namespace winrt::Windows::Storage::Pickers;
|
|
using namespace winrt::Windows::Storage::AccessCache;
|
|
|
|
switch (params.type)
|
|
{
|
|
default:
|
|
case FileDialogParams::OPEN:
|
|
{
|
|
FileOpenPicker picker;
|
|
picker.ViewMode(PickerViewMode::List);
|
|
picker.SuggestedStartLocation(PickerLocationId::Objects3D);
|
|
|
|
for (auto& x : params.extensions)
|
|
{
|
|
std::wstring wstr;
|
|
StringConvert(x, wstr);
|
|
wstr = L"." + wstr;
|
|
picker.FileTypeFilter().Append(wstr);
|
|
}
|
|
|
|
auto file = co_await picker.PickSingleFileAsync();
|
|
|
|
if (file)
|
|
{
|
|
auto futureaccess = StorageApplicationPermissions::FutureAccessList();
|
|
futureaccess.Clear();
|
|
futureaccess.Add(file);
|
|
std::wstring wstr = file.Path().data();
|
|
std::string str;
|
|
StringConvert(wstr, str);
|
|
|
|
onSuccess(str);
|
|
}
|
|
}
|
|
break;
|
|
case FileDialogParams::SAVE:
|
|
{
|
|
FileSavePicker picker;
|
|
picker.SuggestedStartLocation(PickerLocationId::Objects3D);
|
|
|
|
std::wstring wdesc;
|
|
StringConvert(params.description, wdesc);
|
|
winrt::Windows::Foundation::Collections::IVector<winrt::hstring> extensions{ winrt::single_threaded_vector<winrt::hstring>() };
|
|
for (auto& x : params.extensions)
|
|
{
|
|
std::wstring wstr;
|
|
StringConvert(x, wstr);
|
|
wstr = L"." + wstr;
|
|
extensions.Append(wstr);
|
|
}
|
|
picker.FileTypeChoices().Insert(wdesc, extensions);
|
|
|
|
auto file = co_await picker.PickSaveFileAsync();
|
|
if (file)
|
|
{
|
|
auto futureaccess = StorageApplicationPermissions::FutureAccessList();
|
|
futureaccess.Clear();
|
|
futureaccess.Add(file);
|
|
std::wstring wstr = file.Path().data();
|
|
std::string str;
|
|
StringConvert(wstr, str);
|
|
onSuccess(str);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
filedialoghelper(params, onSuccess);
|
|
|
|
#endif // PLATFORM_UWP
|
|
|
|
#else
|
|
if (!pfd::settings::available()) {
|
|
const char *message = "No dialog backend available";
|
|
#ifdef SDL2
|
|
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
|
|
"File dialog error!",
|
|
message,
|
|
nullptr);
|
|
#endif
|
|
std::cerr << message << std::endl;
|
|
}
|
|
|
|
std::vector<std::string> extensions = {params.description, ""};
|
|
int extcount = 0;
|
|
for (auto& x : params.extensions)
|
|
{
|
|
extensions[1] += "*." + toLower(x);
|
|
extensions[1] += " ";
|
|
extensions[1] += "*." + toUpper(x);
|
|
if (extcount < params.extensions.size() - 1) {
|
|
extensions[1] += " ";
|
|
}
|
|
extcount++;
|
|
}
|
|
|
|
switch (params.type) {
|
|
case FileDialogParams::OPEN: {
|
|
std::vector<std::string> selection = pfd::open_file(
|
|
"Open file",
|
|
std::filesystem::current_path().string(),
|
|
extensions
|
|
).result();
|
|
if (!selection.empty())
|
|
{
|
|
onSuccess(selection[0]);
|
|
}
|
|
break;
|
|
}
|
|
case FileDialogParams::SAVE: {
|
|
std::string destination = pfd::save_file(
|
|
"Save file",
|
|
std::filesystem::current_path().string(),
|
|
extensions
|
|
).result();
|
|
if (!destination.empty())
|
|
{
|
|
onSuccess(destination);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
#endif // _WIN32
|
|
}
|
|
|
|
bool Bin2H(const uint8_t* data, size_t size, const std::string& dst_filename, const char* dataName)
|
|
{
|
|
std::string ss;
|
|
ss += "const uint8_t ";
|
|
ss += dataName ;
|
|
ss += "[] = {";
|
|
for (size_t i = 0; i < size; ++i)
|
|
{
|
|
if (i % 32 == 0)
|
|
{
|
|
ss += "\n";
|
|
}
|
|
ss += std::to_string((uint32_t)data[i]) + ",";
|
|
}
|
|
ss += "\n};\n";
|
|
return FileWrite(dst_filename, (uint8_t*)ss.c_str(), ss.length());
|
|
}
|
|
|
|
void StringConvert(const std::string& from, std::wstring& to)
|
|
{
|
|
#ifdef _WIN32
|
|
int num = MultiByteToWideChar(CP_UTF8, 0, from.c_str(), -1, NULL, 0);
|
|
if (num > 0)
|
|
{
|
|
to.resize(size_t(num) - 1);
|
|
MultiByteToWideChar(CP_UTF8, 0, from.c_str(), -1, &to[0], num);
|
|
}
|
|
#else
|
|
std::wstring_convert<std::codecvt_utf8<wchar_t>> cv;
|
|
to = cv.from_bytes(from);
|
|
#endif // _WIN32
|
|
}
|
|
|
|
void StringConvert(const std::wstring& from, std::string& to)
|
|
{
|
|
#ifdef _WIN32
|
|
int num = WideCharToMultiByte(CP_UTF8, 0, from.c_str(), -1, NULL, 0, NULL, NULL);
|
|
if (num > 0)
|
|
{
|
|
to.resize(size_t(num) - 1);
|
|
WideCharToMultiByte(CP_UTF8, 0, from.c_str(), -1, &to[0], num, NULL, NULL);
|
|
}
|
|
#else
|
|
std::wstring_convert<std::codecvt_utf8<wchar_t>> cv;
|
|
to = cv.to_bytes(from);
|
|
#endif // _WIN32
|
|
}
|
|
|
|
int StringConvert(const char* from, wchar_t* to)
|
|
{
|
|
#ifdef _WIN32
|
|
int num = MultiByteToWideChar(CP_UTF8, 0, from, -1, NULL, 0);
|
|
if (num > 0)
|
|
{
|
|
MultiByteToWideChar(CP_UTF8, 0, from, -1, &to[0], num);
|
|
}
|
|
return num;
|
|
#else
|
|
std::wstring_convert<std::codecvt_utf8<wchar_t>> cv;
|
|
std::memcpy(to, cv.from_bytes(from).c_str(), cv.converted());
|
|
return (int)cv.converted();
|
|
#endif // _WIN32
|
|
}
|
|
|
|
int StringConvert(const wchar_t* from, char* to)
|
|
{
|
|
#ifdef _WIN32
|
|
int num = WideCharToMultiByte(CP_UTF8, 0, from, -1, NULL, 0, NULL, NULL);
|
|
if (num > 0)
|
|
{
|
|
WideCharToMultiByte(CP_UTF8, 0, from, -1, &to[0], num, NULL, NULL);
|
|
}
|
|
return num;
|
|
#else
|
|
std::wstring_convert<std::codecvt_utf8<wchar_t>> cv;
|
|
std::memcpy(to, cv.to_bytes(from).c_str(), cv.converted());
|
|
return (int)cv.converted();
|
|
#endif // _WIN32
|
|
}
|
|
|
|
void Sleep(float milliseconds)
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds((int)milliseconds));
|
|
}
|
|
|
|
void Spin(float milliseconds)
|
|
{
|
|
milliseconds /= 1000.0f;
|
|
std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();
|
|
double ms = 0;
|
|
while (ms < milliseconds)
|
|
{
|
|
std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
|
|
std::chrono::duration<double> time_span = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
|
|
ms = time_span.count();
|
|
}
|
|
}
|
|
|
|
void OpenUrl(const std::string& url)
|
|
{
|
|
#ifdef PLATFORM_UWP
|
|
winrt::Windows::System::Launcher::LaunchUriAsync(winrt::Windows::Foundation::Uri(winrt::to_hstring(url)));
|
|
#endif // PLATFORM_UWP
|
|
|
|
#ifdef PLATFORM_WINDOWS_DESKTOP
|
|
std::string op = "start " + url;
|
|
int status = system(op.c_str());
|
|
wi::backlog::post("wi::helper::OpenUrl(" + url + ") returned status: " + std::to_string(status));
|
|
return;
|
|
#endif // PLATFORM_WINDOWS_DESKTOP
|
|
|
|
#ifdef PLATFORM_LINUX
|
|
std::string op = "xdg-open " + url;
|
|
int status = system(op.c_str());
|
|
wi::backlog::post("wi::helper::OpenUrl(" + url + ") returned status: " + std::to_string(status));
|
|
return;
|
|
#endif // PLATFORM_WINDOWS_DESKTOP
|
|
|
|
wi::backlog::post("wi::helper::OpenUrl(" + url + "): not implemented for this operating system!", wi::backlog::LogLevel::Warning);
|
|
}
|
|
}
|