lsfg-vk/lsfg-vk-backend/src/lsfgvk.cpp

554 lines
20 KiB
C++

#include "lsfg-vk-backend/lsfgvk.hpp"
#include "extraction/dll_reader.hpp"
#include "extraction/shader_registry.hpp"
#include "helpers/utils.hpp"
#include "lsfg-vk-common/helpers/errors.hpp"
#include "lsfg-vk-common/helpers/pointers.hpp"
#include "lsfg-vk-common/vulkan/buffer.hpp"
#include "lsfg-vk-common/vulkan/command_buffer.hpp"
#include "lsfg-vk-common/vulkan/image.hpp"
#include "lsfg-vk-common/vulkan/timeline_semaphore.hpp"
#include "lsfg-vk-common/vulkan/vulkan.hpp"
#include "shaderchains/alpha0.hpp"
#include "shaderchains/alpha1.hpp"
#include "shaderchains/beta0.hpp"
#include "shaderchains/beta1.hpp"
#include "shaderchains/delta0.hpp"
#include "shaderchains/delta1.hpp"
#include "shaderchains/gamma0.hpp"
#include "shaderchains/gamma1.hpp"
#include "shaderchains/generate.hpp"
#include "shaderchains/mipmaps.hpp"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <vulkan/vulkan_core.h>
#ifdef LSFGVK__RENDERDOC_INTEGRATION
#include <renderdoc_app.h>
#include <dlfcn.h>
#endif
using namespace lsfgvk;
namespace lsfgvk {
/// instance class
class InstanceImpl {
public:
/// create an instance
/// (see lsfg-vk documentation)
InstanceImpl(vk::PhysicalDeviceSelector selectPhysicalDevice,
const std::filesystem::path& shaderDllPath,
bool allowLowPrecision);
/// get the Vulkan instance
/// @return the Vulkan instance
[[nodiscard]] const auto& getVulkan() const { return this->vk; }
/// get the shader registry
/// @return the shader registry
[[nodiscard]] const auto& getShaderRegistry() const { return this->shaders; }
#ifdef LSFGVK__RENDERDOC_INTEGRATION
/// get the RenderDoc API
/// @return the RenderDoc API
[[nodiscard]] const auto& getRenderDocAPI() const { return this->renderdoc; }
#endif
private:
vk::Vulkan vk;
extr::ShaderRegistry shaders;
#ifdef LSFGVK__RENDERDOC_INTEGRATION
std::optional<RENDERDOC_API_1_6_0> renderdoc;
#endif
};
/// context class
class ContextImpl {
public:
/// create a context
/// (see lsfg-vk documentation)
ContextImpl(const InstanceImpl& instance,
std::pair<int, int> sourceFds, const std::vector<int>& destFds, int syncFd,
VkExtent2D extent, bool hdr, float flow, bool perf);
/// schedule frames
/// (see lsfg-vk documentation)
void scheduleFrames();
private:
std::pair<vk::Image, vk::Image> sourceImages;
std::vector<vk::Image> destImages;
vk::Image blackImage;
vk::TimelineSemaphore syncSemaphore; // imported
vk::TimelineSemaphore prepassSemaphore;
size_t idx{1};
size_t fidx{0}; // real frame index
std::vector<vk::CommandBuffer> cmdbufs; // TODO: ponder reuse
size_t cmdbuf_idx{0};
ls::Ctx ctx;
chains::Mipmaps mipmaps;
std::array<chains::Alpha0, 7> alpha0;
std::array<chains::Alpha1, 7> alpha1;
chains::Beta0 beta0;
chains::Beta1 beta1;
struct Pass {
std::vector<chains::Gamma0> gamma0;
std::vector<chains::Gamma1> gamma1;
std::vector<chains::Delta0> delta0;
std::vector<chains::Delta1> delta1;
ls::lazy<chains::Generate> generate;
};
std::vector<Pass> passes;
};
}
Instance::Instance(
const std::function<bool(const std::string&)>& devicePicker,
const std::filesystem::path& shaderDllPath,
bool allowLowPrecision) {
const auto selectFunc = [&devicePicker](const std::vector<VkPhysicalDevice>& devices) {
for (const auto& device : devices) {
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(device, &props);
std::array<char, 256> devname = std::to_array(props.deviceName);
devname[255] = '\0'; // ensure null-termination
const std::string& deviceName{devname.data()};
if (devicePicker(deviceName))
return device;
}
throw ls::vulkan_error("no suitable physical device found");
};
this->m_impl = std::make_unique<InstanceImpl>(
selectFunc, shaderDllPath, allowLowPrecision
);
}
namespace {
/// create a Vulkan instance
vk::Vulkan createVulkanInstance(vk::PhysicalDeviceSelector selectPhysicalDevice) {
try {
return{
"lsfg-vk", vk::version{1, 1, 0},
"lsfg-vk-engine", vk::version{1, 1, 0},
selectPhysicalDevice
};
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to initialize Vulkan", e);
}
}
/// build a shader registry
extr::ShaderRegistry createShaderRegistry(vk::Vulkan& vk,
const std::filesystem::path& shaderDllPath,
bool allowLowPrecision) {
std::unordered_map<uint32_t, std::vector<uint8_t>> resources{};
try {
resources = extr::extractResourcesFromDLL(shaderDllPath);
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to parse Lossless Scaling DLL", e);
}
try {
return extr::buildShaderRegistry(
vk, allowLowPrecision && vk.supportsFP16(),
resources
);
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to build shader registry", e);
}
}
#ifdef LSFGVK__RENDERDOC_INTEGRATION
/// load RenderDoc integration
std::optional<RENDERDOC_API_1_6_0> loadRenderDocIntegration() {
void* module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
if (!module)
return std::nullopt;
auto renderdocGetAPI = reinterpret_cast<pRENDERDOC_GetAPI>(
dlsym(module, "RENDERDOC_GetAPI"));
if (!renderdocGetAPI)
return std::nullopt;
RENDERDOC_API_1_6_0* api{};
renderdocGetAPI(eRENDERDOC_API_Version_1_6_0, reinterpret_cast<void**>(&api));
if (!api)
return std::nullopt;
return *api;
}
#endif
}
InstanceImpl::InstanceImpl(vk::PhysicalDeviceSelector selectPhysicalDevice,
const std::filesystem::path& shaderDllPath,
bool allowLowPrecision)
: vk(createVulkanInstance(selectPhysicalDevice)),
shaders(createShaderRegistry(this->vk, shaderDllPath,
allowLowPrecision && vk.supportsFP16())) {
#ifdef LSFGVK__RENDERDOC_INTEGRATION
this->renderdoc = loadRenderDocIntegration();
#endif
}
Context& Instance::openContext(std::pair<int, int> sourceFds, const std::vector<int>& destFds,
int syncFd, uint32_t width, uint32_t height,
bool hdr, float flow, bool perf) {
const VkExtent2D extent{ width, height };
return *this->m_contexts.emplace_back(std::make_unique<ContextImpl>(*this->m_impl,
sourceFds, destFds, syncFd,
extent, hdr, flow, perf
)).get();
}
namespace {
/// import source images
std::pair<vk::Image, vk::Image> importImages(const vk::Vulkan& vk,
const std::pair<int, int>& sourceFds,
VkExtent2D extent, VkFormat format) {
try {
return {
vk::Image(vk, extent, format,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, sourceFds.first),
vk::Image(vk, extent, format,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, sourceFds.second)
};
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to import destination images", e);
}
}
/// import destination images
std::vector<vk::Image> importImages(const vk::Vulkan& vk,
const std::vector<int>& destFds,
VkExtent2D extent, VkFormat format) {
try {
std::vector<vk::Image> destImages;
destImages.reserve(destFds.size());
for (const auto& fd : destFds)
destImages.emplace_back(vk, extent, format,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, fd);
return destImages;
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to import destination images", e);
}
}
/// create a black image
vk::Image createBlackImage(const vk::Vulkan& vk) {
try {
return{vk,
{ .width = 4, .height = 4 }
};
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to create black image", e);
}
}
/// import timeline semaphore
vk::TimelineSemaphore importTimelineSemaphore(const vk::Vulkan& vk, int syncFd) {
try {
return{vk, 0, syncFd};
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to import timeline semaphore", e);
}
}
/// create prepass semaphores
vk::TimelineSemaphore createPrepassSemaphore(const vk::Vulkan& vk) {
try {
return{vk, 0};
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to create prepass semaphore", e);
}
}
/// create command buffers
std::vector<vk::CommandBuffer> createCommandBuffers(const vk::Vulkan& vk, size_t count) {
try {
std::vector<vk::CommandBuffer> cmdbufs;
cmdbufs.reserve(count);
for (size_t i = 0; i < count; ++i)
cmdbufs.emplace_back(vk);
return cmdbufs;
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to create command buffers", e);
}
}
/// create context data
ls::Ctx createCtx(const InstanceImpl& instance, VkExtent2D extent,
bool hdr, float flow, bool perf, size_t count) {
const auto& vk = instance.getVulkan();
const auto& shaders = instance.getShaderRegistry();
try {
std::vector<vk::Buffer> constantBuffers{};
constantBuffers.reserve(count);
for (size_t i = 0; i < count; ++i)
constantBuffers.emplace_back(vk,
ls::getDefaultConstantBuffer(
i, count,
hdr, flow
)
);
return {
.vk = std::ref(vk),
.shaders = std::ref(shaders),
.constantBuffer{vk,
ls::getDefaultConstantBuffer(0, 1, hdr, flow)},
.constantBuffers{std::move(constantBuffers)},
.bnbSampler{vk, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, false},
.bnwSampler{vk, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true},
.eabSampler{vk, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false},
.sourceExtent = extent,
.flowExtent = VkExtent2D {
.width = static_cast<uint32_t>(static_cast<float>(extent.width) / flow),
.height = static_cast<uint32_t>(static_cast<float>(extent.height) / flow)
},
.hdr = hdr,
.flow = flow,
.perf = perf,
.count = count
};
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to create context", e);
}
}
}
ContextImpl::ContextImpl(const InstanceImpl& instance,
std::pair<int, int> sourceFds, const std::vector<int>& destFds, int syncFd,
VkExtent2D extent, bool hdr, float flow, bool perf) :
sourceImages(importImages(instance.getVulkan(), sourceFds,
extent, hdr ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_R8G8B8A8_UNORM)),
destImages(importImages(instance.getVulkan(), destFds,
extent, hdr ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_R8G8B8A8_UNORM)),
blackImage(createBlackImage(instance.getVulkan())),
syncSemaphore(importTimelineSemaphore(instance.getVulkan(), syncFd)),
prepassSemaphore(createPrepassSemaphore(instance.getVulkan())),
cmdbufs(createCommandBuffers(instance.getVulkan(), 16)),
ctx(createCtx(instance, extent, hdr, flow, perf, destFds.size())),
mipmaps(ctx, sourceImages),
alpha0{
chains::Alpha0(ctx, mipmaps.getImages().at(0)),
chains::Alpha0(ctx, mipmaps.getImages().at(1)),
chains::Alpha0(ctx, mipmaps.getImages().at(2)),
chains::Alpha0(ctx, mipmaps.getImages().at(3)),
chains::Alpha0(ctx, mipmaps.getImages().at(4)),
chains::Alpha0(ctx, mipmaps.getImages().at(5)),
chains::Alpha0(ctx, mipmaps.getImages().at(6))
},
alpha1{
chains::Alpha1(ctx, alpha0.at(0).getImages()),
chains::Alpha1(ctx, alpha0.at(1).getImages()),
chains::Alpha1(ctx, alpha0.at(2).getImages()),
chains::Alpha1(ctx, alpha0.at(3).getImages()),
chains::Alpha1(ctx, alpha0.at(4).getImages()),
chains::Alpha1(ctx, alpha0.at(5).getImages()),
chains::Alpha1(ctx, alpha0.at(6).getImages())
},
beta0(ctx, alpha1.at(0).getImages()),
beta1(ctx, beta0.getImages()) {
// build main passes
for (size_t i = 0; i < destImages.size(); ++i) {
auto& pass = this->passes.emplace_back();
pass.gamma0.reserve(7);
pass.gamma1.reserve(7);
pass.delta0.reserve(3);
pass.delta1.reserve(3);
for (size_t j = 0; j < 7; j++) {
if (j == 0) { // first pass has no prior data
pass.gamma0.emplace_back(ctx, i,
this->alpha1.at(6 - j).getImages(),
this->blackImage
);
pass.gamma1.emplace_back(ctx, i,
pass.gamma0.at(j).getImages(),
this->blackImage,
this->beta1.getImages().at(5)
);
} else { // other passes use prior data
pass.gamma0.emplace_back(ctx, i,
this->alpha1.at(6 - j).getImages(),
pass.gamma1.at(j - 1).getImage()
);
pass.gamma1.emplace_back(ctx, i,
pass.gamma0.at(j).getImages(),
pass.gamma1.at(j - 1).getImage(),
this->beta1.getImages().at(6 - j)
);
}
if (j == 4) { // first special pass has no prior data
pass.delta0.emplace_back(ctx, i,
this->alpha1.at(6 - j).getImages(),
this->blackImage,
pass.gamma1.at(j - 1).getImage(),
this->blackImage
);
pass.delta1.emplace_back(ctx, i,
pass.delta0.at(j - 4).getImages0(),
pass.delta0.at(j - 4).getImages1(),
this->blackImage,
this->beta1.getImages().at(6 - j),
this->blackImage
);
} else if (j > 4) { // further passes do
pass.delta0.emplace_back(ctx, i,
this->alpha1.at(6 - j).getImages(),
pass.delta1.at(j - 5).getImage0(),
pass.gamma1.at(j - 1).getImage(),
this->beta1.getImages().at(6 - j)
);
pass.delta1.emplace_back(ctx, i,
pass.delta0.at(j - 4).getImages0(),
pass.delta0.at(j - 4).getImages1(),
pass.delta1.at(j - 5).getImage0(),
this->beta1.getImages().at(6 - j),
pass.delta1.at(j - 5).getImage1()
);
}
}
pass.generate.emplace(ctx, i,
this->sourceImages,
pass.gamma1.at(6).getImage(),
pass.delta1.at(2).getImage0(),
pass.delta1.at(2).getImage1(),
this->destImages.at(i)
);
}
// initialize all images
const vk::CommandBuffer cmdbuf{ctx.vk};
cmdbuf.prepareImage(this->blackImage);
mipmaps.prepare(cmdbuf);
for (size_t i = 0; i < 7; ++i) {
alpha0.at(i).prepare(cmdbuf);
alpha1.at(i).prepare(cmdbuf);
}
beta0.prepare(cmdbuf);
beta1.prepare(cmdbuf);
for (const auto& pass : this->passes) {
for (size_t i = 0; i < 7; ++i) {
pass.gamma0.at(i).prepare(cmdbuf);
pass.gamma1.at(i).prepare(cmdbuf);
if (i < 4) continue;
pass.delta0.at(i - 4).prepare(cmdbuf);
pass.delta1.at(i - 4).prepare(cmdbuf);
}
}
cmdbuf.submit(ctx.vk); // wait for completion
}
void Instance::scheduleFrames(Context& context) {
#ifdef LSFGVK__RENDERDOC_INTEGRATION
const auto& impl = this->m_impl;
if (impl->getRenderDocAPI()) {
impl->getRenderDocAPI()->StartFrameCapture(
RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(impl->getVulkan().inst()),
nullptr);
}
#endif
try {
context.scheduleFrames();
} catch (const std::exception& e) {
throw lsfgvk::error("Unable to schedule frames", e);
}
#ifdef LSFGVK__RENDERDOC_INTEGRATION
if (impl->getRenderDocAPI()) {
vkDeviceWaitIdle(impl->getVulkan().dev());
impl->getRenderDocAPI()->EndFrameCapture(
RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(impl->getVulkan().inst()),
nullptr);
}
#endif
}
void Context::scheduleFrames() {
// schedule pre-pass
vk::CommandBuffer& cmdbuf = this->cmdbufs.at(this->cmdbuf_idx++ % this->cmdbufs.size());
cmdbuf = vk::CommandBuffer(this->ctx.vk);
this->mipmaps.render(cmdbuf, this->fidx);
for (size_t i = 0; i < 7; ++i) {
this->alpha0.at(6 - i).render(cmdbuf);
this->alpha1.at(6 - i).render(cmdbuf, this->fidx);
}
this->beta0.render(cmdbuf, this->fidx);
this->beta1.render(cmdbuf);
cmdbuf.submit(this->ctx.vk,
this->syncSemaphore, this->idx,
this->prepassSemaphore, this->idx
);
this->idx++;
// schedule main passes
for (size_t i = 0; i < this->destImages.size(); i++) {
vk::CommandBuffer& cmdbuf = this->cmdbufs.at(this->cmdbuf_idx++ % this->cmdbufs.size());
cmdbuf = vk::CommandBuffer(this->ctx.vk);
const auto& pass = this->passes.at(i);
for (size_t j = 0; j < 7; j++) {
pass.gamma0.at(j).render(cmdbuf, this->fidx);
pass.gamma1.at(j).render(cmdbuf);
if (j < 4) continue;
pass.delta0.at(j - 4).render(cmdbuf, this->fidx);
pass.delta1.at(j - 4).render(cmdbuf);
}
pass.generate->render(cmdbuf, this->fidx);
cmdbuf.submit(this->ctx.vk,
this->prepassSemaphore, this->idx - 1,
this->syncSemaphore, this->idx + i
);
}
this->idx += this->destImages.size();
this->fidx++;
}
void Instance::closeContext(const Context& context) {
auto it = std::ranges::find_if(this->m_contexts,
[context = &context](const std::unique_ptr<ContextImpl>& ctx) {
return ctx.get() == context;
});
if (it == this->m_contexts.end())
throw lsfgvk::error("attempted to close unknown context",
std::runtime_error("no such context"));
vkDeviceWaitIdle(this->m_impl->getVulkan().dev());
this->m_contexts.erase(it);
}
Instance::~Instance() = default;
error::error(const std::string& msg, const std::exception& inner)
: std::runtime_error(msg + "\n- " + inner.what()) {}
error::~error() = default;