renderer_vulkan: Add vulkan swapchain class
* This commit implements the Vulkan swapchain required for presenting images to the screen. Currently it's not complete as the API will be finalized in the next subsequent commits.
This commit is contained in:
@ -83,8 +83,6 @@ add_library(video_core STATIC
|
||||
renderer_vulkan/vk_rasterizer.h
|
||||
renderer_vulkan/vk_pipeline.cpp
|
||||
renderer_vulkan/vk_pipeline.h
|
||||
renderer_vulkan/vk_pipeline_manager.cpp
|
||||
renderer_vulkan/vk_pipeline_manager.h
|
||||
renderer_vulkan/vk_shader_state.h
|
||||
renderer_vulkan/vk_state.cpp
|
||||
renderer_vulkan/vk_state.h
|
||||
|
@ -43,6 +43,12 @@ public:
|
||||
vk::Device& GetDevice() { return device.get(); }
|
||||
vk::PhysicalDevice& GetPhysicalDevice() { return physical_device; }
|
||||
|
||||
/// Retrieve queue information
|
||||
u32 GetGraphicsQueueFamilyIndex() const { return graphics_queue_family_index; }
|
||||
u32 GetPresentQueueFamilyIndex() const { return present_queue_family_index; }
|
||||
vk::Queue GetGraphicsQueue() { return graphics_queue; }
|
||||
vk::Queue GetPresentQueue() { return present_queue; }
|
||||
|
||||
/// Feature support
|
||||
bool SupportsAnisotropicFiltering() const;
|
||||
|
||||
|
@ -1,668 +0,0 @@
|
||||
// Copyright 2022 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "core/core.h"
|
||||
#include "video_core/video_core.h"
|
||||
#include "core/frontend/scope_acquire_context.h"
|
||||
#include "video_core/renderer_vulkan/vk_pipeline_manager.h"
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <boost/variant.hpp>
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
static u64 GetUniqueIdentifier(const Pica::Regs& regs, const ProgramCode& code) {
|
||||
std::size_t hash = 0;
|
||||
u64 regs_uid = Common::ComputeHash64(regs.reg_array.data(), Pica::Regs::NUM_REGS * sizeof(u32));
|
||||
boost::hash_combine(hash, regs_uid);
|
||||
if (code.size() > 0) {
|
||||
u64 code_uid = Common::ComputeHash64(code.data(), code.size() * sizeof(u32));
|
||||
boost::hash_combine(hash, code_uid);
|
||||
}
|
||||
return static_cast<u64>(hash);
|
||||
}
|
||||
|
||||
static OGLProgram GeneratePrecompiledProgram(const ShaderDiskCacheDump& dump,
|
||||
const std::set<GLenum>& supported_formats,
|
||||
bool separable) {
|
||||
|
||||
if (supported_formats.find(dump.binary_format) == supported_formats.end()) {
|
||||
LOG_INFO(Render_OpenGL, "Precompiled cache entry with unsupported format - removing");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto shader = OGLProgram();
|
||||
shader.handle = glCreateProgram();
|
||||
if (separable) {
|
||||
glProgramParameteri(shader.handle, GL_PROGRAM_SEPARABLE, GL_TRUE);
|
||||
}
|
||||
glProgramBinary(shader.handle, dump.binary_format, dump.binary.data(),
|
||||
static_cast<GLsizei>(dump.binary.size()));
|
||||
|
||||
GLint link_status{};
|
||||
glGetProgramiv(shader.handle, GL_LINK_STATUS, &link_status);
|
||||
if (link_status == GL_FALSE) {
|
||||
LOG_INFO(Render_OpenGL, "Precompiled cache rejected by the driver - removing");
|
||||
return {};
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
static std::set<GLenum> GetSupportedFormats() {
|
||||
std::set<GLenum> supported_formats;
|
||||
|
||||
GLint num_formats{};
|
||||
glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &num_formats);
|
||||
|
||||
std::vector<GLint> formats(num_formats);
|
||||
glGetIntegerv(GL_PROGRAM_BINARY_FORMATS, formats.data());
|
||||
|
||||
for (const GLint format : formats)
|
||||
supported_formats.insert(static_cast<GLenum>(format));
|
||||
return supported_formats;
|
||||
}
|
||||
|
||||
static std::tuple<PicaVSConfig, Pica::Shader::ShaderSetup> BuildVSConfigFromRaw(
|
||||
const ShaderDiskCacheRaw& raw) {
|
||||
Pica::Shader::ProgramCode program_code{};
|
||||
Pica::Shader::SwizzleData swizzle_data{};
|
||||
std::copy_n(raw.GetProgramCode().begin(), Pica::Shader::MAX_PROGRAM_CODE_LENGTH,
|
||||
program_code.begin());
|
||||
std::copy_n(raw.GetProgramCode().begin() + Pica::Shader::MAX_PROGRAM_CODE_LENGTH,
|
||||
Pica::Shader::MAX_SWIZZLE_DATA_LENGTH, swizzle_data.begin());
|
||||
Pica::Shader::ShaderSetup setup;
|
||||
setup.program_code = program_code;
|
||||
setup.swizzle_data = swizzle_data;
|
||||
return {PicaVSConfig{raw.GetRawShaderConfig().vs, setup}, setup};
|
||||
}
|
||||
|
||||
static void SetShaderUniformBlockBinding(GLuint shader, const char* name, UniformBindings binding,
|
||||
std::size_t expected_size) {
|
||||
const GLuint ub_index = glGetUniformBlockIndex(shader, name);
|
||||
if (ub_index == GL_INVALID_INDEX) {
|
||||
return;
|
||||
}
|
||||
GLint ub_size = 0;
|
||||
glGetActiveUniformBlockiv(shader, ub_index, GL_UNIFORM_BLOCK_DATA_SIZE, &ub_size);
|
||||
ASSERT_MSG(ub_size == expected_size, "Uniform block size did not match! Got {}, expected {}",
|
||||
static_cast<int>(ub_size), expected_size);
|
||||
glUniformBlockBinding(shader, ub_index, static_cast<GLuint>(binding));
|
||||
}
|
||||
|
||||
static void SetShaderUniformBlockBindings(GLuint shader) {
|
||||
SetShaderUniformBlockBinding(shader, "shader_data", UniformBindings::Common,
|
||||
sizeof(UniformData));
|
||||
SetShaderUniformBlockBinding(shader, "vs_config", UniformBindings::VS, sizeof(VSUniformData));
|
||||
}
|
||||
|
||||
static void SetShaderSamplerBinding(GLuint shader, const char* name,
|
||||
TextureUnits::TextureUnit binding) {
|
||||
GLint uniform_tex = glGetUniformLocation(shader, name);
|
||||
if (uniform_tex != -1) {
|
||||
glUniform1i(uniform_tex, binding.id);
|
||||
}
|
||||
}
|
||||
|
||||
static void SetShaderImageBinding(GLuint shader, const char* name, GLuint binding) {
|
||||
GLint uniform_tex = glGetUniformLocation(shader, name);
|
||||
if (uniform_tex != -1) {
|
||||
glUniform1i(uniform_tex, static_cast<GLint>(binding));
|
||||
}
|
||||
}
|
||||
|
||||
static void SetShaderSamplerBindings(GLuint shader) {
|
||||
OpenGLState cur_state = OpenGLState::GetCurState();
|
||||
GLuint old_program = std::exchange(cur_state.draw.shader_program, shader);
|
||||
cur_state.Apply();
|
||||
|
||||
// Set the texture samplers to correspond to different texture units
|
||||
SetShaderSamplerBinding(shader, "tex0", TextureUnits::PicaTexture(0));
|
||||
SetShaderSamplerBinding(shader, "tex1", TextureUnits::PicaTexture(1));
|
||||
SetShaderSamplerBinding(shader, "tex2", TextureUnits::PicaTexture(2));
|
||||
SetShaderSamplerBinding(shader, "tex_cube", TextureUnits::TextureCube);
|
||||
|
||||
// Set the texture samplers to correspond to different lookup table texture units
|
||||
SetShaderSamplerBinding(shader, "texture_buffer_lut_lf", TextureUnits::TextureBufferLUT_LF);
|
||||
SetShaderSamplerBinding(shader, "texture_buffer_lut_rg", TextureUnits::TextureBufferLUT_RG);
|
||||
SetShaderSamplerBinding(shader, "texture_buffer_lut_rgba", TextureUnits::TextureBufferLUT_RGBA);
|
||||
|
||||
SetShaderImageBinding(shader, "shadow_buffer", ImageUnits::ShadowBuffer);
|
||||
SetShaderImageBinding(shader, "shadow_texture_px", ImageUnits::ShadowTexturePX);
|
||||
SetShaderImageBinding(shader, "shadow_texture_nx", ImageUnits::ShadowTextureNX);
|
||||
SetShaderImageBinding(shader, "shadow_texture_py", ImageUnits::ShadowTexturePY);
|
||||
SetShaderImageBinding(shader, "shadow_texture_ny", ImageUnits::ShadowTextureNY);
|
||||
SetShaderImageBinding(shader, "shadow_texture_pz", ImageUnits::ShadowTexturePZ);
|
||||
SetShaderImageBinding(shader, "shadow_texture_nz", ImageUnits::ShadowTextureNZ);
|
||||
|
||||
cur_state.draw.shader_program = old_program;
|
||||
cur_state.Apply();
|
||||
}
|
||||
|
||||
void PicaUniformsData::SetFromRegs(const Pica::ShaderRegs& regs,
|
||||
const Pica::Shader::ShaderSetup& setup) {
|
||||
std::transform(std::begin(setup.uniforms.b), std::end(setup.uniforms.b), std::begin(bools),
|
||||
[](bool value) -> BoolAligned { return {value ? GL_TRUE : GL_FALSE}; });
|
||||
std::transform(std::begin(regs.int_uniforms), std::end(regs.int_uniforms), std::begin(i),
|
||||
[](const auto& value) -> GLuvec4 {
|
||||
return {value.x.Value(), value.y.Value(), value.z.Value(), value.w.Value()};
|
||||
});
|
||||
std::transform(std::begin(setup.uniforms.f), std::end(setup.uniforms.f), std::begin(f),
|
||||
[](const auto& value) -> GLvec4 {
|
||||
return {value.x.ToFloat32(), value.y.ToFloat32(), value.z.ToFloat32(),
|
||||
value.w.ToFloat32()};
|
||||
});
|
||||
}
|
||||
|
||||
template <typename KeyConfigType,
|
||||
ShaderDecompiler::ProgramResult (*CodeGenerator)(const KeyConfigType&, bool),
|
||||
GLenum ShaderType>
|
||||
class ShaderCache {
|
||||
public:
|
||||
explicit ShaderCache(bool separable) : separable(separable) {}
|
||||
std::tuple<GLuint, std::optional<ShaderDecompiler::ProgramResult>> Get(
|
||||
const KeyConfigType& config) {
|
||||
auto [iter, new_shader] = shaders.emplace(config, OGLShaderStage{separable});
|
||||
OGLShaderStage& cached_shader = iter->second;
|
||||
std::optional<ShaderDecompiler::ProgramResult> result{};
|
||||
if (new_shader) {
|
||||
result = CodeGenerator(config, separable);
|
||||
cached_shader.Create(result->code.c_str(), ShaderType);
|
||||
}
|
||||
return {cached_shader.GetHandle(), std::move(result)};
|
||||
}
|
||||
|
||||
void Inject(const KeyConfigType& key, OGLProgram&& program) {
|
||||
OGLShaderStage stage{separable};
|
||||
stage.Inject(std::move(program));
|
||||
shaders.emplace(key, std::move(stage));
|
||||
}
|
||||
|
||||
void Inject(const KeyConfigType& key, OGLShaderStage&& stage) {
|
||||
shaders.emplace(key, std::move(stage));
|
||||
}
|
||||
|
||||
private:
|
||||
bool separable;
|
||||
std::unordered_map<KeyConfigType, OGLShaderStage> shaders;
|
||||
};
|
||||
|
||||
// This is a cache designed for shaders translated from PICA shaders. The first cache matches the
|
||||
// config structure like a normal cache does. On cache miss, the second cache matches the generated
|
||||
// GLSL code. The configuration is like this because there might be leftover code in the PICA shader
|
||||
// program buffer from the previous shader, which is hashed into the config, resulting several
|
||||
// different config values from the same shader program.
|
||||
template <typename KeyConfigType,
|
||||
std::optional<ShaderDecompiler::ProgramResult> (*CodeGenerator)(
|
||||
const Pica::Shader::ShaderSetup&, const KeyConfigType&, bool),
|
||||
GLenum ShaderType>
|
||||
class ShaderDoubleCache {
|
||||
public:
|
||||
explicit ShaderDoubleCache(bool separable) : separable(separable) {}
|
||||
std::tuple<GLuint, std::optional<ShaderDecompiler::ProgramResult>> Get(
|
||||
const KeyConfigType& key, const Pica::Shader::ShaderSetup& setup) {
|
||||
std::optional<ShaderDecompiler::ProgramResult> result{};
|
||||
auto map_it = shader_map.find(key);
|
||||
if (map_it == shader_map.end()) {
|
||||
auto program_opt = CodeGenerator(setup, key, separable);
|
||||
if (!program_opt) {
|
||||
shader_map[key] = nullptr;
|
||||
return {0, std::nullopt};
|
||||
}
|
||||
|
||||
std::string& program = program_opt->code;
|
||||
auto [iter, new_shader] = shader_cache.emplace(program, OGLShaderStage{separable});
|
||||
OGLShaderStage& cached_shader = iter->second;
|
||||
if (new_shader) {
|
||||
result.emplace();
|
||||
result->code = program;
|
||||
cached_shader.Create(program.c_str(), ShaderType);
|
||||
}
|
||||
shader_map[key] = &cached_shader;
|
||||
return {cached_shader.GetHandle(), std::move(result)};
|
||||
}
|
||||
|
||||
if (map_it->second == nullptr) {
|
||||
return {0, std::nullopt};
|
||||
}
|
||||
|
||||
return {map_it->second->GetHandle(), std::nullopt};
|
||||
}
|
||||
|
||||
void Inject(const KeyConfigType& key, std::string decomp, OGLProgram&& program) {
|
||||
OGLShaderStage stage{separable};
|
||||
stage.Inject(std::move(program));
|
||||
const auto iter = shader_cache.emplace(std::move(decomp), std::move(stage)).first;
|
||||
OGLShaderStage& cached_shader = iter->second;
|
||||
shader_map.insert_or_assign(key, &cached_shader);
|
||||
}
|
||||
|
||||
void Inject(const KeyConfigType& key, std::string decomp, OGLShaderStage&& stage) {
|
||||
const auto iter = shader_cache.emplace(std::move(decomp), std::move(stage)).first;
|
||||
OGLShaderStage& cached_shader = iter->second;
|
||||
shader_map.insert_or_assign(key, &cached_shader);
|
||||
}
|
||||
|
||||
private:
|
||||
bool separable;
|
||||
std::unordered_map<KeyConfigType, OGLShaderStage*> shader_map;
|
||||
std::unordered_map<std::string, OGLShaderStage> shader_cache;
|
||||
};
|
||||
|
||||
using ProgrammableVertexShaders =
|
||||
ShaderDoubleCache<PicaVSConfig, &GenerateVertexShader, GL_VERTEX_SHADER>;
|
||||
|
||||
using FixedGeometryShaders =
|
||||
ShaderCache<PicaFixedGSConfig, &GenerateFixedGeometryShader, GL_GEOMETRY_SHADER>;
|
||||
|
||||
using FragmentShaders = ShaderCache<PicaFSConfig, &GenerateFragmentShader, GL_FRAGMENT_SHADER>;
|
||||
|
||||
class ShaderProgramManager::Impl {
|
||||
public:
|
||||
explicit Impl(bool separable, bool is_amd)
|
||||
: is_amd(is_amd), separable(separable), programmable_vertex_shaders(separable),
|
||||
trivial_vertex_shader(separable), fixed_geometry_shaders(separable),
|
||||
fragment_shaders(separable), disk_cache(separable) {
|
||||
if (separable)
|
||||
pipeline.Create();
|
||||
}
|
||||
|
||||
struct ShaderTuple {
|
||||
GLuint vs = 0;
|
||||
GLuint gs = 0;
|
||||
GLuint fs = 0;
|
||||
|
||||
std::size_t vs_hash = 0;
|
||||
std::size_t gs_hash = 0;
|
||||
std::size_t fs_hash = 0;
|
||||
|
||||
bool operator==(const ShaderTuple& rhs) const {
|
||||
return std::tie(vs, gs, fs) == std::tie(rhs.vs, rhs.gs, rhs.fs);
|
||||
}
|
||||
|
||||
bool operator!=(const ShaderTuple& rhs) const {
|
||||
return std::tie(vs, gs, fs) != std::tie(rhs.vs, rhs.gs, rhs.fs);
|
||||
}
|
||||
|
||||
std::size_t GetConfigHash() const {
|
||||
std::size_t hash = 0;
|
||||
boost::hash_combine(hash, vs_hash);
|
||||
boost::hash_combine(hash, gs_hash);
|
||||
boost::hash_combine(hash, fs_hash);
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
bool is_amd;
|
||||
bool separable;
|
||||
|
||||
ShaderTuple current;
|
||||
|
||||
ProgrammableVertexShaders programmable_vertex_shaders;
|
||||
TrivialVertexShader trivial_vertex_shader;
|
||||
|
||||
FixedGeometryShaders fixed_geometry_shaders;
|
||||
|
||||
FragmentShaders fragment_shaders;
|
||||
std::unordered_map<u64, OGLProgram> program_cache;
|
||||
OGLPipeline pipeline;
|
||||
ShaderDiskCache disk_cache;
|
||||
};
|
||||
|
||||
ShaderProgramManager::ShaderProgramManager(Frontend::EmuWindow& emu_window_, bool separable,
|
||||
bool is_amd)
|
||||
: impl(std::make_unique<Impl>(separable, is_amd)), emu_window{emu_window_} {}
|
||||
|
||||
ShaderProgramManager::~ShaderProgramManager() = default;
|
||||
|
||||
bool ShaderProgramManager::UseProgrammableVertexShader(const Pica::Regs& regs,
|
||||
Pica::Shader::ShaderSetup& setup) {
|
||||
PicaVSConfig config{regs.vs, setup};
|
||||
auto [handle, result] = impl->programmable_vertex_shaders.Get(config, setup);
|
||||
if (handle == 0)
|
||||
return false;
|
||||
impl->current.vs = handle;
|
||||
impl->current.vs_hash = config.Hash();
|
||||
|
||||
// Save VS to the disk cache if its a new shader
|
||||
if (result) {
|
||||
auto& disk_cache = impl->disk_cache;
|
||||
ProgramCode program_code{setup.program_code.begin(), setup.program_code.end()};
|
||||
program_code.insert(program_code.end(), setup.swizzle_data.begin(),
|
||||
setup.swizzle_data.end());
|
||||
const u64 unique_identifier = GetUniqueIdentifier(regs, program_code);
|
||||
const ShaderDiskCacheRaw raw{unique_identifier, ProgramType::VS, regs,
|
||||
std::move(program_code)};
|
||||
disk_cache.SaveRaw(raw);
|
||||
disk_cache.SaveDecompiled(unique_identifier, *result, VideoCore::g_hw_shader_accurate_mul);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderProgramManager::UseTrivialVertexShader() {
|
||||
impl->current.vs = impl->trivial_vertex_shader.Get();
|
||||
impl->current.vs_hash = 0;
|
||||
}
|
||||
|
||||
void ShaderProgramManager::UseFixedGeometryShader(const Pica::Regs& regs) {
|
||||
PicaFixedGSConfig gs_config(regs);
|
||||
auto [handle, _] = impl->fixed_geometry_shaders.Get(gs_config);
|
||||
impl->current.gs = handle;
|
||||
impl->current.gs_hash = gs_config.Hash();
|
||||
}
|
||||
|
||||
void ShaderProgramManager::UseTrivialGeometryShader() {
|
||||
impl->current.gs = 0;
|
||||
impl->current.gs_hash = 0;
|
||||
}
|
||||
|
||||
void ShaderProgramManager::UseFragmentShader(const Pica::Regs& regs) {
|
||||
PicaFSConfig config = PicaFSConfig::BuildFromRegs(regs);
|
||||
auto [handle, result] = impl->fragment_shaders.Get(config);
|
||||
impl->current.fs = handle;
|
||||
impl->current.fs_hash = config.Hash();
|
||||
// Save FS to the disk cache if its a new shader
|
||||
if (result) {
|
||||
auto& disk_cache = impl->disk_cache;
|
||||
u64 unique_identifier = GetUniqueIdentifier(regs, {});
|
||||
ShaderDiskCacheRaw raw{unique_identifier, ProgramType::FS, regs, {}};
|
||||
disk_cache.SaveRaw(raw);
|
||||
disk_cache.SaveDecompiled(unique_identifier, *result, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderProgramManager::ApplyTo(OpenGLState& state) {
|
||||
if (impl->separable) {
|
||||
if (impl->is_amd) {
|
||||
// Without this reseting, AMD sometimes freezes when one stage is changed but not
|
||||
// for the others. On the other hand, including this reset seems to introduce memory
|
||||
// leak in Intel Graphics.
|
||||
glUseProgramStages(
|
||||
impl->pipeline.handle,
|
||||
GL_VERTEX_SHADER_BIT | GL_GEOMETRY_SHADER_BIT | GL_FRAGMENT_SHADER_BIT, 0);
|
||||
}
|
||||
|
||||
glUseProgramStages(impl->pipeline.handle, GL_VERTEX_SHADER_BIT, impl->current.vs);
|
||||
glUseProgramStages(impl->pipeline.handle, GL_GEOMETRY_SHADER_BIT, impl->current.gs);
|
||||
glUseProgramStages(impl->pipeline.handle, GL_FRAGMENT_SHADER_BIT, impl->current.fs);
|
||||
state.draw.shader_program = 0;
|
||||
state.draw.program_pipeline = impl->pipeline.handle;
|
||||
} else {
|
||||
const u64 unique_identifier = impl->current.GetConfigHash();
|
||||
OGLProgram& cached_program = impl->program_cache[unique_identifier];
|
||||
if (cached_program.handle == 0) {
|
||||
cached_program.Create(false, {impl->current.vs, impl->current.gs, impl->current.fs});
|
||||
auto& disk_cache = impl->disk_cache;
|
||||
disk_cache.SaveDumpToFile(unique_identifier, cached_program.handle,
|
||||
VideoCore::g_hw_shader_accurate_mul);
|
||||
|
||||
SetShaderUniformBlockBindings(cached_program.handle);
|
||||
SetShaderSamplerBindings(cached_program.handle);
|
||||
}
|
||||
state.draw.shader_program = cached_program.handle;
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading,
|
||||
const VideoCore::DiskResourceLoadCallback& callback) {
|
||||
if (!GLAD_GL_ARB_get_program_binary && !GLES) {
|
||||
LOG_ERROR(Render_OpenGL,
|
||||
"Cannot load disk cache as ARB_get_program_binary is not supported!");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& disk_cache = impl->disk_cache;
|
||||
const auto transferable = disk_cache.LoadTransferable();
|
||||
if (!transferable) {
|
||||
return;
|
||||
}
|
||||
const auto& raws = *transferable;
|
||||
|
||||
// Load uncompressed precompiled file for non-separable shaders.
|
||||
// Precompiled file for separable shaders is compressed.
|
||||
auto [decompiled, dumps] = disk_cache.LoadPrecompiled(impl->separable);
|
||||
|
||||
if (stop_loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::set<GLenum> supported_formats = GetSupportedFormats();
|
||||
|
||||
// Track if precompiled cache was altered during loading to know if we have to serialize the
|
||||
// virtual precompiled cache file back to the hard drive
|
||||
bool precompiled_cache_altered = false;
|
||||
|
||||
std::mutex mutex;
|
||||
std::atomic_bool compilation_failed = false;
|
||||
if (callback) {
|
||||
callback(VideoCore::LoadCallbackStage::Decompile, 0, raws.size());
|
||||
}
|
||||
std::vector<std::size_t> load_raws_index;
|
||||
// Loads both decompiled and precompiled shaders from the cache. If either one is missing for
|
||||
const auto LoadPrecompiledShader = [&](std::size_t begin, std::size_t end,
|
||||
const std::vector<ShaderDiskCacheRaw>& raw_cache,
|
||||
const ShaderDecompiledMap& decompiled_map,
|
||||
const ShaderDumpsMap& dump_map) {
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
if (stop_loading || compilation_failed) {
|
||||
return;
|
||||
}
|
||||
const auto& raw{raw_cache[i]};
|
||||
const u64 unique_identifier{raw.GetUniqueIdentifier()};
|
||||
|
||||
const u64 calculated_hash =
|
||||
GetUniqueIdentifier(raw.GetRawShaderConfig(), raw.GetProgramCode());
|
||||
if (unique_identifier != calculated_hash) {
|
||||
LOG_ERROR(Render_OpenGL,
|
||||
"Invalid hash in entry={:016x} (obtained hash={:016x}) - removing "
|
||||
"shader cache",
|
||||
raw.GetUniqueIdentifier(), calculated_hash);
|
||||
disk_cache.InvalidateAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto dump{dump_map.find(unique_identifier)};
|
||||
const auto decomp{decompiled_map.find(unique_identifier)};
|
||||
|
||||
OGLProgram shader;
|
||||
|
||||
if (dump != dump_map.end() && decomp != decompiled_map.end()) {
|
||||
// Only load the vertex shader if its sanitize_mul setting matches
|
||||
if (raw.GetProgramType() == ProgramType::VS &&
|
||||
decomp->second.sanitize_mul != VideoCore::g_hw_shader_accurate_mul) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the shader is dumped, attempt to load it
|
||||
shader =
|
||||
GeneratePrecompiledProgram(dump->second, supported_formats, impl->separable);
|
||||
if (shader.handle == 0) {
|
||||
// If any shader failed, stop trying to compile, delete the cache, and start
|
||||
// loading from raws
|
||||
compilation_failed = true;
|
||||
return;
|
||||
}
|
||||
// we have both the binary shader and the decompiled, so inject it into the
|
||||
// cache
|
||||
if (raw.GetProgramType() == ProgramType::VS) {
|
||||
auto [conf, setup] = BuildVSConfigFromRaw(raw);
|
||||
std::scoped_lock lock(mutex);
|
||||
impl->programmable_vertex_shaders.Inject(conf, decomp->second.result.code,
|
||||
std::move(shader));
|
||||
} else if (raw.GetProgramType() == ProgramType::FS) {
|
||||
PicaFSConfig conf = PicaFSConfig::BuildFromRegs(raw.GetRawShaderConfig());
|
||||
std::scoped_lock lock(mutex);
|
||||
impl->fragment_shaders.Inject(conf, std::move(shader));
|
||||
} else {
|
||||
// Unsupported shader type got stored somehow so nuke the cache
|
||||
|
||||
LOG_CRITICAL(Frontend, "failed to load raw ProgramType {}",
|
||||
raw.GetProgramType());
|
||||
compilation_failed = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Since precompiled didn't have the dump, we'll load them in the next phase
|
||||
std::scoped_lock lock(mutex);
|
||||
load_raws_index.push_back(i);
|
||||
}
|
||||
if (callback) {
|
||||
callback(VideoCore::LoadCallbackStage::Decompile, i, raw_cache.size());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto LoadPrecompiledProgram = [&](const ShaderDecompiledMap& decompiled_map,
|
||||
const ShaderDumpsMap& dump_map) {
|
||||
std::size_t i{0};
|
||||
for (const auto& dump : dump_map) {
|
||||
if (stop_loading) {
|
||||
break;
|
||||
}
|
||||
const u64 unique_identifier{dump.first};
|
||||
const auto decomp{decompiled_map.find(unique_identifier)};
|
||||
|
||||
// Only load the program if its sanitize_mul setting matches
|
||||
if (decomp->second.sanitize_mul != VideoCore::g_hw_shader_accurate_mul) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the shader program is dumped, attempt to load it
|
||||
OGLProgram shader =
|
||||
GeneratePrecompiledProgram(dump.second, supported_formats, impl->separable);
|
||||
if (shader.handle != 0) {
|
||||
SetShaderUniformBlockBindings(shader.handle);
|
||||
SetShaderSamplerBindings(shader.handle);
|
||||
impl->program_cache.emplace(unique_identifier, std::move(shader));
|
||||
} else {
|
||||
LOG_ERROR(Frontend, "Failed to link Precompiled program!");
|
||||
compilation_failed = true;
|
||||
break;
|
||||
}
|
||||
if (callback) {
|
||||
callback(VideoCore::LoadCallbackStage::Decompile, ++i, dump_map.size());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (impl->separable) {
|
||||
LoadPrecompiledShader(0, raws.size(), raws, decompiled, dumps);
|
||||
} else {
|
||||
LoadPrecompiledProgram(decompiled, dumps);
|
||||
}
|
||||
|
||||
bool load_all_raws = false;
|
||||
if (compilation_failed) {
|
||||
// Invalidate the precompiled cache if a shader dumped shader was rejected
|
||||
impl->program_cache.clear();
|
||||
disk_cache.InvalidatePrecompiled();
|
||||
dumps.clear();
|
||||
precompiled_cache_altered = true;
|
||||
load_all_raws = true;
|
||||
}
|
||||
// TODO(SachinV): Skip loading raws until we implement a proper way to link non-seperable
|
||||
// shaders.
|
||||
if (!impl->separable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t load_raws_size = load_all_raws ? raws.size() : load_raws_index.size();
|
||||
|
||||
if (callback) {
|
||||
callback(VideoCore::LoadCallbackStage::Build, 0, load_raws_size);
|
||||
}
|
||||
|
||||
compilation_failed = false;
|
||||
|
||||
std::size_t built_shaders = 0; // It doesn't have be atomic since it's used behind a mutex
|
||||
const auto LoadRawSepareble = [&](Frontend::GraphicsContext* context, std::size_t begin,
|
||||
std::size_t end) {
|
||||
Frontend::ScopeAcquireContext scope(*context);
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
if (stop_loading || compilation_failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t raws_index = load_all_raws ? i : load_raws_index[i];
|
||||
const auto& raw{raws[raws_index]};
|
||||
const u64 unique_identifier{raw.GetUniqueIdentifier()};
|
||||
|
||||
bool sanitize_mul = false;
|
||||
GLuint handle{0};
|
||||
std::optional<ShaderDecompiler::ProgramResult> result;
|
||||
// Otherwise decompile and build the shader at boot and save the result to the
|
||||
// precompiled file
|
||||
if (raw.GetProgramType() == ProgramType::VS) {
|
||||
auto [conf, setup] = BuildVSConfigFromRaw(raw);
|
||||
result = GenerateVertexShader(setup, conf, impl->separable);
|
||||
OGLShaderStage stage{impl->separable};
|
||||
stage.Create(result->code.c_str(), GL_VERTEX_SHADER);
|
||||
handle = stage.GetHandle();
|
||||
sanitize_mul = conf.state.sanitize_mul;
|
||||
std::scoped_lock lock(mutex);
|
||||
impl->programmable_vertex_shaders.Inject(conf, result->code, std::move(stage));
|
||||
} else if (raw.GetProgramType() == ProgramType::FS) {
|
||||
PicaFSConfig conf = PicaFSConfig::BuildFromRegs(raw.GetRawShaderConfig());
|
||||
result = GenerateFragmentShader(conf, impl->separable);
|
||||
OGLShaderStage stage{impl->separable};
|
||||
stage.Create(result->code.c_str(), GL_FRAGMENT_SHADER);
|
||||
handle = stage.GetHandle();
|
||||
std::scoped_lock lock(mutex);
|
||||
impl->fragment_shaders.Inject(conf, std::move(stage));
|
||||
} else {
|
||||
// Unsupported shader type got stored somehow so nuke the cache
|
||||
LOG_ERROR(Frontend, "failed to load raw ProgramType {}", raw.GetProgramType());
|
||||
compilation_failed = true;
|
||||
return;
|
||||
}
|
||||
if (handle == 0) {
|
||||
LOG_ERROR(Frontend, "compilation from raw failed {:x} {:x}",
|
||||
raw.GetProgramCode().at(0), raw.GetProgramCode().at(1));
|
||||
compilation_failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
std::scoped_lock lock(mutex);
|
||||
// If this is a new separable shader, add it the precompiled cache
|
||||
if (result) {
|
||||
disk_cache.SaveDecompiled(unique_identifier, *result, sanitize_mul);
|
||||
disk_cache.SaveDump(unique_identifier, handle);
|
||||
precompiled_cache_altered = true;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(VideoCore::LoadCallbackStage::Build, ++built_shaders, load_raws_size);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const std::size_t num_workers{std::max(1U, std::thread::hardware_concurrency())};
|
||||
const std::size_t bucket_size{load_raws_size / num_workers};
|
||||
std::vector<std::unique_ptr<Frontend::GraphicsContext>> contexts(num_workers);
|
||||
std::vector<std::thread> threads(num_workers);
|
||||
for (std::size_t i = 0; i < num_workers; ++i) {
|
||||
const bool is_last_worker = i + 1 == num_workers;
|
||||
const std::size_t start{bucket_size * i};
|
||||
const std::size_t end{is_last_worker ? load_raws_size : start + bucket_size};
|
||||
|
||||
// On some platforms the shared context has to be created from the GUI thread
|
||||
contexts[i] = emu_window.CreateSharedContext();
|
||||
threads[i] = std::thread(LoadRawSepareble, contexts[i].get(), start, end);
|
||||
}
|
||||
for (auto& thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
if (compilation_failed) {
|
||||
disk_cache.InvalidateAll();
|
||||
}
|
||||
|
||||
if (precompiled_cache_altered) {
|
||||
disk_cache.SaveVirtualPrecompiledFile();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OpenGL
|
@ -1,131 +0,0 @@
|
||||
// Copyright 2022 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
#include "video_core/rasterizer_interface.h"
|
||||
#include "video_core/regs_lighting.h"
|
||||
#include "video_core/renderer_vulkan/pica_to_vulkan.h"
|
||||
#include "video_core/renderer_vulkan/vk_shader_state.h"
|
||||
#include "video_core/renderer_vulkan/vk_texture.h"
|
||||
|
||||
namespace Core {
|
||||
class System;
|
||||
}
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
enum class UniformBindings : GLuint { Common, VS, GS };
|
||||
|
||||
struct LightSrc {
|
||||
alignas(16) glm::vec3 specular_0;
|
||||
alignas(16) glm::vec3 specular_1;
|
||||
alignas(16) glm::vec3 diffuse;
|
||||
alignas(16) glm::vec3 ambient;
|
||||
alignas(16) glm::vec3 position;
|
||||
alignas(16) glm::vec3 spot_direction; // negated
|
||||
float dist_atten_bias;
|
||||
float dist_atten_scale;
|
||||
};
|
||||
|
||||
/// Uniform structure for the Uniform Buffer Object, all vectors must be 16-byte aligned
|
||||
// NOTE: Always keep a vec4 at the end. The GL spec is not clear wether the alignment at
|
||||
// the end of a uniform block is included in UNIFORM_BLOCK_DATA_SIZE or not.
|
||||
// Not following that rule will cause problems on some AMD drivers.
|
||||
struct UniformData {
|
||||
int framebuffer_scale;
|
||||
int alphatest_ref;
|
||||
float depth_scale;
|
||||
float depth_offset;
|
||||
float shadow_bias_constant;
|
||||
float shadow_bias_linear;
|
||||
int scissor_x1;
|
||||
int scissor_y1;
|
||||
int scissor_x2;
|
||||
int scissor_y2;
|
||||
int fog_lut_offset;
|
||||
int proctex_noise_lut_offset;
|
||||
int proctex_color_map_offset;
|
||||
int proctex_alpha_map_offset;
|
||||
int proctex_lut_offset;
|
||||
int proctex_diff_lut_offset;
|
||||
float proctex_bias;
|
||||
int shadow_texture_bias;
|
||||
alignas(16) glm::ivec4 lighting_lut_offset[Pica::LightingRegs::NumLightingSampler / 4];
|
||||
alignas(16) glm::vec3 fog_color;
|
||||
alignas(8) glm::vec2 proctex_noise_f;
|
||||
alignas(8) glm::vec2 proctex_noise_a;
|
||||
alignas(8) glm::vec2 proctex_noise_p;
|
||||
alignas(16) glm::vec3 lighting_global_ambient;
|
||||
LightSrc light_src[8];
|
||||
alignas(16) glm::vec4 const_color[6]; // A vec4 color for each of the six tev stages
|
||||
alignas(16) glm::vec4 tev_combiner_buffer_color;
|
||||
alignas(16) glm::vec4 clip_coef;
|
||||
};
|
||||
|
||||
static_assert(sizeof(UniformData) == 0x4F0, "The size of the UniformData structure has changed, update the structure in the shader");
|
||||
static_assert(sizeof(UniformData) < 16384, "UniformData structure must be less than 16kb as per the OpenGL spec");
|
||||
|
||||
/// Uniform struct for the Uniform Buffer Object that contains PICA vertex/geometry shader uniforms.
|
||||
// NOTE: the same rule from UniformData also applies here.
|
||||
struct PicaUniformsData {
|
||||
void SetFromRegs(const Pica::ShaderRegs& regs, const Pica::Shader::ShaderSetup& setup);
|
||||
|
||||
struct BoolAligned {
|
||||
alignas(16) int b;
|
||||
};
|
||||
|
||||
std::array<BoolAligned, 16> bools;
|
||||
alignas(16) std::array<glm::uvec4, 4> i;
|
||||
alignas(16) std::array<glm::vec4, 96> f;
|
||||
};
|
||||
|
||||
struct VSUniformData {
|
||||
PicaUniformsData uniforms;
|
||||
};
|
||||
|
||||
static_assert(sizeof(VSUniformData) == 1856, "The size of the VSUniformData structure has changed, update the structure in the shader");
|
||||
static_assert(sizeof(VSUniformData) < 16384, "VSUniformData structure must be less than 16kb as per the Vulkan spec");
|
||||
|
||||
using Resource = std::variant<VKBuffer, VKTexture>;
|
||||
|
||||
/// Includes all required information to build a Vulkan pipeline object
|
||||
class VKPipelineInfo : private NonCopyable {
|
||||
VKPipelineInfo() = default;
|
||||
~VKPipelineInfo() = default;
|
||||
|
||||
/// Assign a shader module to a specific stage
|
||||
void AddShaderModule(const vk::ShaderModule& module, vk::ShaderStageFlagBits stage);
|
||||
|
||||
/// Add a texture or a buffer to the target descriptor set
|
||||
void AddResource(const Resource& resource, vk::DescriptorType type, vk::ShaderStageFlags stages, int set = 0);
|
||||
|
||||
private:
|
||||
using ResourceInfo = std::pair<std::reference_wrapper<Resource>, vk::DescriptorSetLayoutBinding>;
|
||||
|
||||
std::unordered_map<int, std::vector<ResourceInfo>> descriptor_sets;
|
||||
std::vector<vk::PipelineShaderStageCreateInfo> shader_stages;
|
||||
};
|
||||
|
||||
/// A class that manages the storage and management of Vulkan pipeline objects.
|
||||
class PipelineManager {
|
||||
public:
|
||||
PipelineManager(Frontend::EmuWindow& emu_window);
|
||||
~PipelineManager();
|
||||
|
||||
/// Retrieves the Vulkan pipeline that maps to the current PICA state.
|
||||
/// If not present, it is compiled and cached
|
||||
vk::Pipeline GetPipeline(const Pica::Regs& config, Pica::Shader::ShaderSetup& setup);
|
||||
|
||||
private:
|
||||
std::unordered_map<VKPipelineCacheKey, vk::UniquePipeline> pipelines;
|
||||
vk::UniquePipelineCache pipeline_cache;
|
||||
|
||||
Frontend::EmuWindow& emu_window;
|
||||
};
|
||||
} // namespace Vulkan
|
@ -13,16 +13,102 @@
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
struct SwapchainDetails {
|
||||
vk::SurfaceFormatKHR format;
|
||||
vk::PresentModeKHR present_mode;
|
||||
vk::Extent2D extent;
|
||||
vk::SurfaceTransformFlagBitsKHR transform;
|
||||
u32 image_count;
|
||||
};
|
||||
VKSwapChain::VKSwapChain(vk::SurfaceKHR surface_) : surface(surface_) {
|
||||
|
||||
SwapchainDetails PopulateSwapchainDetails(vk::SurfaceKHR surface, u32 width, u32 height) {
|
||||
SwapchainDetails details;
|
||||
}
|
||||
|
||||
bool VKSwapChain::Create(u32 width, u32 height, bool vsync_enabled) {
|
||||
is_outdated = false;
|
||||
is_suboptimal = false;
|
||||
|
||||
// Fetch information about the provided surface
|
||||
PopulateSwapchainDetails(surface, width, height);
|
||||
|
||||
// Now we can actually create the swapchain
|
||||
vk::SwapchainCreateInfoKHR swapchain_info
|
||||
(
|
||||
{},
|
||||
surface,
|
||||
details.image_count,
|
||||
details.format.format, details.format.colorSpace,
|
||||
details.extent, 1,
|
||||
vk::ImageUsageFlagBits::eColorAttachment,
|
||||
vk::SharingMode::eExclusive,
|
||||
0, nullptr,
|
||||
details.transform,
|
||||
vk::CompositeAlphaFlagBitsKHR::eOpaque,
|
||||
details.present_mode,
|
||||
VK_TRUE,
|
||||
swapchain.get()
|
||||
);
|
||||
|
||||
std::array<u32, 2> indices = {
|
||||
g_vk_instace->GetGraphicsQueueFamilyIndex(),
|
||||
g_vk_instace->GetPresentQueueFamilyIndex(),
|
||||
};
|
||||
|
||||
// For dedicated present queues, select concurrent sharing mode
|
||||
if (indices[0] != indices[1]) {
|
||||
swapchain_info.setImageSharingMode(vk::SharingMode::eConcurrent);
|
||||
swapchain_info.setQueueFamilyIndices(indices);
|
||||
}
|
||||
|
||||
auto new_swapchain = g_vk_instace->GetDevice().createSwapchainKHRUnique(swapchain_info);
|
||||
|
||||
// If an old swapchain exists, destroy it and move the new one to its place.
|
||||
// Synchronization is the responsibility of the caller, not us
|
||||
if (!!swapchain) {
|
||||
swapchain_images.clear();
|
||||
swapchain.swap(new_swapchain);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VKSwapChain::AcquireNextImage(vk::Semaphore present_semaphore) {
|
||||
const auto result = g_vk_instace->GetDevice().acquireNextImageKHR(*swapchain,
|
||||
std::numeric_limits<u64>::max(), present_semaphore,
|
||||
VK_NULL_HANDLE, &image_index);
|
||||
|
||||
switch (result) {
|
||||
case vk::Result::eSuccess:
|
||||
break;
|
||||
case vk::Result::eSuboptimalKHR:
|
||||
is_suboptimal = true;
|
||||
break;
|
||||
case vk::Result::eErrorOutOfDateKHR:
|
||||
is_outdated = true;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(Render_Vulkan, "acquireNextImageKHR returned unknown result");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VKSwapChain::Present(vk::Semaphore render_semaphore) {
|
||||
const auto present_queue = g_vk_instace->GetPresentQueue();
|
||||
|
||||
vk::PresentInfoKHR present_info(render_semaphore, swapchain.get(), image_index);
|
||||
vk::Result result = present_queue.presentKHR(present_info);
|
||||
|
||||
switch (result) {
|
||||
case vk::Result::eSuccess:
|
||||
break;
|
||||
case vk::Result::eSuboptimalKHR:
|
||||
LOG_DEBUG(Render_Vulkan, "Suboptimal swapchain");
|
||||
break;
|
||||
case vk::Result::eErrorOutOfDateKHR:
|
||||
is_outdated = true;
|
||||
break;
|
||||
default:
|
||||
LOG_CRITICAL(Render_Vulkan, "Swapchain presentation failed");
|
||||
break;
|
||||
}
|
||||
|
||||
frame_index = (frame_index + 1) % swapchain_images.size();
|
||||
}
|
||||
|
||||
void VKSwapChain::PopulateSwapchainDetails(vk::SurfaceKHR surface, u32 width, u32 height) {
|
||||
auto& gpu = g_vk_instace->GetPhysicalDevice();
|
||||
|
||||
// Choose surface format
|
||||
@ -52,7 +138,7 @@ SwapchainDetails PopulateSwapchainDetails(vk::SurfaceKHR surface, u32 width, u32
|
||||
return it != modes.end();
|
||||
};
|
||||
|
||||
// FIFO is guaranteed by the standard to be available
|
||||
// FIFO is guaranteed by the Vulkan standard to be available
|
||||
details.present_mode = vk::PresentModeKHR::eFifo;
|
||||
|
||||
// Prefer Mailbox if present for lowest latency
|
||||
@ -60,7 +146,7 @@ SwapchainDetails PopulateSwapchainDetails(vk::SurfaceKHR surface, u32 width, u32
|
||||
details.present_mode = vk::PresentModeKHR::eMailbox;
|
||||
}
|
||||
|
||||
// Query surface capabilities
|
||||
// Query surface extent
|
||||
auto capabilities = gpu.getSurfaceCapabilitiesKHR(surface);
|
||||
details.extent = capabilities.currentExtent;
|
||||
|
||||
@ -82,267 +168,39 @@ SwapchainDetails PopulateSwapchainDetails(vk::SurfaceKHR surface, u32 width, u32
|
||||
if (!(capabilities.supportedTransforms & details.transform)) {
|
||||
details.transform = capabilities.currentTransform;
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
VKSwapchain::VKSwapchain(vk::SurfaceKHR surface_) : surface(surface_) {
|
||||
void VKSwapChain::SetupImages() {
|
||||
// Get the swap chain images
|
||||
auto& device = g_vk_instace->GetDevice();
|
||||
auto images = device.getSwapchainImagesKHR(swapchain.get());
|
||||
|
||||
}
|
||||
// Create the swapchain buffers containing the image and imageview
|
||||
swapchain_images.resize(images.size());
|
||||
for (size_t i = 0; i < swapchain_images.size(); i++)
|
||||
{
|
||||
swapchain_images[i].image = images[i];
|
||||
|
||||
void VKSwapchain::Create(u32 width, u32 height, bool vsync_enabled) {
|
||||
is_outdated = false;
|
||||
is_suboptimal = false;
|
||||
|
||||
const auto gpu = g_vk_instace->GetPhysicalDevice();
|
||||
auto details = PopulateSwapchainDetails(surface, width, height);
|
||||
|
||||
// Store the old/current swap chain when recreating for resize
|
||||
vk::SwapchainKHR old_swapchain = swapchain.get();
|
||||
|
||||
// Now we can actually create the swap chain
|
||||
vk::SwapchainCreateInfoKHR swap_chain_info
|
||||
// Create swapchain image view
|
||||
vk::ImageViewCreateInfo color_attachment_view
|
||||
(
|
||||
{},
|
||||
surface,
|
||||
details.image_count,
|
||||
details.format.format, details.format.colorSpace,
|
||||
details.extent, 1,
|
||||
vk::ImageUsageFlagBits::eColorAttachment,
|
||||
vk::SharingMode::eExclusive,
|
||||
0, nullptr,
|
||||
details.transform,
|
||||
vk::CompositeAlphaFlagBitsKHR::eOpaque,
|
||||
details.present_mode,
|
||||
VK_TRUE,
|
||||
old_swapchain
|
||||
images[i],
|
||||
vk::ImageViewType::e2D,
|
||||
details.format.format,
|
||||
{},
|
||||
{ vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 }
|
||||
);
|
||||
|
||||
std::array<uint32_t, 2> indices = {{
|
||||
g_vulkan_context->GetGraphicsQueueFamilyIndex(),
|
||||
g_vulkan_context->GetPresentQueueFamilyIndex(),
|
||||
}};
|
||||
if (g_vulkan_context->GetGraphicsQueueFamilyIndex() !=
|
||||
g_vulkan_context->GetPresentQueueFamilyIndex())
|
||||
{
|
||||
swap_chain_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
|
||||
swap_chain_info.queueFamilyIndexCount = 2;
|
||||
swap_chain_info.pQueueFamilyIndices = indices.data();
|
||||
}
|
||||
swapchain_images[i].image_view = device.createImageViewUnique(color_attachment_view);
|
||||
|
||||
#ifdef SUPPORTS_VULKAN_EXCLUSIVE_FULLSCREEN
|
||||
if (m_fullscreen_supported)
|
||||
{
|
||||
VkSurfaceFullScreenExclusiveInfoEXT fullscreen_support = {};
|
||||
swap_chain_info.pNext = &fullscreen_support;
|
||||
fullscreen_support.sType = VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT;
|
||||
fullscreen_support.fullScreenExclusive = VK_FULL_SCREEN_EXCLUSIVE_APPLICATION_CONTROLLED_EXT;
|
||||
|
||||
auto platform_info = g_vulkan_context->GetPlatformExclusiveFullscreenInfo(m_wsi);
|
||||
fullscreen_support.pNext = &platform_info;
|
||||
|
||||
res = vkCreateSwapchainKHR(g_vulkan_context->GetDevice(), &swap_chain_info, nullptr,
|
||||
&m_swap_chain);
|
||||
if (res != VK_SUCCESS)
|
||||
{
|
||||
// Try without exclusive fullscreen.
|
||||
WARN_LOG_FMT(VIDEO, "Failed to create exclusive fullscreen swapchain, trying without.");
|
||||
swap_chain_info.pNext = nullptr;
|
||||
g_Config.backend_info.bSupportsExclusiveFullscreen = false;
|
||||
g_ActiveConfig.backend_info.bSupportsExclusiveFullscreen = false;
|
||||
m_fullscreen_supported = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (m_swap_chain == VK_NULL_HANDLE)
|
||||
{
|
||||
res = vkCreateSwapchainKHR(g_vulkan_context->GetDevice(), &swap_chain_info, nullptr,
|
||||
&m_swap_chain);
|
||||
}
|
||||
if (res != VK_SUCCESS)
|
||||
{
|
||||
LOG_VULKAN_ERROR(res, "vkCreateSwapchainKHR failed: ");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now destroy the old swap chain, since it's been recreated.
|
||||
// We can do this immediately since all work should have been completed before calling resize.
|
||||
if (old_swap_chain != VK_NULL_HANDLE)
|
||||
vkDestroySwapchainKHR(g_vulkan_context->GetDevice(), old_swap_chain, nullptr);
|
||||
|
||||
m_width = size.width;
|
||||
m_height = size.height;
|
||||
m_layers = image_layers;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VKSwapchain::AcquireNextImage() {
|
||||
const auto result = g_vk_instace->GetDevice().acquireNextImageKHR(*swapchain,
|
||||
std::numeric_limits<u64>::max(), *present_semaphores[frame_index],
|
||||
VK_NULL_HANDLE, &image_index);
|
||||
|
||||
switch (result) {
|
||||
case vk::Result::eSuccess:
|
||||
break;
|
||||
case vk::Result::eSuboptimalKHR:
|
||||
is_suboptimal = true;
|
||||
break;
|
||||
case vk::Result::eErrorOutOfDateKHR:
|
||||
is_outdated = true;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(Render_Vulkan, "acquireNextImageKHR returned unknown result");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VKSwapchain::Present(vk::Semaphore render_semaphore) {
|
||||
const auto present_queue{device.GetPresentQueue()};
|
||||
|
||||
|
||||
vk::PresentInfoKHR present_info(
|
||||
|
||||
const VkPresentInfoKHR present_info{
|
||||
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
|
||||
.pNext = nullptr,
|
||||
.waitSemaphoreCount = render_semaphore ? 1U : 0U,
|
||||
.pWaitSemaphores = &render_semaphore,
|
||||
.swapchainCount = 1,
|
||||
.pSwapchains = swapchain.address(),
|
||||
.pImageIndices = &image_index,
|
||||
.pResults = nullptr,
|
||||
};
|
||||
switch (const VkResult result = present_queue.Present(present_info)) {
|
||||
case VK_SUCCESS:
|
||||
break;
|
||||
case VK_SUBOPTIMAL_KHR:
|
||||
LOG_DEBUG(Render_Vulkan, "Suboptimal swapchain");
|
||||
break;
|
||||
case VK_ERROR_OUT_OF_DATE_KHR:
|
||||
is_outdated = true;
|
||||
break;
|
||||
default:
|
||||
LOG_CRITICAL(Render_Vulkan, "Failed to present with error {}", vk::ToString(result));
|
||||
break;
|
||||
}
|
||||
++frame_index;
|
||||
if (frame_index >= image_count) {
|
||||
frame_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void VKSwapchain::CreateSwapchain(const VkSurfaceCapabilitiesKHR& capabilities, u32 width,
|
||||
u32 height, bool srgb) {
|
||||
const auto physical_device{device.GetPhysical()};
|
||||
const auto formats{physical_device.GetSurfaceFormatsKHR(surface)};
|
||||
const auto present_modes{physical_device.GetSurfacePresentModesKHR(surface)};
|
||||
|
||||
const VkSurfaceFormatKHR surface_format{ChooseSwapSurfaceFormat(formats)};
|
||||
present_mode = ChooseSwapPresentMode(present_modes);
|
||||
|
||||
u32 requested_image_count{capabilities.minImageCount + 1};
|
||||
if (capabilities.maxImageCount > 0 && requested_image_count > capabilities.maxImageCount) {
|
||||
requested_image_count = capabilities.maxImageCount;
|
||||
}
|
||||
VkSwapchainCreateInfoKHR swapchain_ci{
|
||||
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.surface = surface,
|
||||
.minImageCount = requested_image_count,
|
||||
.imageFormat = surface_format.format,
|
||||
.imageColorSpace = surface_format.colorSpace,
|
||||
.imageExtent = {},
|
||||
.imageArrayLayers = 1,
|
||||
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
|
||||
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
|
||||
.queueFamilyIndexCount = 0,
|
||||
.pQueueFamilyIndices = nullptr,
|
||||
.preTransform = capabilities.currentTransform,
|
||||
.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
|
||||
.presentMode = present_mode,
|
||||
.clipped = VK_FALSE,
|
||||
.oldSwapchain = nullptr,
|
||||
};
|
||||
const u32 graphics_family{device.GetGraphicsFamily()};
|
||||
const u32 present_family{device.GetPresentFamily()};
|
||||
const std::array<u32, 2> queue_indices{graphics_family, present_family};
|
||||
if (graphics_family != present_family) {
|
||||
swapchain_ci.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
|
||||
swapchain_ci.queueFamilyIndexCount = static_cast<u32>(queue_indices.size());
|
||||
swapchain_ci.pQueueFamilyIndices = queue_indices.data();
|
||||
}
|
||||
static constexpr std::array view_formats{VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_B8G8R8A8_SRGB};
|
||||
VkImageFormatListCreateInfo format_list{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO_KHR,
|
||||
.pNext = nullptr,
|
||||
.viewFormatCount = static_cast<u32>(view_formats.size()),
|
||||
.pViewFormats = view_formats.data(),
|
||||
};
|
||||
if (device.IsKhrSwapchainMutableFormatEnabled()) {
|
||||
format_list.pNext = std::exchange(swapchain_ci.pNext, &format_list);
|
||||
swapchain_ci.flags |= VK_SWAPCHAIN_CREATE_MUTABLE_FORMAT_BIT_KHR;
|
||||
}
|
||||
// Request the size again to reduce the possibility of a TOCTOU race condition.
|
||||
const auto updated_capabilities = physical_device.GetSurfaceCapabilitiesKHR(surface);
|
||||
swapchain_ci.imageExtent = ChooseSwapExtent(updated_capabilities, width, height);
|
||||
// Don't add code within this and the swapchain creation.
|
||||
swapchain = device.GetLogical().CreateSwapchainKHR(swapchain_ci);
|
||||
|
||||
extent = swapchain_ci.imageExtent;
|
||||
current_srgb = srgb;
|
||||
current_fps_unlocked = Settings::values.disable_fps_limit.GetValue();
|
||||
|
||||
images = swapchain.GetImages();
|
||||
image_count = static_cast<u32>(images.size());
|
||||
image_view_format = srgb ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM;
|
||||
}
|
||||
|
||||
void VKSwapchain::CreateImageViews() {
|
||||
VkImageViewCreateInfo ci{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.image = {},
|
||||
.viewType = VK_IMAGE_VIEW_TYPE_2D,
|
||||
.format = image_view_format,
|
||||
.components =
|
||||
{
|
||||
.r = VK_COMPONENT_SWIZZLE_IDENTITY,
|
||||
.g = VK_COMPONENT_SWIZZLE_IDENTITY,
|
||||
.b = VK_COMPONENT_SWIZZLE_IDENTITY,
|
||||
.a = VK_COMPONENT_SWIZZLE_IDENTITY,
|
||||
},
|
||||
.subresourceRange =
|
||||
{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
// Create framebuffer for each swapchain image
|
||||
VKFramebuffer::Info fb_info = {
|
||||
.color = // TODO
|
||||
};
|
||||
|
||||
image_views.resize(image_count);
|
||||
for (std::size_t i = 0; i < image_count; i++) {
|
||||
ci.image = images[i];
|
||||
image_views[i] = device.GetLogical().CreateImageView(ci);
|
||||
swapchain_images[i].framebuffer.Create(fb_info);
|
||||
}
|
||||
}
|
||||
|
||||
void VKSwapchain::Destroy() {
|
||||
frame_index = 0;
|
||||
present_semaphores.clear();
|
||||
framebuffers.clear();
|
||||
image_views.clear();
|
||||
swapchain.reset();
|
||||
}
|
||||
|
||||
bool VKSwapchain::NeedsPresentModeUpdate() const {
|
||||
// Mailbox present mode is the ideal for all scenarios. If it is not available,
|
||||
// A different present mode is needed to support unlocked FPS above the monitor's refresh rate.
|
||||
return present_mode != VK_PRESENT_MODE_MAILBOX_KHR && HasFpsUnlockChanged();
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
|
@ -12,20 +12,29 @@ namespace Vulkan {
|
||||
|
||||
struct SwapChainImage {
|
||||
vk::Image image;
|
||||
VKTexture texture;
|
||||
vk::UniqueImageView image_view;
|
||||
VKFramebuffer framebuffer;
|
||||
};
|
||||
|
||||
class VKSwapchain {
|
||||
struct SwapChainDetails {
|
||||
vk::SurfaceFormatKHR format;
|
||||
vk::PresentModeKHR present_mode;
|
||||
vk::Extent2D extent;
|
||||
vk::SurfaceTransformFlagBitsKHR transform;
|
||||
u32 image_count;
|
||||
};
|
||||
|
||||
class VKSwapChain {
|
||||
public:
|
||||
VKSwapchain(vk::SurfaceKHR surface);
|
||||
~VKSwapchain() = default;
|
||||
VKSwapChain(vk::SurfaceKHR surface);
|
||||
~VKSwapChain() = default;
|
||||
|
||||
/// Creates (or recreates) the swapchain with a given size.
|
||||
void Create(u32 width, u32 height, bool vsync_enabled);
|
||||
bool Create(u32 width, u32 height, bool vsync_enabled);
|
||||
|
||||
/// Acquires the next image in the swapchain, waits as needed.
|
||||
void AcquireNextImage();
|
||||
/// Acquire the next image in the swapchain.
|
||||
void AcquireNextImage(vk::Semaphore present_semaphore);
|
||||
void Present(vk::Semaphore render_semaphore);
|
||||
|
||||
/// Returns true when the swapchain needs to be recreated.
|
||||
bool NeedsRecreation() const { return IsSubOptimal(); }
|
||||
@ -35,23 +44,23 @@ public:
|
||||
u32 GetCurrentImageIndex() const { return image_index; }
|
||||
|
||||
/// Get current swapchain state
|
||||
vk::Extent2D GetSize() const { return extent; }
|
||||
vk::Extent2D GetSize() const { return details.extent; }
|
||||
vk::SurfaceKHR GetSurface() const { return surface; }
|
||||
vk::SurfaceFormatKHR GetSurfaceFormat() const { return surface_format; }
|
||||
vk::Format GetTextureFormat() const { return texture_format; }
|
||||
vk::SurfaceFormatKHR GetSurfaceFormat() const { return details.format; }
|
||||
vk::SwapchainKHR GetSwapChain() const { return swapchain.get(); }
|
||||
vk::Image GetCurrentImage() const { return swapchain_images[image_index].image; }
|
||||
|
||||
/// Retrieve current texture and framebuffer
|
||||
VKTexture& GetCurrentTexture() { return swapchain_images[image_index].texture; }
|
||||
vk::Image GetCurrentImage() { return swapchain_images[image_index].image; }
|
||||
VKFramebuffer& GetCurrentFramebuffer() { return swapchain_images[image_index].framebuffer; }
|
||||
|
||||
private:
|
||||
void PopulateSwapchainDetails(vk::SurfaceKHR surface, u32 width, u32 height);
|
||||
void SetupImages();
|
||||
|
||||
private:
|
||||
SwapChainDetails details{};
|
||||
vk::SurfaceKHR surface;
|
||||
vk::SurfaceFormatKHR surface_format = {};
|
||||
vk::PresentModeKHR present_mode = vk::PresentModeKHR::eFifo;
|
||||
vk::Format texture_format = vk::Format::eUndefined;
|
||||
vk::Extent2D extent;
|
||||
bool vsync_enabled = false;
|
||||
bool is_outdated = false, is_suboptimal = false;
|
||||
|
||||
|
@ -84,8 +84,8 @@ enum Attachments {
|
||||
class VKFramebuffer final : public NonCopyable {
|
||||
public:
|
||||
struct Info {
|
||||
VKTexture* color;
|
||||
VKTexture* depth_stencil;
|
||||
VKTexture* color = nullptr;
|
||||
VKTexture* depth_stencil = nullptr;
|
||||
};
|
||||
|
||||
VKFramebuffer() = default;
|
||||
|
Reference in New Issue
Block a user