mirror of
https://github.com/overte-org/overte.git
synced 2025-08-04 05:23:33 +02:00
Even better frame capture
This commit is contained in:
parent
88514540d1
commit
243120b95c
13 changed files with 242 additions and 165 deletions
|
@ -4441,7 +4441,7 @@ void Application::keyPressEvent(QKeyEvent* event) {
|
||||||
static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR)
|
static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR)
|
||||||
? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR)
|
? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR)
|
||||||
: "hifiFrames";
|
: "hifiFrames";
|
||||||
static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}.hfb";
|
static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}";
|
||||||
QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE));
|
QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE));
|
||||||
if (FileUtils::canCreateFile(fullPath)) {
|
if (FileUtils::canCreateFile(fullPath)) {
|
||||||
getActiveDisplayPlugin()->captureFrame(fullPath.toStdString());
|
getActiveDisplayPlugin()->captureFrame(fullPath.toStdString());
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
#include <shaders/Shaders.h>
|
#include <shaders/Shaders.h>
|
||||||
#include <gpu/gl/GLShared.h>
|
#include <gpu/gl/GLShared.h>
|
||||||
#include <gpu/gl/GLBackend.h>
|
#include <gpu/gl/GLBackend.h>
|
||||||
|
#include <gpu/gl/GLTexelFormat.h>
|
||||||
#include <GeometryCache.h>
|
#include <GeometryCache.h>
|
||||||
|
|
||||||
#include <CursorManager.h>
|
#include <CursorManager.h>
|
||||||
|
@ -476,34 +477,50 @@ void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ktx::StoragePointer textureToKtx(const gpu::Texture& texture) {
|
||||||
|
ktx::Header header;
|
||||||
|
{
|
||||||
|
auto gpuDims = texture.getDimensions();
|
||||||
|
header.pixelWidth = gpuDims.x;
|
||||||
|
header.pixelHeight = gpuDims.y;
|
||||||
|
header.pixelDepth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto gltexelformat = gpu::gl::GLTexelFormat::evalGLTexelFormat(texture.getStoredMipFormat());
|
||||||
|
header.glInternalFormat = gltexelformat.internalFormat;
|
||||||
|
header.glFormat = gltexelformat.format;
|
||||||
|
header.glBaseInternalFormat = gltexelformat.format;
|
||||||
|
header.glType = gltexelformat.type;
|
||||||
|
header.glTypeSize = 1;
|
||||||
|
header.numberOfMipmapLevels = 1 + texture.getMaxMip();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto memKtx = ktx::KTX::createBare(header);
|
||||||
|
auto storage = memKtx->_storage;
|
||||||
|
uint32_t faceCount = std::max(header.numberOfFaces, 1u);
|
||||||
|
uint32_t mipCount = std::max(header.numberOfMipmapLevels, 1u);
|
||||||
|
for (uint32_t mip = 0; mip < mipCount; ++mip) {
|
||||||
|
for (uint32_t face = 0; face < faceCount; ++face) {
|
||||||
|
const auto& image = memKtx->_images[mip];
|
||||||
|
auto& faceBytes = const_cast<gpu::Byte*&>(image._faceBytes[face]);
|
||||||
|
if (texture.isStoredMipFaceAvailable(mip, face)) {
|
||||||
|
auto storedImage = texture.accessStoredMipFace(mip, face);
|
||||||
|
auto storedSize = storedImage->size();
|
||||||
|
memcpy(faceBytes, storedImage->data(), storedSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return storage;
|
||||||
|
}
|
||||||
|
|
||||||
void OpenGLDisplayPlugin::captureFrame(const std::string& filename) const {
|
void OpenGLDisplayPlugin::captureFrame(const std::string& filename) const {
|
||||||
withOtherThreadContext([&] {
|
withOtherThreadContext([&] {
|
||||||
using namespace gpu;
|
using namespace gpu;
|
||||||
auto glBackend = const_cast<OpenGLDisplayPlugin&>(*this).getGLBackend();
|
auto glBackend = const_cast<OpenGLDisplayPlugin&>(*this).getGLBackend();
|
||||||
FramebufferPointer framebuffer{ Framebuffer::create("captureFramebuffer") };
|
FramebufferPointer framebuffer{ Framebuffer::create("captureFramebuffer") };
|
||||||
TextureCapturer captureLambda = [&](std::vector<uint8_t>& outputBuffer, const gpu::TexturePointer& texture, uint16 layer) {
|
TextureCapturer captureLambda = [&](const gpu::TexturePointer& texture)->storage::StoragePointer {
|
||||||
QImage image;
|
return textureToKtx(*texture);
|
||||||
if (texture->getUsageType() == TextureUsageType::STRICT_RESOURCE) {
|
|
||||||
image = QImage{ 1, 1, QImage::Format_ARGB32 };
|
|
||||||
auto storedImage = texture->accessStoredMipFace(0, 0);
|
|
||||||
memcpy(image.bits(), storedImage->data(), image.sizeInBytes());
|
|
||||||
//if (texture == textureCache->getWhiteTexture()) {
|
|
||||||
//} else if (texture == textureCache->getBlackTexture()) {
|
|
||||||
//} else if (texture == textureCache->getBlueTexture()) {
|
|
||||||
//} else if (texture == textureCache->getGrayTexture()) {
|
|
||||||
} else {
|
|
||||||
ivec4 rect = { 0, 0, texture->getWidth(), texture->getHeight() };
|
|
||||||
framebuffer->setRenderBuffer(0, texture, layer);
|
|
||||||
glBackend->syncGPUObject(*framebuffer);
|
|
||||||
|
|
||||||
image = QImage{ rect.z, rect.w, QImage::Format_ARGB32 };
|
|
||||||
glBackend->downloadFramebuffer(framebuffer, rect, image);
|
|
||||||
}
|
|
||||||
QBuffer buffer;
|
|
||||||
QImageWriter(&buffer, "png").write(image);
|
|
||||||
const auto& data = buffer.data();
|
|
||||||
outputBuffer.resize(data.size());
|
|
||||||
memcpy(outputBuffer.data(), data.constData(), data.size());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_currentFrame) {
|
if (_currentFrame) {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include "FrameIO.h"
|
#include "FrameIO.h"
|
||||||
#include <shared/Storage.h>
|
#include <shared/Storage.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
using namespace gpu::hfb;
|
using namespace gpu::hfb;
|
||||||
|
|
||||||
|
@ -30,30 +31,44 @@ static bool read(const uint8_t*& ptr, size_t& remaining, T& output) {
|
||||||
return skip(ptr, remaining, readSize);
|
return skip(ptr, remaining, readSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
Descriptor Descriptor::parse(const uint8_t* const data, size_t size) {
|
Descriptor::Descriptor(const StoragePointer& storage) : storage(storage) {
|
||||||
const auto* ptr = data;
|
const auto* const start = storage->data();
|
||||||
auto remaining = size;
|
const auto* ptr = storage->data();
|
||||||
Descriptor result;
|
auto remaining = storage->size();
|
||||||
if (!read(ptr, remaining, result.header)) {
|
|
||||||
return {};
|
try {
|
||||||
|
// Can't parse files more than 4GB
|
||||||
|
if (remaining > UINT32_MAX) {
|
||||||
|
throw std::runtime_error("File too large");
|
||||||
}
|
}
|
||||||
if (result.header.length != size) {
|
|
||||||
return {};
|
if (!read(ptr, remaining, header)) {
|
||||||
|
throw std::runtime_error("Couldn't read binary header");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.length != storage->size()) {
|
||||||
|
throw std::runtime_error("Header/Actual size mismatch");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (remaining != 0) {
|
while (remaining != 0) {
|
||||||
result.chunks.emplace_back();
|
chunks.emplace_back();
|
||||||
auto& chunk = result.chunks.back();
|
auto& chunk = chunks.back();
|
||||||
ChunkHeader& chunkHeader = chunk;
|
ChunkHeader& chunkHeader = chunk;
|
||||||
if (!read(ptr, remaining, chunkHeader)) {
|
if (!read(ptr, remaining, chunkHeader)) {
|
||||||
return {};
|
throw std::runtime_error("Coulnd't read chunk header");
|
||||||
|
}
|
||||||
|
chunk.offset = (uint32_t)(ptr - start);
|
||||||
|
if (chunk.end() > storage->size()) {
|
||||||
|
throw std::runtime_error("Chunk too large for file");
|
||||||
}
|
}
|
||||||
chunk.offset = ptr - data;
|
|
||||||
if (!skip(ptr, remaining, chunk.length)) {
|
if (!skip(ptr, remaining, chunk.length)) {
|
||||||
return {};
|
throw std::runtime_error("Skip chunk data failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
} catch (const std::runtime_error&) {
|
||||||
|
// LOG somnething
|
||||||
|
header.magic = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t Chunk::end() const {
|
size_t Chunk::end() const {
|
||||||
|
@ -62,30 +77,15 @@ size_t Chunk::end() const {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StoragePointer Descriptor::getChunk(uint32_t chunkIndex) const {
|
||||||
bool Descriptor::getChunkString(std::string& output, size_t chunkIndex, const uint8_t* const data, size_t size) {
|
|
||||||
if (chunkIndex >= chunks.size()) {
|
if (chunkIndex >= chunks.size()) {
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
const auto& chunk = chunks[chunkIndex];
|
const auto& chunk = chunks[chunkIndex];
|
||||||
if (chunk.end() > size) {
|
if (chunk.end() > storage->size()) {
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
output = std::string{ (const char*)(data + chunk.offset), chunk.length };
|
return storage->createView(chunk.length, chunk.offset);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Descriptor::getChunkBuffer(Buffer& output, size_t chunkIndex, const uint8_t* const data, size_t size) {
|
|
||||||
if (chunkIndex >= chunks.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto& chunk = chunks[chunkIndex];
|
|
||||||
if (chunk.end() > size) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
output.resize(chunk.length);
|
|
||||||
memcpy(output.data(), data + chunk.offset, chunk.length);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void writeUint(uint8_t*& dest, uint32_t value) {
|
static void writeUint(uint8_t*& dest, uint32_t value) {
|
||||||
|
@ -105,12 +105,15 @@ static void writeChunk(uint8_t*& dest, uint32_t chunkType, const T& chunkData) {
|
||||||
void gpu::hfb::writeFrame(const std::string& filename,
|
void gpu::hfb::writeFrame(const std::string& filename,
|
||||||
const std::string& json,
|
const std::string& json,
|
||||||
const Buffer& binaryBuffer,
|
const Buffer& binaryBuffer,
|
||||||
const Buffers& pngBuffers) {
|
const StorageBuilders& ktxBuilders) {
|
||||||
uint32_t strLen = (uint32_t)json.size();
|
uint32_t strLen = (uint32_t)json.size();
|
||||||
uint32_t size = gpu::hfb::HEADER_SIZE + gpu::hfb::CHUNK_HEADER_SIZE + strLen;
|
uint32_t size = gpu::hfb::HEADER_SIZE + gpu::hfb::CHUNK_HEADER_SIZE + strLen;
|
||||||
size += gpu::hfb::CHUNK_HEADER_SIZE + (uint32_t)binaryBuffer.size();
|
size += gpu::hfb::CHUNK_HEADER_SIZE + (uint32_t)binaryBuffer.size();
|
||||||
for (const auto& pngBuffer : pngBuffers) {
|
for (const auto& builder : ktxBuilders) {
|
||||||
size += gpu::hfb::CHUNK_HEADER_SIZE + (uint32_t)pngBuffer.size();
|
auto storage = builder();
|
||||||
|
if (storage) {
|
||||||
|
size += gpu::hfb::CHUNK_HEADER_SIZE + (uint32_t)storage->size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto outputConst = storage::FileStorage::create(filename.c_str(), size, nullptr);
|
auto outputConst = storage::FileStorage::create(filename.c_str(), size, nullptr);
|
||||||
|
@ -121,8 +124,13 @@ void gpu::hfb::writeFrame(const std::string& filename,
|
||||||
writeUint(ptr, size);
|
writeUint(ptr, size);
|
||||||
writeChunk(ptr, gpu::hfb::CHUNK_TYPE_JSON, json);
|
writeChunk(ptr, gpu::hfb::CHUNK_TYPE_JSON, json);
|
||||||
writeChunk(ptr, gpu::hfb::CHUNK_TYPE_BIN, binaryBuffer);
|
writeChunk(ptr, gpu::hfb::CHUNK_TYPE_BIN, binaryBuffer);
|
||||||
for (const auto& png : pngBuffers) {
|
for (const auto& builder : ktxBuilders) {
|
||||||
writeChunk(ptr, gpu::hfb::CHUNK_TYPE_PNG, png);
|
static StoragePointer EMPTY_STORAGE{ std::make_shared<storage::MemoryStorage>(0, nullptr) };
|
||||||
|
auto storage = builder();
|
||||||
|
if (!storage) {
|
||||||
|
storage = EMPTY_STORAGE;
|
||||||
|
}
|
||||||
|
writeChunk(ptr, gpu::hfb::CHUNK_TYPE_KTX, *storage);
|
||||||
}
|
}
|
||||||
assert((ptr - output->data()) == size);
|
assert((ptr - output->data()) == size);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,28 +12,34 @@
|
||||||
#include "Forward.h"
|
#include "Forward.h"
|
||||||
#include "Format.h"
|
#include "Format.h"
|
||||||
|
|
||||||
|
#include <shared/Storage.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
namespace gpu {
|
namespace gpu {
|
||||||
|
|
||||||
using TextureCapturer = std::function<void(std::vector<uint8_t>&, const TexturePointer&, uint16 layer)>;
|
using TextureCapturer = std::function<storage::StoragePointer(const TexturePointer&)>;
|
||||||
using TextureLoader = std::function<void(const std::vector<uint8_t>&, const TexturePointer&, uint16 layer)>;
|
//using TextureLoader = std::function<void(const storage::StoragePointer& storage, const TexturePointer&)>;
|
||||||
void writeFrame(const std::string& filename, const FramePointer& frame, const TextureCapturer& capturer = nullptr);
|
void writeFrame(const std::string& filename, const FramePointer& frame, const TextureCapturer& capturer = nullptr);
|
||||||
FramePointer readFrame(const std::string& filename, uint32_t externalTexture, const TextureLoader& loader = nullptr);
|
FramePointer readFrame(const std::string& filename, uint32_t externalTexture);
|
||||||
|
|
||||||
namespace hfb {
|
namespace hfb {
|
||||||
|
|
||||||
|
using Storage = storage::Storage;
|
||||||
|
using StoragePointer = storage::Pointer;
|
||||||
|
using StorageBuilders = storage::Builders;
|
||||||
|
|
||||||
constexpr char* const EXTENSION{ ".hfb" };
|
constexpr char* const EXTENSION{ ".hfb" };
|
||||||
constexpr uint32_t HEADER_SIZE{ sizeof(uint32_t) * 3 };
|
constexpr uint32_t HEADER_SIZE{ sizeof(uint32_t) * 3 };
|
||||||
constexpr uint32_t CHUNK_HEADER_SIZE = sizeof(uint32_t) * 2;
|
constexpr uint32_t CHUNK_HEADER_SIZE = sizeof(uint32_t) * 2;
|
||||||
constexpr uint32_t MAGIC{ 0x49464948 };
|
constexpr uint32_t MAGIC{ 0x49464948 };
|
||||||
constexpr uint32_t VERSION{ 0x01 };
|
constexpr uint32_t VERSION{ 0x01 };
|
||||||
constexpr uint32_t CHUNK_TYPE_JSON{ 0x4E4F534A };
|
constexpr uint32_t CHUNK_TYPE_JSON{ 0x4E4F534A };
|
||||||
|
constexpr uint32_t CHUNK_TYPE_KTX{ 0x0058544b };
|
||||||
constexpr uint32_t CHUNK_TYPE_BIN{ 0x004E4942 };
|
constexpr uint32_t CHUNK_TYPE_BIN{ 0x004E4942 };
|
||||||
constexpr uint32_t CHUNK_TYPE_PNG{ 0x00474E50 };
|
constexpr uint32_t CHUNK_TYPE_PNG{ 0x00474E50 };
|
||||||
|
|
||||||
using Buffer = std::vector<uint8_t>;
|
using Buffer = std::vector<uint8_t>;
|
||||||
using Buffers = std::vector<Buffer>;
|
|
||||||
|
|
||||||
struct Header {
|
struct Header {
|
||||||
uint32_t magic{ 0 };
|
uint32_t magic{ 0 };
|
||||||
|
@ -55,16 +61,21 @@ struct Chunk : public ChunkHeader {
|
||||||
using Chunks = std::vector<Chunk>;
|
using Chunks = std::vector<Chunk>;
|
||||||
|
|
||||||
struct Descriptor {
|
struct Descriptor {
|
||||||
|
using Pointer = std::shared_ptr<Descriptor>;
|
||||||
|
|
||||||
Header header;
|
Header header;
|
||||||
Chunks chunks;
|
Chunks chunks;
|
||||||
|
StoragePointer storage;
|
||||||
|
|
||||||
|
Descriptor(const StoragePointer& storage);
|
||||||
operator bool() const { return header.magic == MAGIC; }
|
operator bool() const { return header.magic == MAGIC; }
|
||||||
bool getChunkString(std::string& output, size_t chunk, const uint8_t* const data, size_t size);
|
StoragePointer getChunk(uint32_t chunk) const;
|
||||||
bool getChunkBuffer(Buffer& output, size_t chunk, const uint8_t* const data, size_t size);
|
|
||||||
static Descriptor parse(const uint8_t* const data, size_t size);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeFrame(const std::string& filename, const std::string& json, const Buffer& binaryBuffer, const Buffers& pngBuffers);
|
void writeFrame(const std::string& filename,
|
||||||
|
const std::string& json,
|
||||||
|
const Buffer& binaryBuffer,
|
||||||
|
const StorageBuilders& pngBuffers);
|
||||||
|
|
||||||
} // namespace hfb
|
} // namespace hfb
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,7 @@
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <QtCore/QFileInfo>
|
#include <shared/FileUtils.h>
|
||||||
#include <QtCore/QDir>
|
|
||||||
|
|
||||||
#include <ktx/KTX.h>
|
#include <ktx/KTX.h>
|
||||||
#include "Frame.h"
|
#include "Frame.h"
|
||||||
#include "Batch.h"
|
#include "Batch.h"
|
||||||
|
@ -33,7 +31,7 @@ public:
|
||||||
auto lastSlash = filename.rfind('/');
|
auto lastSlash = filename.rfind('/');
|
||||||
result = filename.substr(0, lastSlash + 1);
|
result = filename.substr(0, lastSlash + 1);
|
||||||
} else {
|
} else {
|
||||||
result = QFileInfo(filename.c_str()).absoluteDir().canonicalPath().toStdString();
|
result = FileUtils::getParentPath(filename.c_str()).toStdString();
|
||||||
if (*result.rbegin() != '/') {
|
if (*result.rbegin() != '/') {
|
||||||
result += '/';
|
result += '/';
|
||||||
}
|
}
|
||||||
|
@ -41,18 +39,17 @@ public:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Deserializer(const std::string& filename_, uint32_t externalTexture = 0, const TextureLoader& loader = {}) :
|
Deserializer(const std::string& filename_, uint32_t externalTexture = 0) :
|
||||||
filename(filename_), basedir(getBaseDir(filename_)), mappedFile(std::make_shared<FileStorage>(filename.c_str())),
|
filename(filename_), basedir(getBaseDir(filename_)), mappedFile(std::make_shared<FileStorage>(filename.c_str())),
|
||||||
externalTexture(externalTexture), textureLoader(loader) {
|
externalTexture(externalTexture) {
|
||||||
descriptor = hfb::Descriptor::parse(mappedFile->data(), (uint32_t)mappedFile->size());
|
descriptor = std::make_shared<hfb::Descriptor>(mappedFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string filename;
|
const std::string filename;
|
||||||
const std::string basedir;
|
const std::string basedir;
|
||||||
const StoragePointer mappedFile;
|
const StoragePointer mappedFile;
|
||||||
const uint32_t externalTexture;
|
const uint32_t externalTexture;
|
||||||
hfb::Descriptor descriptor;
|
hfb::Descriptor::Pointer descriptor;
|
||||||
TextureLoader textureLoader;
|
|
||||||
std::vector<ShaderPointer> shaders;
|
std::vector<ShaderPointer> shaders;
|
||||||
std::vector<ShaderPointer> programs;
|
std::vector<ShaderPointer> programs;
|
||||||
std::vector<TexturePointer> textures;
|
std::vector<TexturePointer> textures;
|
||||||
|
@ -70,19 +67,8 @@ public:
|
||||||
FramePointer deserializeFrame();
|
FramePointer deserializeFrame();
|
||||||
|
|
||||||
std::string getStringChunk(size_t chunkIndex) {
|
std::string getStringChunk(size_t chunkIndex) {
|
||||||
std::string result;
|
auto storage = descriptor->getChunk((uint32_t)chunkIndex);
|
||||||
if (!descriptor.getChunkString(result, chunkIndex, mappedFile->data(), mappedFile->size())) {
|
return std::string{ (const char*)storage->data(), storage->size() };
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
hfb::Buffer getBufferChunk(size_t chunkIndex) {
|
|
||||||
hfb::Buffer result;
|
|
||||||
if (!descriptor.getChunkBuffer(result, chunkIndex, mappedFile->data(), mappedFile->size())) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void readBuffers(const json& node);
|
void readBuffers(const json& node);
|
||||||
|
@ -240,8 +226,8 @@ public:
|
||||||
static void readCommand(const json& node, Batch& batch);
|
static void readCommand(const json& node, Batch& batch);
|
||||||
};
|
};
|
||||||
|
|
||||||
FramePointer readFrame(const std::string& filename, uint32_t externalTexture, const TextureLoader& loader) {
|
FramePointer readFrame(const std::string& filename, uint32_t externalTexture) {
|
||||||
return Deserializer(filename, externalTexture, loader).readFrame();
|
return Deserializer(filename, externalTexture).readFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gpu
|
} // namespace gpu
|
||||||
|
@ -249,7 +235,7 @@ FramePointer readFrame(const std::string& filename, uint32_t externalTexture, co
|
||||||
using namespace gpu;
|
using namespace gpu;
|
||||||
|
|
||||||
void Deserializer::readBuffers(const json& buffersNode) {
|
void Deserializer::readBuffers(const json& buffersNode) {
|
||||||
const auto& binaryChunk = descriptor.chunks[1];
|
const auto& binaryChunk = descriptor->chunks[1];
|
||||||
const auto* mapped = mappedFile->data() + binaryChunk.offset;
|
const auto* mapped = mappedFile->data() + binaryChunk.offset;
|
||||||
const auto mappedSize = binaryChunk.length;
|
const auto mappedSize = binaryChunk.length;
|
||||||
size_t bufferCount = buffersNode.size();
|
size_t bufferCount = buffersNode.size();
|
||||||
|
@ -333,7 +319,7 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) {
|
||||||
std::string ktxFile;
|
std::string ktxFile;
|
||||||
readOptional(ktxFile, node, keys::ktxFile);
|
readOptional(ktxFile, node, keys::ktxFile);
|
||||||
if (!ktxFile.empty()) {
|
if (!ktxFile.empty()) {
|
||||||
if (!QFileInfo(ktxFile.c_str()).exists()) {
|
if (!FileUtils::exists(ktxFile.c_str())) {
|
||||||
qDebug() << "Warning" << ktxFile.c_str() << " not found, ignoring";
|
qDebug() << "Warning" << ktxFile.c_str() << " not found, ignoring";
|
||||||
ktxFile = {};
|
ktxFile = {};
|
||||||
}
|
}
|
||||||
|
@ -347,8 +333,8 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) {
|
||||||
frameReaderPath.replace("libraries/gpu/src/gpu/framereader.cpp", "interface/resources", Qt::CaseInsensitive);
|
frameReaderPath.replace("libraries/gpu/src/gpu/framereader.cpp", "interface/resources", Qt::CaseInsensitive);
|
||||||
ktxFile.replace(0, 1, frameReaderPath.toStdString());
|
ktxFile.replace(0, 1, frameReaderPath.toStdString());
|
||||||
}
|
}
|
||||||
if (QFileInfo(ktxFile.c_str()).isRelative()) {
|
if (FileUtils::isRelative(ktxFile.c_str())) {
|
||||||
ktxFile = basedir + ktxFile;
|
ktxFile = basedir + "/" + ktxFile;
|
||||||
}
|
}
|
||||||
ktx::StoragePointer ktxStorage{ new storage::FileStorage(ktxFile.c_str()) };
|
ktx::StoragePointer ktxStorage{ new storage::FileStorage(ktxFile.c_str()) };
|
||||||
auto ktxObject = ktx::KTX::create(ktxStorage);
|
auto ktxObject = ktx::KTX::create(ktxStorage);
|
||||||
|
@ -381,19 +367,14 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) {
|
||||||
auto& texture = *result;
|
auto& texture = *result;
|
||||||
readOptional(texture._source, node, keys::source);
|
readOptional(texture._source, node, keys::source);
|
||||||
|
|
||||||
if (!ktxFile.empty()) {
|
|
||||||
if (QFileInfo(ktxFile.c_str()).isRelative()) {
|
if (chunkIndex != INVALID_CHUNK_INDEX) {
|
||||||
ktxFile = basedir + "/" + ktxFile;
|
auto ktxChunk = descriptor->getChunk(chunkIndex);
|
||||||
}
|
texture.setKtxBacking(ktxChunk);
|
||||||
|
} else if (!ktxFile.empty()) {
|
||||||
texture.setSource(ktxFile);
|
texture.setSource(ktxFile);
|
||||||
texture.setKtxBacking(ktxFile);
|
texture.setKtxBacking(ktxFile);
|
||||||
} else if (chunkIndex != INVALID_CHUNK_INDEX) {
|
|
||||||
if (textureLoader) {
|
|
||||||
texture.setSource("Chunk " + std::to_string(chunkIndex));
|
|
||||||
textureLoader(getBufferChunk(chunkIndex), result, 0);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ public:
|
||||||
std::unordered_map<QueryPointer, uint32_t> queryMap;
|
std::unordered_map<QueryPointer, uint32_t> queryMap;
|
||||||
std::unordered_set<TexturePointer> captureTextures;
|
std::unordered_set<TexturePointer> captureTextures;
|
||||||
hfb::Buffer binaryBuffer;
|
hfb::Buffer binaryBuffer;
|
||||||
hfb::Buffers pngBuffers;
|
hfb::StorageBuilders ktxBuilders;
|
||||||
|
|
||||||
Serializer(const std::string& basename, const TextureCapturer& capturer) : filename(basename + hfb::EXTENSION), textureCapturer(capturer) {}
|
Serializer(const std::string& basename, const TextureCapturer& capturer) : filename(basename + hfb::EXTENSION), textureCapturer(capturer) {}
|
||||||
|
|
||||||
|
@ -392,13 +392,17 @@ json Serializer::writeTexture(const TexturePointer& texturePointer) {
|
||||||
const auto* storage = texture._storage.get();
|
const auto* storage = texture._storage.get();
|
||||||
const auto* ktxStorage = dynamic_cast<const Texture::KtxStorage*>(storage);
|
const auto* ktxStorage = dynamic_cast<const Texture::KtxStorage*>(storage);
|
||||||
if (ktxStorage) {
|
if (ktxStorage) {
|
||||||
result[keys::ktxFile] = ktxStorage->_filename;
|
result[keys::chunk] = 2 + ktxBuilders.size();
|
||||||
|
auto filename = ktxStorage->_filename;
|
||||||
|
ktxBuilders.push_back([=] {
|
||||||
|
return std::make_shared<storage::FileStorage>(filename.c_str());
|
||||||
|
});
|
||||||
} else if (textureCapturer && captureTextures.count(texturePointer) != 0) {
|
} else if (textureCapturer && captureTextures.count(texturePointer) != 0) {
|
||||||
auto layers = std::max<uint16>(texture.getNumSlices(), 1);
|
result[keys::chunk] = 2 + ktxBuilders.size();
|
||||||
result[keys::chunk] = 2 + pngBuffers.size();
|
auto storage = textureCapturer(texturePointer);
|
||||||
pngBuffers.push_back({});
|
ktxBuilders.push_back([=] {
|
||||||
hfb::Buffer& pngBuffer = pngBuffers.back();
|
return storage;
|
||||||
textureCapturer(pngBuffer, texturePointer, 0);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -790,7 +794,7 @@ void Serializer::writeFrame(const Frame& frame) {
|
||||||
|
|
||||||
writeBinaryBlob();
|
writeBinaryBlob();
|
||||||
|
|
||||||
hfb::writeFrame(filename, frameNode.dump(), binaryBuffer, pngBuffers);
|
hfb::writeFrame(filename, frameNode.dump(), binaryBuffer, ktxBuilders);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Serializer::writeBinaryBlob() {
|
void Serializer::writeBinaryBlob() {
|
||||||
|
|
|
@ -343,6 +343,7 @@ public:
|
||||||
|
|
||||||
class KtxStorage : public Storage {
|
class KtxStorage : public Storage {
|
||||||
public:
|
public:
|
||||||
|
KtxStorage(const storage::StoragePointer& storage);
|
||||||
KtxStorage(const std::string& filename);
|
KtxStorage(const std::string& filename);
|
||||||
KtxStorage(const cache::FilePointer& file);
|
KtxStorage(const cache::FilePointer& file);
|
||||||
PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override;
|
PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override;
|
||||||
|
@ -366,6 +367,7 @@ public:
|
||||||
static std::vector<std::pair<std::shared_ptr<storage::FileStorage>, std::shared_ptr<std::mutex>>> _cachedKtxFiles;
|
static std::vector<std::pair<std::shared_ptr<storage::FileStorage>, std::shared_ptr<std::mutex>>> _cachedKtxFiles;
|
||||||
static std::mutex _cachedKtxFilesMutex;
|
static std::mutex _cachedKtxFilesMutex;
|
||||||
|
|
||||||
|
storage::StoragePointer _storage;
|
||||||
std::string _filename;
|
std::string _filename;
|
||||||
cache::FilePointer _cacheEntry;
|
cache::FilePointer _cacheEntry;
|
||||||
std::atomic<uint8_t> _minMipLevelAvailable;
|
std::atomic<uint8_t> _minMipLevelAvailable;
|
||||||
|
@ -543,6 +545,7 @@ public:
|
||||||
Size getStoredSize() const;
|
Size getStoredSize() const;
|
||||||
|
|
||||||
void setStorage(std::unique_ptr<Storage>& newStorage);
|
void setStorage(std::unique_ptr<Storage>& newStorage);
|
||||||
|
void setKtxBacking(const storage::StoragePointer& storage);
|
||||||
void setKtxBacking(const std::string& filename);
|
void setKtxBacking(const std::string& filename);
|
||||||
void setKtxBacking(const cache::FilePointer& cacheEntry);
|
void setKtxBacking(const cache::FilePointer& cacheEntry);
|
||||||
|
|
||||||
|
|
|
@ -159,6 +159,30 @@ struct IrradianceKTXPayload {
|
||||||
};
|
};
|
||||||
const std::string IrradianceKTXPayload::KEY{ "hifi.irradianceSH" };
|
const std::string IrradianceKTXPayload::KEY{ "hifi.irradianceSH" };
|
||||||
|
|
||||||
|
KtxStorage::KtxStorage(const storage::StoragePointer& storage) : _storage(storage) {
|
||||||
|
auto ktxPointer = ktx::KTX::create(storage);
|
||||||
|
_ktxDescriptor.reset(new ktx::KTXDescriptor(ktxPointer->toDescriptor()));
|
||||||
|
if (_ktxDescriptor->images.size() < _ktxDescriptor->header.numberOfMipmapLevels) {
|
||||||
|
qWarning() << "Bad images found in ktx";
|
||||||
|
}
|
||||||
|
|
||||||
|
_offsetToMinMipKV = _ktxDescriptor->getValueOffsetForKey(ktx::HIFI_MIN_POPULATED_MIP_KEY);
|
||||||
|
if (_offsetToMinMipKV) {
|
||||||
|
auto data = storage->data() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV;
|
||||||
|
_minMipLevelAvailable = *data;
|
||||||
|
} else {
|
||||||
|
// Assume all mip levels are available
|
||||||
|
_minMipLevelAvailable = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now that we know the ktx, let's get the header info to configure this Texture::Storage:
|
||||||
|
Format mipFormat = Format::COLOR_BGRA_32;
|
||||||
|
Format texelFormat = Format::COLOR_SRGBA_32;
|
||||||
|
if (Texture::evalTextureFormat(_ktxDescriptor->header, mipFormat, texelFormat)) {
|
||||||
|
_format = mipFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KtxStorage::KtxStorage(const cache::FilePointer& cacheEntry) : KtxStorage(cacheEntry->getFilepath()) {
|
KtxStorage::KtxStorage(const cache::FilePointer& cacheEntry) : KtxStorage(cacheEntry->getFilepath()) {
|
||||||
_cacheEntry = cacheEntry;
|
_cacheEntry = cacheEntry;
|
||||||
}
|
}
|
||||||
|
@ -228,28 +252,31 @@ void KtxStorage::releaseOpenKtxFiles() {
|
||||||
PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const {
|
PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const {
|
||||||
auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face);
|
auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face);
|
||||||
auto faceSize = _ktxDescriptor->getMipFaceTexelsSize(level, face);
|
auto faceSize = _ktxDescriptor->getMipFaceTexelsSize(level, face);
|
||||||
|
storage::StoragePointer storageView;
|
||||||
if (faceSize != 0 && faceOffset != 0) {
|
if (faceSize != 0 && faceOffset != 0) {
|
||||||
|
if (_storage) {
|
||||||
|
storageView = _storage->createView(faceSize, faceOffset);
|
||||||
|
} else {
|
||||||
std::lock_guard<std::mutex> lock(*_cacheFileMutex);
|
std::lock_guard<std::mutex> lock(*_cacheFileMutex);
|
||||||
auto file = maybeOpenFile();
|
auto file = maybeOpenFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
auto storageView = file->createView(faceSize, faceOffset);
|
storageView = file->createView(faceSize, faceOffset);
|
||||||
if (storageView) {
|
|
||||||
return storageView->toMemoryStorage();
|
|
||||||
} else {
|
|
||||||
qWarning() << "Failed to get a valid storageView for faceSize=" << faceSize << " faceOffset=" << faceOffset << "out of valid file " << QString::fromStdString(_filename);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Failed to get a valid file out of maybeOpenFile " << QString::fromStdString(_filename);
|
qWarning() << "Failed to get a valid file out of maybeOpenFile " << QString::fromStdString(_filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullptr;
|
}
|
||||||
|
if (!storageView) {
|
||||||
|
qWarning() << "Failed to get a valid storageView for faceSize=" << faceSize << " faceOffset=" << faceOffset
|
||||||
|
<< "out of valid file " << QString::fromStdString(_filename);
|
||||||
|
}
|
||||||
|
return storageView->toMemoryStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const {
|
Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const {
|
||||||
return _ktxDescriptor->getMipFaceTexelsSize(level, face);
|
return _ktxDescriptor->getMipFaceTexelsSize(level, face);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const {
|
bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const {
|
||||||
return level >= _minMipLevelAvailable;
|
return level >= _minMipLevelAvailable;
|
||||||
}
|
}
|
||||||
|
@ -311,8 +338,7 @@ void KtxStorage::assignMipFaceData(uint16 level, uint8 face, const storage::Stor
|
||||||
throw std::runtime_error("Invalid call");
|
throw std::runtime_error("Invalid call");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool validKtx(const std::string& filename) {
|
bool validKtx(const storage::StoragePointer& storage) {
|
||||||
ktx::StoragePointer storage { new storage::FileStorage(filename.c_str()) };
|
|
||||||
auto ktxPointer = ktx::KTX::create(storage);
|
auto ktxPointer = ktx::KTX::create(storage);
|
||||||
if (!ktxPointer) {
|
if (!ktxPointer) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -320,6 +346,21 @@ bool validKtx(const std::string& filename) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool validKtx(const std::string& filename) {
|
||||||
|
ktx::StoragePointer storage{ new storage::FileStorage(filename.c_str()) };
|
||||||
|
return validKtx(storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Texture::setKtxBacking(const storage::StoragePointer& storage) {
|
||||||
|
// Check the KTX file for validity before using it as backing storage
|
||||||
|
if (!validKtx(storage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto newBacking = std::unique_ptr<Storage>(new KtxStorage(storage));
|
||||||
|
setStorage(newBacking);
|
||||||
|
}
|
||||||
|
|
||||||
void Texture::setKtxBacking(const std::string& filename) {
|
void Texture::setKtxBacking(const std::string& filename) {
|
||||||
// Check the KTX file for validity before using it as backing storage
|
// Check the KTX file for validity before using it as backing storage
|
||||||
if (!validKtx(filename)) {
|
if (!validKtx(filename)) {
|
||||||
|
|
|
@ -96,7 +96,7 @@ namespace ktx {
|
||||||
using GLBaseInternalFormat = khronos::gl::texture::BaseInternalFormat;
|
using GLBaseInternalFormat = khronos::gl::texture::BaseInternalFormat;
|
||||||
|
|
||||||
using Storage = storage::Storage;
|
using Storage = storage::Storage;
|
||||||
using StoragePointer = std::shared_ptr<Storage>;
|
using StoragePointer = std::shared_ptr<const Storage>;
|
||||||
|
|
||||||
struct ImageDescriptor;
|
struct ImageDescriptor;
|
||||||
using ImageDescriptors = std::vector<ImageDescriptor>;
|
using ImageDescriptors = std::vector<ImageDescriptor>;
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
#include <QtCore/QUrl>
|
#include <QtCore/QUrl>
|
||||||
#include <QtCore/QTextStream>
|
#include <QtCore/QTextStream>
|
||||||
#include <QtCore/QRegularExpression>
|
#include <QtCore/QRegularExpression>
|
||||||
|
#include <QtCore/QFileSelector>
|
||||||
#include <QtGui/QDesktopServices>
|
#include <QtGui/QDesktopServices>
|
||||||
|
|
||||||
|
|
||||||
|
@ -176,3 +177,15 @@ bool FileUtils::canCreateFile(const QString& fullPath) {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString FileUtils::getParentPath(const QString& fullPath) {
|
||||||
|
return QFileInfo(fullPath).absoluteDir().canonicalPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileUtils::exists(const QString& fileName) {
|
||||||
|
return QFileInfo(fileName).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileUtils::isRelative(const QString& fileName) {
|
||||||
|
return QFileInfo(fileName).isRelative();
|
||||||
|
}
|
||||||
|
|
|
@ -12,20 +12,23 @@
|
||||||
#ifndef hifi_FileUtils_h
|
#ifndef hifi_FileUtils_h
|
||||||
#define hifi_FileUtils_h
|
#define hifi_FileUtils_h
|
||||||
|
|
||||||
#include <QString>
|
#include <QtCore/QString>
|
||||||
#include <QtCore/QFileSelector>
|
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static const QStringList& getFileSelectors();
|
static const QStringList& getFileSelectors();
|
||||||
static QString selectFile(const QString& fileName);
|
static QString selectFile(const QString& fileName);
|
||||||
static void locateFile(const QString& fileName);
|
static void locateFile(const QString& fileName);
|
||||||
|
static bool exists(const QString& fileName);
|
||||||
|
static bool isRelative(const QString& fileName);
|
||||||
static QString standardPath(QString subfolder);
|
static QString standardPath(QString subfolder);
|
||||||
static QString readFile(const QString& filename);
|
static QString readFile(const QString& filename);
|
||||||
static QStringList readLines(const QString& filename, QString::SplitBehavior splitBehavior = QString::KeepEmptyParts);
|
static QStringList readLines(const QString& filename, QString::SplitBehavior splitBehavior = QString::KeepEmptyParts);
|
||||||
static QString replaceDateTimeTokens(const QString& path);
|
static QString replaceDateTimeTokens(const QString& path);
|
||||||
static QString computeDocumentPath(const QString& path);
|
static QString computeDocumentPath(const QString& path);
|
||||||
static bool canCreateFile(const QString& fullPath);
|
static bool canCreateFile(const QString& fullPath);
|
||||||
|
static QString getParentPath(const QString& fullPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // hifi_FileUtils_h
|
#endif // hifi_FileUtils_h
|
||||||
|
|
|
@ -10,15 +10,22 @@
|
||||||
#ifndef hifi_Storage_h
|
#ifndef hifi_Storage_h
|
||||||
#define hifi_Storage_h
|
#define hifi_Storage_h
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <QFile>
|
#include <functional>
|
||||||
#include <QString>
|
|
||||||
|
#include <QtCore/QFile>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
|
||||||
namespace storage {
|
namespace storage {
|
||||||
class Storage;
|
class Storage;
|
||||||
using StoragePointer = std::shared_ptr<const Storage>;
|
using Pointer = std::shared_ptr<const Storage>;
|
||||||
|
using StoragePointer = Pointer;
|
||||||
|
// A function that can construct storage, useful for creating a list of
|
||||||
|
// things that can become storage without requiring that they all be instantiated at once
|
||||||
|
using Builder = std::function<StoragePointer()>;
|
||||||
|
using Builders = std::vector<Builder>;
|
||||||
|
|
||||||
// Abstract class to represent memory that stored _somewhere_ (in system memory or in a file, for example)
|
// Abstract class to represent memory that stored _somewhere_ (in system memory or in a file, for example)
|
||||||
class Storage : public std::enable_shared_from_this<Storage> {
|
class Storage : public std::enable_shared_from_this<Storage> {
|
||||||
|
|
|
@ -106,19 +106,8 @@ void PlayerWindow::resizeEvent(QResizeEvent* ev) {
|
||||||
_renderThread.resize(ev->size());
|
_renderThread.resize(ev->size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayerWindow::textureLoader(const std::vector<uint8_t>& imageBytes, const gpu::TexturePointer& texture, uint16_t layer) {
|
|
||||||
QImage image;
|
|
||||||
QByteArray bytes{ (const char*)imageBytes.data(), (int)imageBytes.size() };
|
|
||||||
QBuffer bytesBuffer(&bytes);
|
|
||||||
QImageReader(&bytesBuffer).read(&image);
|
|
||||||
if (layer > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
texture->assignStoredMip(0, image.byteCount(), image.constBits());
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayerWindow::loadFrame(const QString& path) {
|
void PlayerWindow::loadFrame(const QString& path) {
|
||||||
auto frame = gpu::readFrame(path.toStdString(), _renderThread._externalTexture, &PlayerWindow::textureLoader);
|
auto frame = gpu::readFrame(path.toStdString(), _renderThread._externalTexture);
|
||||||
if (frame) {
|
if (frame) {
|
||||||
_renderThread.submitFrame(frame);
|
_renderThread.submitFrame(frame);
|
||||||
if (!_renderThread.isThreaded()) {
|
if (!_renderThread.isThreaded()) {
|
||||||
|
|
Loading…
Reference in a new issue