You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
duckstation/src/util/postprocessing_shader_slang...

2356 lines
82 KiB
C++

// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "postprocessing_shader_slang.h"
#include "dyn_shaderc.h"
#include "dyn_spirv_cross.h"
#include "image.h"
#include "shadergen.h"
#include "spirv_module.h"
#include "core/host.h" // TODO: Remove after removing ReadResourceFile()
#include "common/assert.h"
#include "common/bitutils.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/gsvector.h"
#include "common/gsvector_formatter.h"
#include "common/heterogeneous_containers.h"
#include "common/intrin.h"
#include "common/log.h"
#include "common/path.h"
#include "common/scoped_guard.h"
#include "common/string_util.h"
#include "fmt/format.h"
#include <bitset>
#include <cctype>
#include <cstring>
#include <string>
#include <string_view>
LOG_CHANNEL(PostProcessing);
using namespace std::string_view_literals;
// TODO:
// - Need some sort of cache for the UBO/push constant layout so we don't need to go through SPIR-V Cross every time.
namespace PostProcessing {
static constexpr float DEFAULT_OPTION_STEP = 0.02f;
static constexpr u32 MAX_SLANG_INCLUDE_DEPTH = 16;
static constexpr u32 MAX_PUSH_CONSTANT_SIZE = 128;
namespace SlangShaderStage {
enum Id
{
Common,
Vertex,
Fragment,
MaxCount,
};
}
namespace BuiltinUniform {
enum : s32
{
MVP = -1,
OutputSize = -2,
FinalViewportSize = -3,
FrameCount = -4,
FrameDirection = -5,
TextureSize = -6,
Zero = -7,
};
}
class SlangPresetParser
{
public:
SlangPresetParser();
~SlangPresetParser();
bool Parse(std::string_view path, std::string_view contents, u32 reference_nesting_level, Error* error);
bool ContainsValue(std::string_view key) const;
std::string_view GetStringValue(std::string_view key, std::string_view def) const;
bool GetBoolValue(std::string_view key, bool def) const;
s32 GetIntValue(std::string_view key, int def) const;
u32 GetUIntValue(std::string_view key, u32 def) const;
float GetFloatValue(std::string_view key, float def) const;
bool ContainsIndexedValue(std::string_view key, u32 idx) const;
std::string_view GetIndexedStringValue(std::string_view key, u32 idx, std::string_view def) const;
bool GetIndexedBoolValue(std::string_view key, u32 idx, bool def) const;
s32 GetIndexedIntValue(std::string_view key, u32 idx, int def) const;
u32 GetIndexedUIntValue(std::string_view key, u32 idx, u32 def) const;
float GetIndexedFloatValue(std::string_view key, u32 idx, float def) const;
private:
static bool GetLine(const std::string_view& contents, std::string_view* line, size_t& offset);
bool ParsePresetReference(const std::string_view& path, const std::string_view& line, u32 reference_nesting_level,
Error* error);
UnorderedStringMap<std::string> m_options;
};
class SlangShaderPreprocessor
{
public:
SlangShaderPreprocessor(Error* error);
~SlangShaderPreprocessor();
const std::string& GetShaderName() const { return m_shader_name; }
std::string TakeShaderName() { return std::move(m_shader_name); }
bool HasShaderName() const { return !m_shader_name.empty(); }
const std::vector<ShaderOption>& GetOptions() const { return m_options; }
std::vector<ShaderOption> TakeOptions() { return std::move(m_options); }
bool HasOptions() const { return !m_options.empty(); }
bool HasOutputFormat() const { return m_output_format.has_value(); }
GPUTextureFormat GetOutputFormat() const { return m_output_format.value_or(GPUTextureFormat::RGBA8); }
std::string GetVertexShader() const;
std::string GetFragmentShader() const;
bool ParseFile(std::string_view base_path, std::string_view path);
private:
static std::optional<std::pair<std::string, std::string>> ReadShaderFile(std::string_view base_path,
std::string_view path, Error* error);
std::string_view GetCurrentFilename() const;
bool GetLine(std::string_view* line);
bool HandleIncludeDirective(std::string_view line);
bool HandlePragmaDirective(std::string_view line);
bool HandlePragmaStage(std::span<const std::string_view> tokens);
bool HandlePragmaName(std::span<const std::string_view> tokens);
bool HandlePragmaParameter(std::span<const std::string_view> tokens);
bool HandlePragmaFormat(std::span<const std::string_view> tokens);
template<typename... T>
void Write(fmt::format_string<T...> fmt, T&&... args);
template<typename... T>
void SetError(fmt::format_string<T...> fmt, T&&... args);
Error* m_error;
std::string_view m_path;
std::string_view m_contents;
size_t m_current_offset = 0;
size_t m_current_line_offset = 0;
u32 m_current_line_number = 0;
bool m_needs_line_reset = false;
bool m_needs_version_directive = true;
u32 m_include_depth = 0;
SlangShaderStage::Id m_current_stage = SlangShaderStage::Common;
std::bitset<SlangShaderStage::MaxCount> m_defined_stages = {};
std::string m_shader_code[SlangShaderStage::MaxCount];
std::string m_shader_name;
std::vector<ShaderOption> m_options;
std::optional<GPUTextureFormat> m_output_format;
};
namespace {
struct SlangShaderVertex
{
float position[4];
float texcoord[2];
};
} // namespace
static std::string_view StripCommentsAndWhitespace(std::string_view line);
} // namespace PostProcessing
inline PostProcessing::SlangPresetParser::SlangPresetParser() = default;
inline PostProcessing::SlangPresetParser::~SlangPresetParser() = default;
inline bool PostProcessing::SlangPresetParser::GetLine(const std::string_view& contents, std::string_view* line,
size_t& offset)
{
const size_t length = contents.length();
if (offset == length)
return false;
size_t end_position = offset;
for (; end_position < length; end_position++)
{
// ignore carriage returns
if (contents[end_position] == '\r')
continue;
if (contents[end_position] == '\n')
break;
}
*line = contents.substr(offset, end_position - offset);
offset = std::min(end_position + 1, length);
return true;
}
inline bool PostProcessing::SlangPresetParser::Parse(std::string_view path, std::string_view contents,
u32 reference_nesting_level, Error* error)
{
u32 line_number = 0;
size_t offset = 0;
std::string_view line;
while (GetLine(contents, &line, offset))
{
line_number++;
const std::string_view clean_line = StripCommentsAndWhitespace(line);
if (clean_line.empty())
continue;
if (clean_line.starts_with("#reference "))
{
if (!ParsePresetReference(path, clean_line, reference_nesting_level, error))
return false;
continue;
}
// despite having c-style comments, presets can also use # as a comment, but #reference is a thing
// ughhhhhh what a mess
if (clean_line.starts_with('#'))
continue;
std::string_view key, value;
if (!StringUtil::ParseAssignmentString(clean_line, &key, &value) || key.empty())
{
Error::SetStringFmt(error, "{}:{} Malformed preset line", Path::GetFileName(path), line_number);
return false;
}
if (m_options.find(key) != m_options.end())
{
WARNING_LOG("{}:{} Duplicate preset key '{}'", Path::GetFileName(path), line_number, key);
continue;
}
// quotes appear in some...
std::string fixed_value(value);
std::string::size_type pos;
while ((pos = fixed_value.find('"')) != std::string::npos)
fixed_value.erase(pos, 1);
m_options.emplace(key, std::move(fixed_value));
}
return true;
}
inline bool PostProcessing::SlangPresetParser::ParsePresetReference(const std::string_view& path,
const std::string_view& line,
u32 reference_nesting_level, Error* error)
{
if (reference_nesting_level == MAX_SLANG_INCLUDE_DEPTH)
{
Error::SetStringFmt(error, "{}:{} Too many nested references", Path::GetFileName(path), line);
return false;
}
const std::string_view reference_quoted_path = StringUtil::StripWhitespace(line.substr(11));
if (reference_quoted_path.size() < 3 || !reference_quoted_path.starts_with('"') ||
!reference_quoted_path.ends_with('"'))
{
Error::SetStringFmt(error, "{}:{} Malformed preset reference", Path::GetFileName(path), line);
return false;
}
const std::string_view reference_unquoted_path = reference_quoted_path.substr(1, reference_quoted_path.size() - 2);
std::string reference_path = Path::BuildRelativePath(path, reference_unquoted_path);
Path::ToNativePath(&reference_path);
std::optional<std::string> reference_contents;
if (!Path::IsAbsolute(reference_path))
reference_contents = Host::ReadResourceFileToString(reference_path, true, error);
else
reference_contents = FileSystem::ReadFileToString(reference_path.c_str(), error);
if (!reference_contents.has_value())
{
Error::AddPrefixFmt(error, "Failed to read referenced preset {}: ", reference_unquoted_path);
return false;
}
SlangPresetParser pp;
if (!pp.Parse(reference_path, reference_contents.value(), reference_nesting_level + 1, error))
{
Error::AddPrefixFmt(error, "In referenced preset {}: ", Path::GetFileName(reference_path));
return false;
}
// once we hit a full preset, we need to fix up the paths so that they're relative to the original preset
if (const auto iter = pp.m_options.find("shaders"sv); iter != pp.m_options.end())
{
const u32 num_shaders = StringUtil::FromChars<u32>(iter->second).value_or(0);
for (u32 i = 0; i < num_shaders; i++)
{
const TinyString key = TinyString::from_format("shader{}", i);
const auto siter = pp.m_options.find(key.view());
if (siter == pp.m_options.end())
continue;
// if it's already absolute, no need to do anything
if (Path::IsAbsolute(siter->second))
continue;
// if not, we need to make it absolute, relative to the current preset file
std::string fixed_path = Path::BuildRelativePath(reference_path, siter->second);
Path::ToNativePath(&fixed_path);
DEV_LOG("Fixing up shader path in reference '{}' to '{}' (ref {})", siter->second, fixed_path,
reference_unquoted_path);
siter->second = std::move(fixed_path);
}
}
// same for textures...
if (const auto iter = pp.m_options.find("textures"sv); iter != pp.m_options.end())
{
const std::vector<std::string_view> texture_names = StringUtil::SplitString(iter->second, ';');
for (const std::string_view orig_name : texture_names)
{
const std::string_view name = StringUtil::StripWhitespace(orig_name);
if (name.empty())
continue;
const auto titer = pp.m_options.find(name);
if (titer == pp.m_options.end())
continue;
// if it's already absolute, no need to do anything
if (Path::IsAbsolute(titer->second))
continue;
// if not, we need to make it absolute, relative to the current preset file
std::string fixed_path = Path::BuildRelativePath(reference_path, titer->second);
Path::ToNativePath(&fixed_path);
DEV_LOG("Fixing up texture path in reference '{}' to '{}' (ref {})", titer->second, fixed_path,
reference_unquoted_path);
titer->second = std::move(fixed_path);
}
}
// now merge the options back
for (auto iter = pp.m_options.begin(); iter != pp.m_options.end(); ++iter)
{
const auto biter = m_options.find(iter->first);
if (biter != m_options.end())
biter->second = std::move(iter->second);
else
m_options.emplace(iter->first, std::move(iter->second));
}
return true;
}
inline bool PostProcessing::SlangPresetParser::ContainsValue(std::string_view key) const
{
return (m_options.find(key) != m_options.end());
}
inline std::string_view PostProcessing::SlangPresetParser::GetStringValue(std::string_view key,
std::string_view def) const
{
const auto iter = m_options.find(key);
return (iter != m_options.end()) ? iter->second : def;
}
inline bool PostProcessing::SlangPresetParser::GetBoolValue(std::string_view key, bool def) const
{
const auto iter = m_options.find(key);
return (iter != m_options.end()) ? StringUtil::FromChars<bool>(iter->second).value_or(def) : def;
}
inline int PostProcessing::SlangPresetParser::GetIntValue(std::string_view key, int def) const
{
const auto iter = m_options.find(key);
return (iter != m_options.end()) ? StringUtil::FromChars<s32>(iter->second).value_or(def) : def;
}
inline float PostProcessing::SlangPresetParser::GetFloatValue(std::string_view key, float def) const
{
const auto iter = m_options.find(key);
return (iter != m_options.end()) ? StringUtil::FromChars<float>(iter->second).value_or(def) : def;
}
inline u32 PostProcessing::SlangPresetParser::GetUIntValue(std::string_view key, u32 def) const
{
const auto iter = m_options.find(key);
return (iter != m_options.end()) ? StringUtil::FromChars<u32>(iter->second).value_or(def) : def;
}
inline bool PostProcessing::SlangPresetParser::ContainsIndexedValue(std::string_view key, u32 idx) const
{
const TinyString real_key = TinyString::from_format("{}{}", key, idx);
return ContainsValue(real_key);
}
inline std::string_view PostProcessing::SlangPresetParser::GetIndexedStringValue(std::string_view key, u32 idx,
std::string_view def) const
{
const TinyString real_key = TinyString::from_format("{}{}", key, idx);
return GetStringValue(real_key, def);
}
inline bool PostProcessing::SlangPresetParser::GetIndexedBoolValue(std::string_view key, u32 idx, bool def) const
{
const TinyString real_key = TinyString::from_format("{}{}", key, idx);
return GetBoolValue(real_key, def);
}
inline s32 PostProcessing::SlangPresetParser::GetIndexedIntValue(std::string_view key, u32 idx, int def) const
{
const TinyString real_key = TinyString::from_format("{}{}", key, idx);
return GetIntValue(real_key, def);
}
inline u32 PostProcessing::SlangPresetParser::GetIndexedUIntValue(std::string_view key, u32 idx, u32 def) const
{
const TinyString real_key = TinyString::from_format("{}{}", key, idx);
return GetUIntValue(real_key, def);
}
inline float PostProcessing::SlangPresetParser::GetIndexedFloatValue(std::string_view key, u32 idx, float def) const
{
const TinyString real_key = TinyString::from_format("{}{}", key, idx);
return GetFloatValue(real_key, def);
}
inline PostProcessing::SlangShaderPreprocessor::SlangShaderPreprocessor(Error* error) : m_error(error)
{
}
inline PostProcessing::SlangShaderPreprocessor::~SlangShaderPreprocessor() = default;
inline std::optional<std::pair<std::string, std::string>>
PostProcessing::SlangShaderPreprocessor::ReadShaderFile(std::string_view base_path, std::string_view path, Error* error)
{
std::string real_path(path);
if (!Path::IsAbsolute(path))
real_path = Path::BuildRelativePath(base_path, path);
Path::ToNativePath(&real_path);
std::optional<std::pair<std::string, std::string>> result;
std::optional<std::string> shader_contents = FileSystem::ReadFileToString(real_path.c_str(), error);
if (shader_contents.has_value())
result.emplace(std::move(real_path), std::move(shader_contents.value()));
else
Error::AddPrefixFmt(error, "Failed to read shader file '{}': ", path);
return result;
}
std::string_view PostProcessing::StripCommentsAndWhitespace(std::string_view line)
{
// TODO: Handle block comments, not just line comments
std::string_view clean_line = StringUtil::StripWhitespace(line);
// Parse the line to find comments, but skip string literals
bool in_string = false;
char string_delimiter = '\0';
for (size_t i = 0; i < clean_line.size(); ++i)
{
const char c = clean_line[i];
// Handle string literal delimiters
if (!in_string && (c == '"' || c == '\''))
{
in_string = true;
string_delimiter = c;
}
else if (in_string && c == string_delimiter)
{
// Check if it's escaped
if (i == 0 || clean_line[i - 1] != '\\')
{
in_string = false;
string_delimiter = '\0';
}
}
// Only process comments if we're not inside a string literal
else if (!in_string && c == '/' && i + 1 < clean_line.size() && clean_line[i + 1] == '/')
{
clean_line = StringUtil::StripWhitespace(clean_line.substr(0, i));
break;
}
}
return clean_line;
}
template<typename... T>
inline void PostProcessing::SlangShaderPreprocessor::Write(fmt::format_string<T...> fmt, T&&... args)
{
fmt::vformat_to(std::back_inserter(m_shader_code[m_current_stage]), fmt.get(), fmt::make_format_args(args...));
}
template<typename... T>
inline void PostProcessing::SlangShaderPreprocessor::SetError(fmt::format_string<T...> fmt, T&&... args)
{
std::string msg = fmt::format("{}:{}: {}", GetCurrentFilename(), m_current_line_number,
fmt::vformat(fmt.get(), fmt::make_format_args(args...)));
ERROR_LOG(msg);
Error::SetString(m_error, std::move(msg));
}
inline std::string PostProcessing::SlangShaderPreprocessor::GetVertexShader() const
{
return fmt::format("{}\n{}", m_shader_code[SlangShaderStage::Common], m_shader_code[SlangShaderStage::Vertex]);
}
inline std::string PostProcessing::SlangShaderPreprocessor::GetFragmentShader() const
{
return fmt::format("{}\n{}", m_shader_code[SlangShaderStage::Common], m_shader_code[SlangShaderStage::Fragment]);
}
inline bool PostProcessing::SlangShaderPreprocessor::ParseFile(std::string_view base_path, std::string_view path)
{
const std::optional<std::pair<std::string, std::string>> opt_file_contents = ReadShaderFile(base_path, path, m_error);
if (!opt_file_contents.has_value())
return false;
std::string_view previous_path = m_path;
std::string_view previous_contents = m_contents;
size_t previous_offset = m_current_offset;
size_t previous_line_offset = m_current_line_offset;
u32 previous_line_number = m_current_line_number;
auto previous_defined_stages = m_defined_stages;
m_path = opt_file_contents->first;
m_contents = opt_file_contents->second;
m_current_offset = 0;
m_current_line_offset = 0;
m_current_line_number = 0;
m_needs_line_reset = true;
std::string_view line;
while (GetLine(&line))
{
const std::string_view clean_line = StripCommentsAndWhitespace(line);
// Is this a preprocessor directive?
if (clean_line.size() > 0 && clean_line[0] == '#')
{
const std::string_view line_without_hash = StringUtil::StripWhitespace(clean_line.substr(1));
if (line_without_hash.starts_with("include "))
{
if (!HandleIncludeDirective(line_without_hash))
return false;
// Don't forward line
continue;
}
else if (line_without_hash.starts_with("pragma "))
{
if (!HandlePragmaDirective(line_without_hash))
return false;
else
continue;
}
}
if (m_needs_version_directive)
{
// skip empty lines before version
if (clean_line.empty())
continue;
const std::string_view line_without_hash = StringUtil::StripWhitespace(clean_line.substr(1));
if (clean_line[0] != '#' || !line_without_hash.starts_with("version "))
{
SetError("First line of file must be a #version directive.");
return false;
}
// forward through version directive without #line
m_needs_version_directive = false;
}
else if (m_needs_line_reset)
{
// For debugging purposes, we want to keep track of line numbers and filenames
m_needs_line_reset = false;
Write("#line {} \"{}\"\n", m_current_line_number, GetCurrentFilename());
}
// Forward text through to shader code
m_shader_code[m_current_stage] += line;
m_shader_code[m_current_stage] += '\n';
}
m_defined_stages = previous_defined_stages;
m_current_line_number = previous_line_number;
m_current_line_offset = previous_line_offset;
m_current_offset = previous_offset;
m_contents = previous_contents;
m_path = previous_path;
return true;
}
inline bool PostProcessing::SlangShaderPreprocessor::HandleIncludeDirective(std::string_view line)
{
if (m_include_depth >= MAX_SLANG_INCLUDE_DEPTH)
{
SetError("Maximum include depth exceeded");
return false;
}
const std::string_view operand = StringUtil::StripWhitespace(line.substr(7));
const char quote_char = operand.empty() ? 0 : operand[0];
if (operand.size() < 2 || operand.front() != quote_char || operand.back() != quote_char ||
operand.find(quote_char, 1) != (operand.size() - 1))
{
SetError("Malformed #include directive");
return false;
}
m_include_depth++;
const bool result = ParseFile(m_path, operand.substr(1, operand.size() - 2));
m_include_depth--;
m_needs_line_reset = true;
return result;
}
inline bool PostProcessing::SlangShaderPreprocessor::HandlePragmaDirective(std::string_view line)
{
llvm::SmallVector<std::string_view, 8> tokens;
// Need to parse manually because strings can be quoted.
std::string_view::size_type current_pos = 0;
while (current_pos < line.size())
{
// Skip whitespace
while (current_pos < line.size() && StringUtil::IsWhitespace(line[current_pos]))
current_pos++;
if (current_pos >= line.size())
break;
std::string_view::size_type start_pos = current_pos;
if (line[current_pos] == '"')
{
// Quoted string
current_pos++;
start_pos = current_pos;
while (current_pos < line.size() && line[current_pos] != '"')
current_pos++;
tokens.push_back(line.substr(start_pos, current_pos - start_pos));
if (current_pos < line.size())
current_pos++;
}
else
{
// Unquoted token
while (current_pos < line.size() && !StringUtil::IsWhitespace(line[current_pos]))
current_pos++;
tokens.push_back(line.substr(start_pos, current_pos - start_pos));
}
}
if (tokens.size() < 2)
{
SetError("Malformed #pragma directive");
return false;
}
const std::span<const std::string_view> operands = std::span<const std::string_view>(tokens).subspan(2);
if (tokens[1] == "stage")
{
return HandlePragmaStage(operands);
}
else if (tokens[1] == "name")
{
return HandlePragmaName(operands);
}
else if (tokens[1] == "parameter")
{
return HandlePragmaParameter(operands);
}
else if (tokens[1] == "format")
{
return HandlePragmaFormat(operands);
}
else
{
SetError("Unknown #pragma directive '{}'", tokens[1]);
return false;
}
}
inline bool PostProcessing::SlangShaderPreprocessor::HandlePragmaStage(std::span<const std::string_view> tokens)
{
if (tokens.size() != 1)
{
SetError("Malformed #pragma stage directive");
return false;
}
if (tokens[0] == "vertex")
{
m_current_stage = SlangShaderStage::Vertex;
}
else if (tokens[0] == "fragment")
{
m_current_stage = SlangShaderStage::Fragment;
}
else
{
SetError("Unknown shader stage '{}'", tokens[0]);
return false;
}
m_needs_line_reset = true;
return true;
}
inline bool PostProcessing::SlangShaderPreprocessor::HandlePragmaName(std::span<const std::string_view> tokens)
{
if (!m_shader_name.empty())
{
SetError("Shader name already set");
return false;
}
else if (tokens.size() != 1 || tokens[0].empty())
{
SetError("Malformed #pragma name directive");
return false;
}
m_shader_name = tokens[0];
return true;
}
inline bool PostProcessing::SlangShaderPreprocessor::HandlePragmaParameter(std::span<const std::string_view> tokens)
{
// #pragma parameter IDENTIFIER "DESCRIPTION" INITIAL MINIMUM MAXIMUM [STEP]
if (tokens.size() < 5 || tokens.size() > 6)
{
SetError("Malformed #pragma parameter directive");
return false;
}
else if (std::ranges::any_of(m_options, [&tokens](const ShaderOption& opt) { return opt.name == tokens[0]; }))
{
WARNING_LOG("Duplicate shader option name '{}'", tokens[0]);
return true;
}
ShaderOption option = {};
option.type = ShaderOption::Type::Float;
option.vector_size = 1;
option.name = tokens[0];
option.ui_name = tokens[1];
option.default_value = ShaderOption::MakeFloatVector(StringUtil::FromChars<float>(tokens[2]).value_or(0.0f));
option.min_value = ShaderOption::MakeFloatVector(StringUtil::FromChars<float>(tokens[3]).value_or(0.0f));
option.max_value = ShaderOption::MakeFloatVector(StringUtil::FromChars<float>(tokens[4]).value_or(1.0f));
option.step_value = ShaderOption::MakeFloatVector(
StringUtil::FromChars<float>((tokens.size() > 5) ? tokens[5] : std::string_view()).value_or(DEFAULT_OPTION_STEP));
option.value = option.default_value;
// If it's a float with 0/1 range and a step of 1, it's probably a bool
if (option.min_value[0].float_value == 0.0f && option.max_value[0].float_value == 1.0f &&
option.step_value[0].float_value == 1.0f)
{
option.type = ShaderOption::Type::Bool;
option.default_value[0].int_value = (option.default_value[0].float_value != 0.0f) ? 1 : 0;
option.min_value[0].int_value = 0;
option.max_value[0].int_value = 1;
option.step_value[0].int_value = 1;
option.value[0].int_value = option.default_value[0].int_value;
}
DEV_LOG("Adding shader option {} (default: {}, min: {}, max: {}, step: {})", option.name,
option.default_value[0].float_value, option.min_value[0].float_value, option.max_value[0].float_value,
option.step_value[0].float_value);
m_options.push_back(std::move(option));
return true;
}
bool PostProcessing::SlangShaderPreprocessor::HandlePragmaFormat(std::span<const std::string_view> tokens)
{
if (tokens.size() != 1 || tokens[0].empty())
{
SetError("Malformed #pragma format directive");
return false;
}
static constexpr const std::pair<std::string_view, GPUTextureFormat> format_map[] = {
//{"A2B10G10R10_UINT_PACK32", GPUTexture::Format::RGB10A2UI},
{"A2B10G10R10_UNORM_PACK32", GPUTextureFormat::RGB10A2},
{"R16G16B16A16_SFLOAT", GPUTextureFormat::RGBA16F},
//{"R16G16B16A16_SINT", GPUTexture::Format::RGBA16I},
//{"R16G16B16A16_UINT", GPUTexture::Format::RGBA16U},
{"R16G16_SFLOAT", GPUTextureFormat::RG16F},
//{"R16G16_SINT", GPUTexture::Format::RG16I},
//{"R16G16_UINT", GPUTexture::Format::RG16U},
{"R16_SFLOAT", GPUTextureFormat::R16F},
//{"R16_SINT", GPUTexture::Format::R16I},
//{"R16_UINT", GPUTexture::Format::R16U},
{"R32G32B32A32_SFLOAT", GPUTextureFormat::RGBA32F},
//{"R32G32B32A32_SINT", GPUTexture::Format::RGBA32I},
//{"R32G32B32A32_UINT", GPUTexture::Format::RGBA32U},
{"R32G32_SFLOAT", GPUTextureFormat::RG32F},
//{"R32G32_SINT", GPUTexture::Format::RG32I},
//{"R32G32_UINT", GPUTexture::Format::RG32U},
{"R32_SFLOAT", GPUTextureFormat::R32F},
{"R32_SINT", GPUTextureFormat::R32I},
{"R32_UINT", GPUTextureFormat::R32U},
//{"R8G8B8A8_SINT", GPUTexture::Format::RGBA8I},
{"R8G8B8A8_SRGB", GPUTextureFormat::SRGBA8},
//{"R8G8B8A8_UINT", GPUTexture::Format::RGBA8U},
{"R8G8B8A8_UNORM", GPUTextureFormat::RGBA8},
//{"R8G8_SINT", GPUTexture::Format::RG8I},
//{"R8G8_UINT", GPUTexture::Format::RG8U},
{"R8G8_UNORM", GPUTextureFormat::RG8},
//{"R8_SINT", GPUTexture::Format::R8I},
//{"R8_UINT", GPUTexture::Format::R8U},
{"R8_UNORM", GPUTextureFormat::R8},
};
static_assert(
[]() {
for (size_t i = 1; i < std::size(format_map); i++)
{
if (format_map[i - 1].first >= format_map[i].first)
return false;
}
return true;
}(),
"format_map is sorted");
const std::string_view format_name = tokens[0];
const auto iter = std::lower_bound(std::begin(format_map), std::end(format_map), format_name,
[](const auto& it, const std::string_view& value) { return (it.first < value); });
if (iter == std::end(format_map) || iter->first != format_name)
{
SetError("Unknown texture format '{}'", format_name);
return false;
}
m_output_format = iter->second;
return true;
}
inline bool PostProcessing::SlangShaderPreprocessor::GetLine(std::string_view* line)
{
const size_t length = m_contents.length();
if (m_current_offset == length)
{
m_current_line_offset = m_current_offset;
return false;
}
size_t end_position = m_current_offset;
for (; end_position < length; end_position++)
{
// ignore carriage returns
if (m_contents[end_position] == '\r')
continue;
if (m_contents[end_position] == '\n')
break;
}
m_current_line_number++;
m_current_line_offset = m_current_offset;
*line = m_contents.substr(m_current_offset, end_position - m_current_offset);
m_current_offset = std::min(end_position + 1, length);
return true;
}
inline std::string_view PostProcessing::SlangShaderPreprocessor::GetCurrentFilename() const
{
return Path::GetFileName(m_path);
}
PostProcessing::SlangShader::SlangShader()
{
m_wants_unscaled_input = true;
}
PostProcessing::SlangShader::~SlangShader()
{
for (Texture& tex : m_textures)
{
g_gpu_device->RecycleTexture(std::move(tex.texture));
g_gpu_device->RecycleTexture(std::move(tex.feedback_texture));
}
}
bool PostProcessing::SlangShader::LoadFromFile(std::string name, const char* path, Error* error)
{
std::optional<std::string> code = FileSystem::ReadFileToString(path, error);
if (!code.has_value() || code->empty())
return false;
return LoadFromString(std::move(name), path, code.value(), error);
}
bool PostProcessing::SlangShader::LoadFromString(std::string name, std::string_view path, std::string_view code,
Error* error)
{
m_name = std::move(name);
m_options.clear();
return ParsePresetFile(path, code, error);
}
bool PostProcessing::SlangShader::ParsePresetFile(std::string_view path, std::string_view code, Error* error)
{
SlangPresetParser pp;
if (!pp.Parse(path, code, 0, error))
return false;
const u32 num_shaders = pp.GetUIntValue("shaders", 0);
if (num_shaders == 0)
{
Error::SetStringView(error, "No shaders defined in preset");
return false;
}
// Textures must be parsed first, since the passes may reference them.
if (pp.ContainsValue("textures"))
{
if (!ParsePresetTextures(path, pp, error))
return false;
}
// Parse passes in order.
for (u32 i = 0; i < num_shaders; i++)
{
if (!ParsePresetPass(path, pp, i, (i == (num_shaders - 1)), error))
return false;
}
// We defer reflection until pipeline compilation, so we don't need to do anything else here.
return true;
}
bool PostProcessing::SlangShader::ParseScaleType(ScaleType* dst, std::string_view value, Error* error)
{
if (StringUtil::EqualNoCase(value, "source"))
{
*dst = ScaleType::Source;
return true;
}
if (StringUtil::EqualNoCase(value, "viewport"))
{
*dst = ScaleType::Viewport;
return true;
}
if (StringUtil::EqualNoCase(value, "absolute"))
{
*dst = ScaleType::Absolute;
return true;
}
if (StringUtil::EqualNoCase(value, "original"))
{
*dst = ScaleType::Original;
return true;
}
Error::SetStringFmt(error, "Invalid scale type '{}'", value);
return false;
}
GPUSampler::AddressMode PostProcessing::SlangShader::ParseWrapMode(std::string_view value)
{
if (value == "repeat")
return GPUSampler::AddressMode::Repeat;
if (value == "clamp_to_edge")
return GPUSampler::AddressMode::ClampToEdge;
if (value == "clamp_to_border")
return GPUSampler::AddressMode::ClampToBorder;
if (value == "mirrored_repeat")
return GPUSampler::AddressMode::MirrorRepeat;
WARNING_LOG("Invalid wrap mode '{}', defaulting to border", value);
return GPUSampler::AddressMode::ClampToBorder;
}
bool PostProcessing::SlangShader::ParsePresetTextures(std::string_view preset_path, const SlangPresetParser& parser,
Error* error)
{
const std::string_view textures_str = parser.GetStringValue("textures", {});
const std::vector<std::string_view> texture_names = StringUtil::SplitString(textures_str, ';');
for (const std::string_view orig_name : texture_names)
{
const std::string_view name = StringUtil::StripWhitespace(orig_name);
if (name.empty())
continue;
if (std::ranges::any_of(m_textures, [&name](const Texture& t) { return t.name == name; }))
{
Error::SetStringFmt(error, "Duplicate texture name '{}'", name);
return false;
}
const std::string_view filename = parser.GetStringValue(name, {});
if (filename.empty())
{
Error::SetStringFmt(error, "Texture '{}' has no path defined", name);
return false;
}
std::string path =
Path::IsAbsolute(filename) ? std::string(filename) : Path::BuildRelativePath(preset_path, filename);
Path::ToNativePath(&path);
if (!FileSystem::FileExists(path.c_str()))
{
Error::SetStringFmt(error, "Texture file '{}' does not exist", filename);
return false;
}
const bool linear_filter = parser.GetBoolValue(TinyString::from_format("{}_linear", name), true);
DEV_LOG("Adding LUT texture '{}' from file '{}' (linear filter: {})", name, path, linear_filter);
Texture& tex = m_textures.emplace_back();
tex.name = name;
tex.path = std::move(path);
tex.linear_filter = linear_filter;
}
return true;
}
bool PostProcessing::SlangShader::ParsePresetPass(std::string_view preset_path, const SlangPresetParser& parser,
u32 idx, bool is_final_pass, Error* error)
{
const std::string_view shader_path = parser.GetIndexedStringValue("shader", idx, {});
if (shader_path.empty())
{
Error::SetStringFmt(error, "Shader {} has no path defined", idx);
return false;
}
SlangShaderPreprocessor preprocessor(error);
if (!preprocessor.ParseFile(preset_path, shader_path))
return false;
Pass pass = {};
pass.name = Path::GetFileTitle(shader_path);
pass.vertex_shader_code = preprocessor.GetVertexShader();
pass.fragment_shader_code = preprocessor.GetFragmentShader();
// TODO: texture mipmap
// FRAME COUNT MOD
pass.frame_count_mod = parser.GetIndexedUIntValue("frame_count_mod", idx, 0);
// MIPMAP INPUT - applies to previous pass
if (parser.GetIndexedBoolValue("mipmap_input", idx, false))
{
// Just in case there are some bad files out there...
if (idx > 0)
{
Assert(m_passes[idx - 1].output_texture_id >= 0);
Texture& prev_rt = m_textures[m_passes[idx - 1].output_texture_id];
if (!prev_rt.generate_mipmaps)
{
DEV_LOG("Enabling mipmap input for pass [{}]{} ({})", idx - 1, m_passes[idx - 1].name, prev_rt.name);
prev_rt.generate_mipmaps = true;
}
}
else
{
ERROR_LOG("Pass [{}]{} cannot use mipmap_input since it has no preceding pass", idx, pass.name);
}
}
// OUTPUT FORMAT
pass.output_format = preprocessor.GetOutputFormat();
if (!preprocessor.HasOutputFormat())
{
// srgb > float apparently takes precedence
if (parser.GetIndexedBoolValue("srgb_framebuffer", idx, false))
pass.output_format = GPUTextureFormat::SRGBA8;
else if (parser.GetIndexedBoolValue("float_framebuffer", idx, false))
pass.output_format = GPUTextureFormat::RGBA16F;
}
// OUTPUT SCALE
// Defaults to source scale.
pass.output_scale[0] = {ScaleType::Source, 1.0f};
pass.output_scale[1] = {ScaleType::Source, 1.0f};
for (u32 i = 0; i < 2; i++)
{
const std::string_view type_key = i ? "scale_type_y" : "scale_type_x";
const std::string_view value_key = i ? "scale_y" : "scale_x";
if (!parser.ContainsIndexedValue(type_key, idx))
continue;
if (!ParseScaleType(&pass.output_scale[i].first, parser.GetIndexedStringValue(type_key, idx, {}), error))
return false;
pass.output_scale[i].second = parser.GetIndexedFloatValue(value_key, idx, 1.0f);
}
// non x/y takes precendence
if (parser.ContainsIndexedValue("scale_type", idx))
{
if (!ParseScaleType(&pass.output_scale[0].first, parser.GetIndexedStringValue("scale_type", idx, {}), error))
return false;
pass.output_scale[0].second = parser.GetIndexedFloatValue("scale", idx, 1.0f);
pass.output_scale[1] = pass.output_scale[0];
}
// SAMPLER
const bool linear_sampler = parser.GetIndexedBoolValue("filter_linear", idx, true);
pass.output_sampler_config = linear_sampler ? GPUSampler::GetLinearConfig() : GPUSampler::GetNearestConfig();
// Default mode appears to be clamp-to-border?
const GPUSampler::AddressMode mode = ParseWrapMode(parser.GetIndexedStringValue("wrap_mode", idx, "clamp_to_border"));
pass.output_sampler_config.address_u = mode;
pass.output_sampler_config.address_v = mode;
pass.output_sampler_config.address_w = mode;
// FRAMEBUFFER
// Use the alias as the framebuffer name if specified, otherwise generate one.
const std::string fb_name =
preprocessor.HasShaderName() ? preprocessor.TakeShaderName() : fmt::format("__ds__pass_{}__", idx);
if (std::ranges::any_of(m_textures, [&fb_name](const Texture& t) { return (t.name == fb_name); }))
{
Error::SetStringFmt(error, "Duplicate texture name '{}' when allocating pass framebuffer", fb_name);
return false;
}
// Can bypass the framebuffer when the scale is not defined in the last pass, but allocate a texture anyway in case.
pass.output_texture_id = static_cast<s32>(m_textures.size());
Texture& tex = m_textures.emplace_back();
tex.name = std::move(fb_name);
tex.linear_filter = linear_sampler;
DEV_LOG("Allocating output framebuffer for pass {}: '{}' ({})", idx, tex.name, pass.output_texture_id);
// ALIASES
if (const std::string_view alias = parser.GetIndexedStringValue("alias", idx, {}); !alias.empty())
{
if (std::ranges::any_of(m_aliases, [&alias](const auto& it) { return (it.first == alias); }))
{
Error::SetStringFmt(error, "Duplicate alias '{}' in pass {}", alias, idx);
return false;
}
DEV_LOG("Alias {} to [{}]{}", alias, idx, pass.name);
m_aliases.emplace_back(alias, idx);
}
// OPTIONS
if (preprocessor.HasOptions())
{
if (m_options.empty())
{
m_options = preprocessor.TakeOptions();
}
else
{
// Need to merge options.
for (ShaderOption& option : preprocessor.TakeOptions())
{
if (std::ranges::any_of(m_options, [&option](const ShaderOption& o) { return (o.name == option.name); }))
{
// Already defined.
continue;
}
m_options.push_back(std::move(option));
}
}
// Extract any options from the preset if overridden.
for (ShaderOption& option : m_options)
{
if (!parser.ContainsValue(option.name))
continue;
if (option.type == ShaderOption::Type::Bool)
{
option.default_value[0].int_value = option.value[0].int_value =
parser.GetBoolValue(option.name, option.default_value[0].int_value);
}
else
{
option.default_value[0].float_value = option.value[0].float_value =
parser.GetFloatValue(option.name, option.default_value[0].float_value);
}
}
}
m_passes.push_back(std::move(pass));
return true;
}
static std::optional<DynamicHeapArray<u32>> CompileToSPV(shaderc_shader_kind stage, std::string_view code, Error* error)
{
std::optional<DynamicHeapArray<u32>> ret;
if (!g_dyn_shaderc.Open(error))
return ret;
const bool generate_debug_info = (g_gpu_device && g_gpu_device->IsDebugDevice());
const shaderc_compile_options_t options = g_dyn_shaderc.shaderc_compile_options_initialize();
AssertMsg(options, "shaderc_compile_options_initialize() failed");
g_dyn_shaderc.shaderc_compile_options_set_source_language(options, shaderc_source_language_glsl);
g_dyn_shaderc.shaderc_compile_options_set_target_env(options, shaderc_target_env_vulkan, 0);
g_dyn_shaderc.shaderc_compile_options_set_generate_debug_info(options, generate_debug_info, false);
g_dyn_shaderc.shaderc_compile_options_set_optimization_level(options, shaderc_optimization_level_zero);
const shaderc_compilation_result_t result = g_dyn_shaderc.shaderc_compile_into_spv(
g_dyn_shaderc.compiler, code.data(), code.length(), stage, "source", "main", options);
const shaderc_compilation_status status =
result ? g_dyn_shaderc.shaderc_result_get_compilation_status(result) : shaderc_compilation_status_internal_error;
if (status != shaderc_compilation_status_success)
{
const std::string_view errors(result ? g_dyn_shaderc.shaderc_result_get_error_message(result) :
"null result object");
Error::SetStringFmt(error, "Failed to compile shader to SPIR-V: {}\n{}",
g_dyn_shaderc.shaderc_compilation_status_to_string(status), errors);
ERROR_LOG("Failed to compile shader to SPIR-V: {}\n{}", g_dyn_shaderc.shaderc_compilation_status_to_string(status),
errors);
GPUDevice::DumpBadShader(code, errors);
}
else
{
const size_t num_warnings = g_dyn_shaderc.shaderc_result_get_num_warnings(result);
if (num_warnings > 0)
WARNING_LOG("Shader compiled with warnings:\n{}", g_dyn_shaderc.shaderc_result_get_error_message(result));
const size_t spirv_size = g_dyn_shaderc.shaderc_result_get_length(result);
Assert(spirv_size > 0 && (spirv_size % sizeof(u32)) == 0);
ret.emplace();
ret->resize(spirv_size / sizeof(u32));
std::memcpy(ret->data(), g_dyn_shaderc.shaderc_result_get_bytes(result), spirv_size);
}
g_dyn_shaderc.shaderc_result_release(result);
g_dyn_shaderc.shaderc_compile_options_release(options);
return ret;
}
bool PostProcessing::SlangShader::ReflectPass(Pass& pass, Error* error)
{
std::optional<DynamicHeapArray<u32>> vs_spv = CompileToSPV(shaderc_vertex_shader, pass.vertex_shader_code, error);
std::optional<DynamicHeapArray<u32>> fs_spv = CompileToSPV(shaderc_fragment_shader, pass.fragment_shader_code, error);
if (!vs_spv.has_value() || !fs_spv.has_value())
return false;
pass.vertex_shader_spv = std::move(vs_spv.value());
pass.fragment_shader_spv = std::move(fs_spv.value());
if (!ReflectShader(pass, pass.vertex_shader_spv, GPUShaderStage::Vertex, error) ||
!ReflectShader(pass, pass.fragment_shader_spv, GPUShaderStage::Fragment, error))
{
return false;
}
pass.is_reflected = true;
return true;
}
bool PostProcessing::SlangShader::ReflectShader(Pass& pass, std::span<u32> spv, GPUShaderStage stage, Error* error)
{
if (!g_dyn_spirv_cross.Open(error))
return false;
spvc_context sctx;
spvc_result sres;
if ((sres = g_dyn_spirv_cross.spvc_context_create(&sctx)) != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_context_create() failed: {}", static_cast<int>(sres));
return false;
}
const ScopedGuard sctx_guard = [&sctx]() { g_dyn_spirv_cross.spvc_context_destroy(sctx); };
g_dyn_spirv_cross.spvc_context_set_error_callback(
sctx,
[](void* error, const char* errormsg) {
ERROR_LOG("SPIRV-Cross reported an error: {}", errormsg);
Error::SetStringView(static_cast<Error*>(error), errormsg);
},
error);
spvc_parsed_ir sir;
if ((sres = g_dyn_spirv_cross.spvc_context_parse_spirv(sctx, reinterpret_cast<const u32*>(spv.data()), spv.size(),
&sir)) != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_context_parse_spirv() failed: {}", static_cast<int>(sres));
return false;
}
spvc_compiler scompiler;
if ((sres = g_dyn_spirv_cross.spvc_context_create_compiler(sctx, SPVC_BACKEND_NONE /*SPVC_BACKEND_GLSL*/, sir,
SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &scompiler)) !=
SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_context_create_compiler() failed: {}", static_cast<int>(sres));
return false;
}
spvc_compiler_options soptions;
if ((sres = g_dyn_spirv_cross.spvc_compiler_create_compiler_options(scompiler, &soptions)) != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_compiler_create_compiler_options() failed: {}", static_cast<int>(sres));
return false;
}
spvc_resources resources;
if ((sres = g_dyn_spirv_cross.spvc_compiler_create_shader_resources(scompiler, &resources)) != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_compiler_create_shader_resources() failed: {}", static_cast<int>(sres));
return false;
}
// Need to know if there's UBOs for mapping.
const spvc_reflected_resource *ubos, *push_constants, *textures;
size_t ubos_count, push_constants_count, textures_count;
if ((sres = g_dyn_spirv_cross.spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_UNIFORM_BUFFER,
&ubos, &ubos_count)) != SPVC_SUCCESS ||
(sres = g_dyn_spirv_cross.spvc_resources_get_resource_list_for_type(
resources, SPVC_RESOURCE_TYPE_PUSH_CONSTANT, &push_constants, &push_constants_count)) != SPVC_SUCCESS ||
(sres = g_dyn_spirv_cross.spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_SAMPLED_IMAGE,
&textures, &textures_count)) != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_resources_get_resource_list_for_type() failed: {}", static_cast<int>(sres));
return false;
}
// Try to only allocate active textures.
std::optional<std::span<const spvc_reflected_resource>> active_textures;
if (spvc_set active_interface_variables; (sres = g_dyn_spirv_cross.spvc_compiler_get_active_interface_variables(
scompiler, &active_interface_variables)) == SPVC_SUCCESS)
{
if (spvc_resources active_resources;
(sres = g_dyn_spirv_cross.spvc_compiler_create_shader_resources_for_active_variables(
scompiler, &active_resources, active_interface_variables)) == SPVC_SUCCESS)
{
const spvc_reflected_resource* active_textures_begin;
size_t active_textures_count;
if ((sres = g_dyn_spirv_cross.spvc_resources_get_resource_list_for_type(
active_resources, SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, &active_textures_begin, &active_textures_count)) ==
SPVC_SUCCESS)
{
active_textures = std::span<const spvc_reflected_resource>(active_textures_begin, active_textures_count);
}
else
{
WARNING_LOG("spvc_resources_get_resource_list_for_type() (active) failed: {}", static_cast<unsigned>(sres));
}
}
else
{
WARNING_LOG("spvc_compiler_create_shader_resources_for_active_variables() failed: {}",
static_cast<unsigned>(sres));
}
}
else
{
WARNING_LOG("spvc_compiler_get_active_interface_variables() failed: {}", static_cast<unsigned>(sres));
}
// Should only have one uniform buffer/push constant.
if (ubos_count > 1)
{
Error::SetStringFmt(error, "{} uniform buffers found, only zero or one is supported.", ubos_count);
return false;
}
else if (ubos_count > 0)
{
if (!ReflectPassUniforms(scompiler, ubos[0], pass, false, error))
return false;
}
if (push_constants_count > 1)
{
Error::SetStringFmt(error, "{} push constant blocks found, only zero or one is supported.", push_constants_count);
return false;
}
else if (push_constants_count > 0)
{
if (!ReflectPassUniforms(scompiler, push_constants[0], pass, true, error))
return false;
}
// TEXTURES
std::optional<SPIRVModule> mutable_spv;
u32 num_bound_textures = 0;
u32 num_unused_textures = 0;
for (const spvc_reflected_resource& tex : std::span<const spvc_reflected_resource>(textures, textures_count))
{
if (stage != GPUShaderStage::Fragment)
{
Error::SetStringFmt(error, "Textures can only be used in fragment shaders, found in stage {}",
GPUShader::GetStageName(stage));
return false;
}
const unsigned orig_descriptor_set =
g_dyn_spirv_cross.spvc_compiler_get_decoration(scompiler, tex.id, SpvDecorationDescriptorSet);
const unsigned orig_binding =
g_dyn_spirv_cross.spvc_compiler_get_decoration(scompiler, tex.id, SpvDecorationBinding);
if (orig_descriptor_set != 0)
{
Error::SetStringFmt(error, "Texture '{}' is in descriptor set {}, only set 0 is supported", tex.name,
orig_descriptor_set);
return false;
}
if (orig_binding < 1)
{
Error::SetStringFmt(error, "Texture '{}' has invalid binding {}, must be 1 or higher", tex.name, orig_binding);
return false;
}
// Need to modify SPIR-V to remap bindings.
if (!mutable_spv.has_value() && !(mutable_spv = SPIRVModule::Get(spv, error)).has_value())
return false;
// Is the texture used? If not, we remap it out of range so it won't alias/conflict.
const bool is_active = (!active_textures.has_value() ||
std::ranges::any_of(active_textures.value(), [&tex](const spvc_reflected_resource& at) {
return (at.id == tex.id);
}));
u32 binding;
if (!is_active)
{
binding = GPUDevice::MAX_TEXTURE_SAMPLERS + num_unused_textures++;
}
else
{
binding = num_bound_textures++;
if (binding >= GPUDevice::MAX_TEXTURE_SAMPLERS)
{
Error::SetStringFmt(error, "Texture '{}' has binding {} (original {}), a maximum of {} textures are supported.",
tex.name, binding, orig_binding, GPUDevice::MAX_TEXTURE_SAMPLERS);
return false;
}
}
// Remap to descriptor set #1.
if (!mutable_spv->SetDecoration(tex.id, SpvDecorationDescriptorSet, 1, error) ||
!mutable_spv->SetDecoration(tex.id, SpvDecorationBinding, binding, error))
{
Error::AddPrefixFmt(error, "Failed to remap texture '{}' to descriptor set 1, binding {}: ", tex.name, binding);
return false;
}
// Skip mapping if not used.
if (!is_active)
{
DEV_COLOR_LOG(StrongYellow, "Texture {} (orig {}, unused)", tex.name, orig_binding);
continue;
}
// Map the texture.
const std::optional<TextureID> tex_id = FindTextureByName(tex.name, error);
if (!tex_id.has_value())
return false;
if (const auto iter =
std::ranges::find_if(pass.samplers, [&binding](const auto& it) { return (it.second == binding); });
iter != pass.samplers.end())
{
if (iter->first != tex_id.value())
{
Error::SetStringFmt(error, "Binding {} is used for multiple textures ({} and {})", binding, tex_id.value(),
tex.name);
return false;
}
// don't duplicate it
DEV_LOG("Texture {} @ {} (orig {}, already present)", GetTextureNameForID(tex_id.value()), binding, orig_binding);
continue;
}
DEV_LOG("Texture {} @ {} (orig {})", GetTextureNameForID(tex_id.value()), binding, orig_binding);
pass.samplers.emplace_back(tex_id.value(), binding);
}
return true;
}
bool PostProcessing::SlangShader::ReflectPassUniforms(const spvc_compiler& scompiler,
const spvc_reflected_resource& resource, Pass& pass,
bool push_constant, Error* error)
{
const spvc_type type_handle = g_dyn_spirv_cross.spvc_compiler_get_type_handle(scompiler, resource.base_type_id);
if (!type_handle)
{
Error::SetStringFmt(error, "spvc_compiler_get_type_handle() failed for resource '{}'", resource.name);
return false;
}
size_t struct_size = 0;
if (const spvc_result sres =
g_dyn_spirv_cross.spvc_compiler_get_declared_struct_size(scompiler, type_handle, &struct_size);
sres != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_compiler_get_declared_struct_size() failed for resource '{}': {}", resource.name,
static_cast<int>(sres));
return false;
}
if (push_constant)
{
pass.push_constants_size =
static_cast<u32>(Common::AlignUpPow2(struct_size, GPUDevice::BASE_UNIFORM_BUFFER_ALIGNMENT));
DEV_LOG("Pass '{}' has push constants '{}' of size {} ({} aligned) bytes", pass.name, resource.name, struct_size,
pass.push_constants_size);
if (pass.push_constants_size > MAX_PUSH_CONSTANT_SIZE)
{
Error::SetStringFmt(error, "Push constant block '{}' is too large: {} bytes (max is {})", resource.name,
pass.push_constants_size, MAX_PUSH_CONSTANT_SIZE);
return false;
}
}
else
{
pass.uniforms_size = static_cast<u32>(Common::AlignUpPow2(struct_size, GPUDevice::BASE_UNIFORM_BUFFER_ALIGNMENT));
DEV_LOG("Pass '{}' has uniform buffer '{}' of size {} ({} aligned) bytes", pass.name, resource.name, struct_size,
pass.uniforms_size);
}
for (unsigned member_idx = 0;; member_idx++)
{
const char* member_name =
g_dyn_spirv_cross.spvc_compiler_get_member_name(scompiler, resource.base_type_id, member_idx);
if (!member_name || member_name[0] == '\0')
break;
const u32 offset = g_dyn_spirv_cross.spvc_compiler_get_member_decoration(scompiler, resource.base_type_id,
member_idx, SpvDecorationOffset);
size_t member_size = 0;
if (const spvc_result sres = g_dyn_spirv_cross.spvc_compiler_get_declared_struct_member_size(
scompiler, type_handle, member_idx, &member_size);
sres != SPVC_SUCCESS)
{
Error::SetStringFmt(error, "spvc_compiler_get_declared_struct_member_size() failed for member '{}' of '{}': {}",
member_name, resource.name, static_cast<int>(sres));
return false;
}
else if (member_size > std::numeric_limits<u16>::max())
{
// Shouldn't ever happen.
ERROR_LOG("Member '{}' exceeded size limit: {} bytes", member_name, member_size);
member_size = std::numeric_limits<u16>::max();
}
const std::optional<Uniform> uni_info =
GetUniformInfo(member_name, push_constant, offset, static_cast<u16>(member_size), error);
if (!uni_info.has_value())
return false;
// De-duplicate and check that the same offset isn't being used differently in VS vs FS.
const auto iter = std::ranges::find_if(pass.uniforms, [&uni_info](const Uniform& u) {
return (u.push_constant == uni_info->push_constant && u.offset == uni_info->offset);
});
if (iter != pass.uniforms.end())
{
if (iter->type != uni_info->type || iter->associated_texture != uni_info->associated_texture)
{
Error::SetStringFmt(error, "Conflicting definitions for uniform '{}' at offset {}", member_name, offset);
return false;
}
// Already present, ignore.
continue;
}
// TODO: check for overlap?
pass.uniforms.push_back(uni_info.value());
}
return true;
}
bool PostProcessing::SlangShader::CompilePass(Pass& pass, Error* error)
{
const std::unique_ptr<GPUShader> vs = g_gpu_device->CreateShader(
GPUShaderStage::Vertex, GPUShaderLanguage::SPV,
std::string_view(reinterpret_cast<const char*>(pass.vertex_shader_spv.data()), pass.vertex_shader_spv.size_bytes()),
error);
if (!vs)
return false;
const std::unique_ptr<GPUShader> fs =
g_gpu_device->CreateShader(GPUShaderStage::Fragment, GPUShaderLanguage::SPV,
std::string_view(reinterpret_cast<const char*>(pass.fragment_shader_spv.data()),
pass.fragment_shader_spv.size_bytes()),
error);
if (!fs)
return false;
static constexpr std::array vertex_attributes = {
GPUPipeline::VertexAttribute::Make(0, GPUPipeline::VertexAttribute::Semantic::TexCoord, 0,
GPUPipeline::VertexAttribute::Type::Float, 4,
offsetof(SlangShaderVertex, position)),
GPUPipeline::VertexAttribute::Make(1, GPUPipeline::VertexAttribute::Semantic::TexCoord, 1,
GPUPipeline::VertexAttribute::Type::Float, 2,
offsetof(SlangShaderVertex, texcoord)),
};
GPUPipeline::GraphicsConfig plconfig;
plconfig.layout = GPUPipeline::Layout::MultiTextureAndUBOAndPushConstants;
plconfig.primitive = GPUPipeline::Primitive::TriangleStrips;
plconfig.input_layout = {vertex_attributes, static_cast<u32>(sizeof(SlangShaderVertex))};
plconfig.rasterization = GPUPipeline::RasterizationState::GetNoCullState();
plconfig.depth = GPUPipeline::DepthState::GetNoTestsState();
plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState();
plconfig.render_pass_flags = GPUPipeline::NoRenderPassFlags;
plconfig.vertex_shader = vs.get();
plconfig.fragment_shader = fs.get();
plconfig.geometry_shader = nullptr;
plconfig.SetTargetFormats(pass.output_format);
pass.pipeline = g_gpu_device->CreatePipeline(plconfig, error);
if (!pass.pipeline)
return false;
return true;
}
bool PostProcessing::SlangShader::UploadLUTTextures(Error* error)
{
for (Texture& tex : m_textures)
{
if (tex.path.empty() || tex.texture)
continue;
Assert(!tex.needs_feedback);
Image image;
if (!image.LoadFromFile(tex.path.c_str(), error))
{
Error::AddPrefixFmt(error, "Failed to load LUT texture from file '{}': ", Path::GetFileName(tex.path));
return false;
}
// Flip textures in GL so the texture coordinates match what we're rendering in screen space.
if (g_gpu_device->UsesLowerLeftOrigin())
{
// Have to decompress if compressed.
if (Image::IsCompressedFormat(image.GetFormat()))
{
std::optional<Image> decompressed = image.ConvertToRGBA8(error);
if (!decompressed.has_value())
{
Error::AddPrefixFmt(error, "Failed to decompress LUT texture from file '{}': ", Path::GetFileName(tex.path));
return false;
}
image = std::move(decompressed.value());
}
image.FlipY();
}
if (!(tex.texture = g_gpu_device->FetchAndUploadTextureImage(image, GPUTexture::Flags::None, error)))
{
Error::AddPrefixFmt(error, "Failed to upload LUT texture from file '{}': ", Path::GetFileName(tex.path));
return false;
}
// Use clamp to border for LUTs by default.
GPUSampler::Config sampler_config =
tex.linear_filter ? GPUSampler::GetLinearConfig() : GPUSampler::GetNearestConfig();
sampler_config.address_u = GPUSampler::AddressMode::ClampToBorder;
sampler_config.address_v = GPUSampler::AddressMode::ClampToBorder;
if (!(tex.sampler = g_gpu_device->GetSampler(sampler_config, error)))
return false;
}
return true;
}
std::optional<PostProcessing::SlangShader::TextureID>
PostProcessing::SlangShader::FindTextureByName(std::string_view name, Error* error)
{
const auto enable_feedback_on_texture = [this](TextureID tid, Error* error) -> std::optional<TextureID> {
if (!m_textures[tid].path.empty())
{
Error::SetStringFmt(error, "Cannot enable feedback on texture '{}' loaded from file", m_textures[tid].name);
return std::nullopt;
}
DEV_LOG("Enabling feedback on texture [{}]{}", tid, m_textures[tid].name);
m_textures[tid].needs_feedback = true;
return (tid | TEXTURE_ID_FEEDBACK);
};
// Special cases.
// Original or input.
if (name == "Original")
return TEXTURE_ID_ORIGINAL;
// Previous pass or original.
if (name == "Source")
return TEXTURE_ID_SOURCE;
// Previous pass N output.
if (name.starts_with("OriginalHistory"))
{
const std::optional<u32> index = StringUtil::FromChars<u32>(name.substr(15));
if (index.value_or(MAX_ORIGINAL_HISTORY_SIZE + 1) > MAX_ORIGINAL_HISTORY_SIZE)
{
Error::SetStringFmt(error, "Invalid OriginalHistory suffix '{}'", name);
return std::nullopt;
}
const size_t required_size = index.value() + 2; // +1 for zero index, +1 for current frame
if (required_size >= m_original_history_textures.size())
{
DEV_COLOR_LOG(StrongYellow, "Expanding original history texture array to size {}", required_size);
m_original_history_textures.resize(required_size);
}
return (TEXTURE_ID_ORIGINAL_HISTORY_START + index.value());
}
// Previous pass N output.
if (name.starts_with("PassOutput"))
{
const std::optional<u32> index = StringUtil::FromChars<u32>(name.substr(10));
if (index.value_or(static_cast<u32>(m_passes.size())) >= m_passes.size())
{
Error::SetStringFmt(error, "Invalid PassOutput texture name '{}'", name);
return std::nullopt;
}
return m_passes[index.value()].output_texture_id;
}
// Previous pass N previous output.
if (name.starts_with("PassFeedback"))
{
const std::optional<u32> index = StringUtil::FromChars<u32>(name.substr(12));
if (index.value_or(static_cast<u32>(m_passes.size())) >= m_passes.size())
{
Error::SetStringFmt(error, "Invalid PassFeedback texture name '{}'", name);
return std::nullopt;
}
return enable_feedback_on_texture(m_passes[index.value()].output_texture_id, error);
}
// User texture by name.
if (name.starts_with("User"))
{
const std::optional<u32> index = StringUtil::FromChars<u32>(name.substr(4));
if (index.value_or(static_cast<u32>(m_textures.size())) >= m_textures.size() ||
m_textures[index.value()].path.empty())
{
Error::SetStringFmt(error, "Invalid User texture index '{}'", name);
return std::nullopt;
}
return static_cast<TextureID>(index.value());
}
// Feedback of named texture.
if (name.ends_with("Feedback"))
{
const std::string_view base_name = name.substr(0, name.length() - 8);
std::optional<TextureID> base_tid = FindTextureByName(base_name, error);
if (!base_tid.has_value())
{
Error::AddSuffix(error, "for feedback");
return std::nullopt;
}
return enable_feedback_on_texture(base_tid.value(), error);
}
const auto it =
std::find_if(m_textures.begin(), m_textures.end(), [&name](const Texture& t) { return t.name == name; });
if (it != m_textures.end())
return static_cast<TextureID>(std::distance(m_textures.begin(), it));
// check aliases.
const auto alias_it = std::ranges::find_if(m_aliases, [&name](const auto& it) { return (it.first == name); });
if (alias_it != m_aliases.end())
{
Assert(alias_it->second < m_passes.size());
return m_passes[alias_it->second].output_texture_id;
}
Error::SetStringFmt(error, "Failed to resolve texture named '{}'", name);
return std::nullopt;
}
std::optional<PostProcessing::SlangShader::Uniform> PostProcessing::SlangShader::GetUniformInfo(std::string_view name,
bool push_constant,
u32 offset, u16 size,
Error* error)
{
struct BuiltinUniformEntry
{
const char* name;
s32 type;
u32 expected_size;
};
static constexpr const BuiltinUniformEntry builtins[] = {
{"MVP", BuiltinUniform::MVP, 64},
{"OutputSize", BuiltinUniform::OutputSize, 16},
{"ViewportSize", BuiltinUniform::FinalViewportSize, 16},
{"FrameCount", BuiltinUniform::FrameCount, 4},
{"FrameDirection", BuiltinUniform::FrameDirection, 4},
};
for (const BuiltinUniformEntry& entry : builtins)
{
if (name == entry.name)
{
if (size != entry.expected_size)
WARNING_LOG("Builtin uniform '{}' has incorrect size {}, expected {}", name, size, entry.expected_size);
return Uniform{entry.type, 0, push_constant, size, offset};
}
}
// Texture related.
if (name.ends_with("Size"))
{
const std::string_view tex_name = name.substr(0, name.length() - 4);
const std::optional<TextureID> tex_id = FindTextureByName(tex_name, error);
if (!tex_id.has_value())
{
// Apparently we should just fill with zero in this case.
WARNING_LOG("Texture '{}' not found for uniform '{}'", tex_name, name);
return Uniform{BuiltinUniform::Zero, 0, push_constant, size, offset};
}
return Uniform{BuiltinUniform::TextureSize, tex_id.value(), push_constant, size, offset};
}
// Check options.
for (size_t i = 0; i < m_options.size(); i++)
{
if (m_options[i].name == name)
{
if (size != 4)
WARNING_LOG("Uniform option '{}' has incorrect size {}, expected 4", name, size);
return Uniform{static_cast<s32>(i), 0, push_constant, size, offset};
}
}
WARNING_LOG("Failed to resolve uniform named '{}'", name);
return Uniform{BuiltinUniform::Zero, 0, push_constant, size, offset};
}
bool PostProcessing::SlangShader::CompilePipeline(GPUTextureFormat format, u32 width, u32 height, Error* error,
ProgressCallback* progress)
{
// skip if format hasn't changed
if (m_output_framebuffer_format == format)
return true;
for (Pass& pass : m_passes)
{
if (!pass.is_reflected && !ReflectPass(pass, error))
return false;
pass.pipeline.reset();
if (!CompilePass(pass, error))
return false;
}
// don't compile a blit pipeline if it's not needed
m_output_blit_pipeline.reset();
m_output_blit_pipeline = CreateBlitPipeline(format, error);
if (!m_output_blit_pipeline)
return false;
if (!UploadLUTTextures(error))
return false;
m_output_framebuffer_format = format;
return true;
}
std::unique_ptr<GPUPipeline> PostProcessing::SlangShader::CreateBlitPipeline(GPUTextureFormat format, Error* error)
{
const RenderAPI rapi = g_gpu_device->GetRenderAPI();
const ShaderGen shadergen(rapi, ShaderGen::GetShaderLanguageForAPI(rapi), false, false);
const std::unique_ptr<GPUShader> vs = g_gpu_device->CreateShader(GPUShaderStage::Vertex, shadergen.GetLanguage(),
shadergen.GenerateScreenQuadVertexShader(), error);
if (!vs)
return {};
const std::unique_ptr<GPUShader> fs = g_gpu_device->CreateShader(GPUShaderStage::Fragment, shadergen.GetLanguage(),
shadergen.GenerateCopyFragmentShader(false));
if (!fs)
return {};
GPUPipeline::GraphicsConfig plconfig;
plconfig.layout = GPUPipeline::Layout::SingleTextureAndPushConstants;
plconfig.primitive = GPUPipeline::Primitive::TriangleStrips;
plconfig.input_layout = {};
plconfig.rasterization = GPUPipeline::RasterizationState::GetNoCullState();
plconfig.depth = GPUPipeline::DepthState::GetNoTestsState();
plconfig.blend = GPUPipeline::BlendState::GetNoBlendingState();
plconfig.render_pass_flags = GPUPipeline::NoRenderPassFlags;
plconfig.vertex_shader = vs.get();
plconfig.fragment_shader = fs.get();
plconfig.geometry_shader = nullptr;
plconfig.SetTargetFormats(format);
return g_gpu_device->CreatePipeline(plconfig, error);
}
GPUPresentResult PostProcessing::SlangShader::Apply(GPUTexture* original_color, GPUTexture* input_color,
GPUTexture* input_depth, GPUTexture* final_target,
const GSVector4i& final_rect, s32 orig_width, s32 orig_height,
s32 native_width, s32 native_height, u32 target_width,
u32 target_height, float time)
{
const auto bind_final_target = [](GPUTexture* final_target, const GSVector4i& final_rect) {
if (!final_target)
{
return g_gpu_device->BeginPresent(g_gpu_device->GetMainSwapChain());
}
else
{
// not drawing to the swap chain, so any area outside final_rect needs to be cleared
if (final_target->GetSizeVec().eq(final_rect.rsize()))
{
g_gpu_device->InvalidateRenderTarget(final_target);
g_gpu_device->SetRenderTarget(final_target, nullptr);
}
else
{
g_gpu_device->SetRenderTarget(final_target, nullptr);
g_gpu_device->ClearRenderTarget(final_target, GPUDevice::DEFAULT_CLEAR_COLOR);
}
return GPUPresentResult::OK;
}
};
GL_SCOPE_FMT("Slang Shader {}", m_name);
// TODO: Extract out
if (!m_original_history_textures.empty())
{
GL_INS_FMT("Updating original history texture");
// Need to copy the input before drawing, because of swap chain
Error error;
if (!g_gpu_device->ResizeTexture(&m_original_history_textures[0], original_color->GetWidth(),
original_color->GetHeight(), GPUTexture::Type::Texture,
original_color->GetFormat(), GPUTexture::Flags::None, false, &error))
{
ERROR_LOG("Failed to resize original history texture: {}", error.GetDescription());
}
else
{
g_gpu_device->CopyTextureRegion(m_original_history_textures[0].get(), 0, 0, 0, 0, original_color, 0, 0, 0, 0,
original_color->GetWidth(), original_color->GetHeight());
}
}
m_frame_count++;
// Vertices are computed and uploaded once.
static constexpr std::array<std::array<SlangShaderVertex, 4>, 3> vertices = {{
// position texcoord
{{
// D3D
{{-1.0f, -1.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{1.0f, -1.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
{{-1.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 0.0f}},
{{1.0f, 1.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
}},
{{
// Vulkan
{{-1.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{1.0f, 1.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
{{-1.0f, -1.0f, 0.0f, 1.0f}, {0.0f, 0.0f}},
{{1.0f, -1.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
}},
{{
// OpenGL
{{-1.0f, -1.0f, 0.0f, 1.0f}, {0.0f, 0.0f}},
{{1.0f, -1.0f, 0.0f, 1.0f}, {1.0f, 0.0f}},
{{-1.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{1.0f, 1.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
}},
}};
const RenderAPI rapi = g_gpu_device->GetRenderAPI();
const bool vbuf_index =
(rapi == RenderAPI::Vulkan) ? 1 : ((rapi == RenderAPI::OpenGL || rapi == RenderAPI::OpenGLES) ? 2 : 0);
u32 base_vertex;
g_gpu_device->UploadVertexBuffer(vertices[vbuf_index].data(), sizeof(SlangShaderVertex),
static_cast<u32>(vertices[0].size()), &base_vertex);
GPUTexture* last_framebuffer = nullptr;
for (size_t i = 0; i < m_passes.size(); i++)
{
const Pass& pass = m_passes[i];
GL_SCOPE_FMT("Pass {}: {}", i, pass.name);
// Sucks doing this twice, but we need to set the RT first (for DX11), and transition layouts (for VK).
std::array<GPUTexture*, GPUDevice::MAX_TEXTURE_SAMPLERS> textures = {};
std::array<GPUSampler*, GPUDevice::MAX_TEXTURE_SAMPLERS> samplers = {};
for (const auto& [tex_id, bind_point] : pass.samplers)
{
GL_INS_FMT("Texture {}: ID {} [{}]", bind_point, tex_id, GetTextureNameForID(tex_id));
std::tie(textures[bind_point], samplers[bind_point]) = GetTextureByID(pass, tex_id, original_color, input_color);
if (textures[bind_point])
textures[bind_point]->MakeReadyForSampling();
}
Texture& output_texture = m_textures[static_cast<size_t>(pass.output_texture_id)];
GL_INS_FMT("Output Texture: {}", output_texture.name);
// If there's no padding, we can write directly to the output framebuffer.
// The shader might use gl_FragCoord, so we need (0, 0) to point to the space being shaded,
// because pillarboxes aren't post-processed with slang.
GPUTexture* const framebuffer = m_textures[static_cast<size_t>(pass.output_texture_id)].texture.get();
if (i == (m_passes.size() - 1) && !output_texture.needs_feedback && framebuffer->GetWidth() == target_width &&
framebuffer->GetHeight() == target_height)
{
// last pass, can write directly to final target
GL_INS("Last pass writing directly to final target");
last_framebuffer = final_target;
if (const GPUPresentResult pres = bind_final_target(final_target, final_rect); pres != GPUPresentResult::OK)
return pres;
}
else
{
last_framebuffer = framebuffer;
g_gpu_device->InvalidateRenderTarget(framebuffer);
g_gpu_device->SetRenderTarget(framebuffer, nullptr);
}
const GSVector2i output_size = framebuffer->GetSizeVec();
g_gpu_device->SetViewportAndScissor(GSVector4i::loadh(output_size));
g_gpu_device->SetPipeline(pass.pipeline.get());
for (size_t j = 0; j < textures.size(); j++)
g_gpu_device->SetTextureSampler(static_cast<u32>(j), textures[j], samplers[j]);
alignas(VECTOR_ALIGNMENT) u8 push_constant_data[MAX_PUSH_CONSTANT_SIZE];
BindPassUniforms(pass, push_constant_data, original_color, input_color, output_size, final_rect, orig_width,
orig_height, native_width, native_height, target_width, target_height, time);
if (pass.push_constants_size > 0)
g_gpu_device->DrawWithPushConstants(4, base_vertex, push_constant_data, pass.push_constants_size);
else
g_gpu_device->Draw(4, base_vertex);
if (framebuffer->GetLevels() > 1)
{
GL_INS("Generating mipmaps for output texture");
framebuffer->GenerateMipmaps();
}
if (output_texture.needs_feedback)
std::swap(output_texture.feedback_texture, output_texture.texture);
}
// blit pass if required
if (last_framebuffer != final_target)
{
GL_SCOPE_FMT("Blit {} to target: {}", last_framebuffer->GetSizeVec(), final_rect);
last_framebuffer->MakeReadyForSampling();
if (const GPUPresentResult pres = bind_final_target(final_target, final_rect); pres != GPUPresentResult::OK)
return pres;
g_gpu_device->SetViewportAndScissor(final_rect);
g_gpu_device->SetPipeline(m_output_blit_pipeline.get());
g_gpu_device->SetTextureSampler(0, last_framebuffer, g_gpu_device->GetNearestSampler());
g_gpu_device->Draw(4, 0);
}
// TODO: get rid of the rotate, replace with head ptr instead
if (!m_original_history_textures.empty())
{
std::rotate(m_original_history_textures.rbegin(), m_original_history_textures.rbegin() + 1,
m_original_history_textures.rend());
}
return GPUPresentResult::OK;
}
bool PostProcessing::SlangShader::ResizeTargets(u32 source_width, u32 source_height, GPUTextureFormat target_format,
u32 target_width, u32 target_height, u32 viewport_width,
u32 viewport_height, Error* error)
{
static constexpr auto apply_scale = [](ScaleType type, float val, u32 source_dim, u32 viewport_dim,
u32 original_dim) {
switch (type)
{
case ScaleType::Source:
return static_cast<u32>(static_cast<float>(source_dim) * val);
case ScaleType::Viewport:
return static_cast<u32>(static_cast<float>(viewport_dim) * val);
case ScaleType::Absolute:
return static_cast<u32>(val);
case ScaleType::Original:
return static_cast<u32>(static_cast<float>(original_dim) * val);
DefaultCaseIsUnreachable();
}
};
u32 prev_width = source_width;
u32 prev_height = source_height;
for (Pass& pass : m_passes)
{
if (pass.output_texture_id < 0)
continue;
const u32 tex_width = std::max<u32>(
apply_scale(pass.output_scale[0].first, pass.output_scale[0].second, prev_width, viewport_width, source_width),
1);
const u32 tex_height = std::max<u32>(
apply_scale(pass.output_scale[1].first, pass.output_scale[1].second, prev_height, viewport_height, source_height),
1);
// Source for the next pass is the previous pass's output.
prev_width = tex_width;
prev_height = tex_height;
Texture& tex = m_textures[static_cast<size_t>(pass.output_texture_id)];
if (!tex.texture || tex.texture->GetWidth() != tex_width || tex.texture->GetHeight() != tex_height)
{
const u32 levels = tex.generate_mipmaps ? GPUTexture::GetFullMipmapCount(tex_width, tex_height) : 1;
const GPUTexture::Flags flags = (levels > 1) ? GPUTexture::Flags::AllowGenerateMipmaps : GPUTexture::Flags::None;
g_gpu_device->RecycleTexture(std::move(tex.texture));
g_gpu_device->RecycleTexture(std::move(tex.feedback_texture));
tex.texture = g_gpu_device->FetchTexture(tex_width, tex_height, 1, levels, 1, GPUTexture::Type::RenderTarget,
pass.output_format, flags, nullptr, 0, error);
if (!tex.texture)
return false;
if (tex.needs_feedback)
{
tex.feedback_texture =
g_gpu_device->FetchTexture(tex_width, tex_height, 1, levels, 1, GPUTexture::Type::RenderTarget,
pass.output_format, flags, nullptr, 0, error);
if (!tex.feedback_texture)
{
g_gpu_device->RecycleTexture(std::move(tex.texture));
return false;
}
// in case it's read this frame, pre-clear it
g_gpu_device->ClearRenderTarget(tex.feedback_texture.get(), GPUDevice::DEFAULT_CLEAR_COLOR);
}
}
if (!tex.sampler)
{
if (!(tex.sampler = g_gpu_device->GetSampler(pass.output_sampler_config, error)))
return false;
}
}
return true;
}
TinyString PostProcessing::SlangShader::GetTextureNameForID(TextureID id) const
{
TinyString ret;
if (id == TEXTURE_ID_ORIGINAL)
{
ret = "Original/Input Color Texture";
}
else if (id == TEXTURE_ID_SOURCE)
{
ret = "Source/Last Color Texture";
}
else if (id >= TEXTURE_ID_ORIGINAL_HISTORY_START)
{
ret.format("Original/Input Color History Texture {}", id - TEXTURE_ID_ORIGINAL_HISTORY_START);
}
else
{
const bool is_feedback = ((id & TEXTURE_ID_FEEDBACK) != 0);
const size_t idx = static_cast<size_t>(id & ~TEXTURE_ID_FEEDBACK);
if (idx >= m_textures.size())
ret = "UNKNOWN";
else
ret.format("{}{}", m_textures[idx].name, is_feedback ? " (Feedback)" : "");
}
return ret;
}
std::tuple<GPUTexture*, GPUSampler*> PostProcessing::SlangShader::GetTextureByID(const Pass& pass, TextureID id,
GPUTexture* original_color,
GPUTexture* input_color) const
{
if (id == TEXTURE_ID_SOURCE)
{
const size_t this_pass_idx = static_cast<size_t>(std::distance(&m_passes[0], &pass));
if (this_pass_idx > 0)
{
// Not the first pass, return the last pass's output texture.
id = m_passes[this_pass_idx - 1].output_texture_id;
}
else
{
// First pass, no last texture.
return {input_color, g_gpu_device->GetNearestSampler()};
}
}
if (id == TEXTURE_ID_ORIGINAL)
return {original_color, g_gpu_device->GetNearestSampler()};
if (id >= TEXTURE_ID_ORIGINAL_HISTORY_START)
{
const size_t history_idx = static_cast<size_t>(id - TEXTURE_ID_ORIGINAL_HISTORY_START);
DebugAssert(history_idx < m_original_history_textures.size());
return {m_original_history_textures[history_idx].get(), g_gpu_device->GetNearestSampler()};
}
const bool is_feedback = ((id & TEXTURE_ID_FEEDBACK) != 0);
const size_t idx = static_cast<size_t>(id & ~TEXTURE_ID_FEEDBACK);
if (idx >= m_textures.size())
Panic("Unexpected texture ID");
const Texture& tex = m_textures[idx];
return {is_feedback ? tex.feedback_texture.get() : tex.texture.get(), tex.sampler};
}
void PostProcessing::SlangShader::BindPassUniforms(const Pass& pass, u8* const push_constant_data,
GPUTexture* original_color, GPUTexture* input_color,
GSVector2i output_size, GSVector4i final_rect, s32 orig_width,
s32 orig_height, s32 native_width, s32 native_height,
u32 target_width, u32 target_height, float time)
{
GL_INS_FMT("Uniform buffer: {} bytes", pass.uniforms_size);
GL_INS_FMT("Push constants: {} bytes", pass.push_constants_size);
u8* const ubo_data =
(pass.uniforms_size > 0) ? static_cast<u8*>(g_gpu_device->MapUniformBuffer(pass.uniforms_size)) : nullptr;
for (const Uniform& ui : pass.uniforms)
{
u8* const dst = (ui.push_constant ? push_constant_data : ubo_data) + ui.offset;
if (ui.type < 0)
{
switch (ui.type)
{
case BuiltinUniform::MVP:
{
GSMatrix4x4::Identity().store(dst);
}
break;
case BuiltinUniform::OutputSize:
{
const GSVector2 v = GSVector2(output_size);
GSVector4::store<false>(dst, GSVector4::xyxy(v, GSVector2::cxpr(1.0f, 1.0f) / v));
}
break;
case BuiltinUniform::FinalViewportSize:
{
const GSVector2 v = GSVector2(GSVector2i(target_width, target_height));
GSVector4::store<false>(dst, GSVector4::xyxy(v, GSVector2::cxpr(1.0f, 1.0f) / v));
}
break;
case BuiltinUniform::FrameCount:
{
const u32 val = (pass.frame_count_mod != 0) ? (m_frame_count % pass.frame_count_mod) : m_frame_count;
std::memcpy(dst, &val, sizeof(u32));
}
break;
case BuiltinUniform::FrameDirection:
{
const u32 val = 1;
std::memcpy(dst, &val, sizeof(u32));
}
break;
case BuiltinUniform::TextureSize:
{
const GPUTexture* texture =
std::get<0>(GetTextureByID(pass, ui.associated_texture, original_color, input_color));
DebugAssert(texture);
const GSVector2 v = GSVector2(texture->GetSizeVec());
GSVector4::store<false>(dst, GSVector4::xyxy(v, GSVector2::cxpr(1.0f, 1.0f) / v));
}
break;
case BuiltinUniform::Zero:
{
if (ui.size == sizeof(float))
std::memset(dst, 0, sizeof(float));
else if (ui.size == sizeof(float) * 4)
std::memset(dst, 0, sizeof(float) * 4);
else
std::memset(dst, 0, ui.size);
}
break;
DefaultCaseIsUnreachable();
}
}
else
{
DebugAssert(static_cast<u32>(ui.type) < m_options.size());
const ShaderOption& option = m_options[static_cast<u32>(ui.type)];
const float value = ((option.type == ShaderOption::Type::Bool) ? (option.value[0].int_value ? 1.0f : 0.0f) :
option.value[0].float_value);
std::memcpy(dst, &value, sizeof(value));
}
}
if (pass.uniforms_size > 0)
g_gpu_device->UnmapUniformBuffer(pass.uniforms_size);
}