// 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, 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, 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);
}
