Improve graphics processing and resource management (#382)

Refine graphics processing and resource handling.
This commit is contained in:
Bruno 2026-10-09 15:03:59 +02:00 committed by GitHub
parent f208b8da46
commit 732d0c4cba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1133 additions and 136 deletions

View File

@ -129,8 +129,14 @@ struct VideoManager::Impl
if (videoSettings.use_disk_shader_cache && !stop_token.stop_requested())
{
const u64 program_id = m_modules.OperatingSystem().GetProgramId();
m_gpuCore->Renderer().ReadRasterizer()->LoadDiskResources(program_id, stop_token,
notify_disk_cache_progress);
try {
m_gpuCore->Renderer().ReadRasterizer()->LoadDiskResources(
program_id, stop_token, notify_disk_cache_progress);
} catch (const std::exception& exception) {
// std::async stores exceptions in its future. If one escapes here,
// Start is never called and guest GPU requests wait indefinitely.
LOG_ERROR(Render, "Shader cache loading failed: {}", exception.what());
}
}
}
notify_disk_cache_progress(VideoCore::LoadCallbackStage::Complete, 0, 0);

View File

@ -226,6 +226,7 @@ add_library(shader_recompiler STATIC
ir_opt/lower_fp16_to_fp32.cpp
ir_opt/lower_fp64_to_fp32.cpp
ir_opt/lower_int64_to_int32.cpp
ir_opt/bindless_descriptor.h
ir_opt/passes.h
ir_opt/position_pass.cpp
ir_opt/rescaling_pass.cpp

View File

@ -398,6 +398,13 @@ void SetupTransformFeedbackCapabilities(EmitContext& ctx, Id main_func) {
}
void SetupCapabilities(const Profile& profile, const Info& info, EmitContext& ctx) {
if (ctx.uses_nonuniform_sampled_image) {
if (profile.supported_spirv < 0x00010500) {
ctx.AddExtension("SPV_EXT_descriptor_indexing");
}
ctx.AddCapability(spv::Capability::ShaderNonUniform);
ctx.AddCapability(spv::Capability::SampledImageArrayNonUniformIndexing);
}
if (info.uses_sampled_1d) {
ctx.AddCapability(spv::Capability::Sampled1D);
}

View File

@ -227,11 +227,26 @@ private:
spv::ImageOperandsMask mask{};
};
Id Texture(EmitContext& ctx, IR::TextureInstInfo info, [[maybe_unused]] const IR::Value& index) {
Id Texture(EmitContext& ctx, IR::TextureInstInfo info, const IR::Value& index) {
const TextureDefinition& def{ctx.textures.at(info.descriptor_index)};
if (def.count > 1) {
const Id pointer{ctx.OpAccessChain(def.pointer_type, def.id, ctx.Def(index))};
return ctx.OpLoad(def.sampled_type, pointer);
if (!index.IsImmediate() && !ctx.profile.support_sampled_image_array_nonuniform_indexing) {
throw NotImplementedException("Nonuniform sampled texture indexing is unsupported by the host");
}
const Id array_index{ctx.Def(index)};
const Id pointer{ctx.OpAccessChain(def.pointer_type, def.id, array_index)};
const Id sampled_image{ctx.OpLoad(def.sampled_type, pointer)};
// Guest bindless handles can depend on interpolated surface coordinates. Without
// NonUniform, the host may select a single array element for the entire subgroup.
if (!index.IsImmediate() && ctx.profile.support_sampled_image_array_nonuniform_indexing) {
ctx.uses_nonuniform_sampled_image = true;
for (const Id object : {array_index, pointer, sampled_image}) {
if (ctx.nonuniform_sampled_ids.insert(object.value).second) {
ctx.Decorate(object, spv::Decoration::NonUniform);
}
}
}
return sampled_image;
} else {
return ctx.OpLoad(def.sampled_type, def.id);
}
@ -318,8 +333,12 @@ Id IsScaled(EmitContext& ctx, const IR::Value& index, Id member_index, u32 base_
if (base_index != 0) {
index_value = ctx.OpIAdd(ctx.U32[1], index_value, ctx.Const(base_index));
}
const Id word_index{ctx.OpShiftRightLogical(ctx.U32[1], index_value, ctx.Const(5u))};
const Id pointer{ctx.OpAccessChain(push_constant_u32, ctx.rescaling_push_constants,
member_index, word_index)};
const Id word{ctx.OpLoad(ctx.U32[1], pointer)};
const Id bit_index{ctx.OpBitwiseAnd(ctx.U32[1], index_value, ctx.Const(31u))};
bit = ctx.OpBitFieldUExtract(ctx.U32[1], index_value, bit_index, ctx.Const(1u));
bit = ctx.OpBitFieldUExtract(ctx.U32[1], word, bit_index, ctx.Const(1u));
}
return ctx.OpINotEqual(ctx.U1, bit, ctx.u32_zero_value);
}

View File

@ -1517,6 +1517,8 @@ void EmitContext::DefineInputs(const IR::Program& program) {
}
if (info.uses_sample_id) {
sample_id = DefineInput(*this, U32[1], false, spv::BuiltIn::SampleId);
// Vulkan requires Flat on integer fragment inputs, including built-ins.
Decorate(sample_id, spv::Decoration::Flat);
}
if (info.uses_is_helper_invocation) {
is_helper_invocation = DefineInput(*this, U1, false, spv::BuiltIn::HelperInvocation);

View File

@ -4,6 +4,7 @@
#pragma once
#include <array>
#include <unordered_set>
#include <sirit/sirit.h>
@ -254,6 +255,9 @@ public:
std::vector<TextureDefinition> textures;
std::vector<ImageDefinition> images;
bool uses_nonuniform_sampled_image{};
std::unordered_set<u32> nonuniform_sampled_ids;
Id workgroup_id{};
Id local_invocation_id{};
Id invocation_id{};

View File

@ -20,6 +20,9 @@ public:
[[nodiscard]] virtual u32 ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) = 0;
// Zero means this environment cannot provide the bound size (e.g. an old cache).
[[nodiscard]] virtual u32 ReadCbufSize([[maybe_unused]] u32 cbuf_index) { return 0; }
[[nodiscard]] virtual TextureType ReadTextureType(u32 raw_handle) = 0;
[[nodiscard]] virtual TexturePixelFormat ReadTexturePixelFormat(u32 raw_handle) = 0;

View File

@ -21,6 +21,7 @@ struct HostTranslateInfo {
///< passthrough shaders
bool support_conditional_barrier{}; ///< True when the device supports barriers in conditional
///< control flow
u32 max_bindless_descriptors_per_stage{8};
};
} // namespace Shader

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "yuzu_common/common_types.h"
namespace Shader::Optimization {
// A selected handle field must fit entirely in the bound constant buffer.
constexpr u32 BindlessDescriptorCount(u32 buffer_size, u32 base_offset, u32 size_shift) {
if (size_shift > 31 || base_offset > buffer_size ||
buffer_size - base_offset < sizeof(u32)) {
return 0;
}
return 1 + (buffer_size - base_offset - sizeof(u32)) / (1U << size_shift);
}
}

View File

@ -1,14 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <optional>
#include <queue>
#include <unordered_map>
#include <unordered_set>
#include <boost/container/flat_set.hpp>
#include <boost/container/small_vector.hpp>
#include "yuzu_common/alignment.h"
#include "yuzu_shader_recompiler/frontend/ir/basic_block.h"
#include "yuzu_shader_recompiler/frontend/ir/breadth_first_search.h"
#include "yuzu_shader_recompiler/frontend/ir/ir_emitter.h"
#include "yuzu_shader_recompiler/frontend/ir/value.h"
#include "yuzu_shader_recompiler/host_translate_info.h"
@ -329,8 +332,50 @@ std::optional<LowAddrInfo> TrackLowAddress(IR::Inst* inst) {
};
}
/// Tries to track the storage buffer address used by a global memory instruction
std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias) {
/// Memory reads produce a value unrelated to the address used to locate it.
bool IsIndirectPointerLoad(IR::Opcode opcode) {
switch (opcode) {
case IR::Opcode::LoadGlobalS8:
case IR::Opcode::LoadGlobalU8:
case IR::Opcode::LoadGlobalS16:
case IR::Opcode::LoadGlobalU16:
case IR::Opcode::LoadGlobal32:
case IR::Opcode::LoadGlobal64:
case IR::Opcode::LoadGlobal128:
case IR::Opcode::LoadStorageS8:
case IR::Opcode::LoadStorageU8:
case IR::Opcode::LoadStorageS16:
case IR::Opcode::LoadStorageU16:
case IR::Opcode::LoadStorage32:
case IR::Opcode::LoadStorage64:
case IR::Opcode::LoadStorage128:
case IR::Opcode::LoadSharedS8:
case IR::Opcode::LoadSharedU8:
case IR::Opcode::LoadSharedS16:
case IR::Opcode::LoadSharedU16:
case IR::Opcode::LoadSharedU32:
case IR::Opcode::LoadSharedU64:
case IR::Opcode::LoadSharedU128:
case IR::Opcode::LoadLocal:
case IR::Opcode::GetCbufU8:
case IR::Opcode::GetCbufS8:
case IR::Opcode::GetCbufU16:
case IR::Opcode::GetCbufS16:
case IR::Opcode::GetCbufF32:
return true;
default:
return false;
}
}
struct TrackingState {
std::unordered_map<const IR::Inst*, std::optional<StorageBufferAddr>> selections;
std::unordered_set<const IR::Inst*> active;
};
/// Tries to track a base without crossing pointer loads or choosing one side of a merge.
std::optional<StorageBufferAddr> TrackImpl(const IR::Value& value, const Bias* bias,
bool* indirect_pointer, TrackingState& state) {
const auto pred{[bias](const IR::Inst* inst) -> std::optional<StorageBufferAddr> {
if (inst->GetOpcode() != IR::Opcode::GetCbufU32 &&
inst->GetOpcode() != IR::Opcode::GetCbufU32x2) {
@ -363,7 +408,71 @@ std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias)
}
return storage_buffer;
}};
return BreadthFirstSearch(value, pred);
if (value.IsImmediate()) {
return std::nullopt;
}
small_vector<const IR::Inst*, 16> visited;
std::queue<const IR::Inst*> pending;
const auto enqueue = [&](const IR::Value& arg) {
if (arg.IsImmediate()) return;
const auto* inst = arg.InstRecursive();
if (std::ranges::find(visited, inst) == visited.end()) {
visited.push_back(inst);
pending.push(inst);
}
};
enqueue(value);
while (!pending.empty()) {
const auto* inst = pending.front();
pending.pop();
if (const auto result = pred(inst)) return result;
if (inst->GetOpcode() == IR::Opcode::SelectU32 || inst->GetOpcode() == IR::Opcode::Phi) {
auto cached = state.selections.find(inst);
if (cached == state.selections.end()) {
// Cyclic/very deep SSA merges remain on the global-memory path.
if (state.active.size() >= 64 || !state.active.insert(inst).second) {
if (indirect_pointer) *indirect_pointer = true;
continue;
}
std::optional<StorageBufferAddr> common;
bool mismatch = false;
const size_t begin = inst->GetOpcode() == IR::Opcode::SelectU32 ? 1 : 0;
for (size_t arg = begin; arg < inst->NumArgs(); ++arg) {
const auto branch = TrackImpl(inst->Arg(arg), bias, indirect_pointer, state);
if (!branch || (common && *common != *branch)) {
mismatch = true;
break;
}
common = branch;
}
state.active.erase(inst);
cached = state.selections.emplace(inst, mismatch ? std::nullopt : common).first;
}
if (cached->second) return cached->second;
if (indirect_pointer) *indirect_pointer = true;
// A separate additive base may still be present in the pending queue.
continue;
}
if (IsIndirectPointerLoad(inst->GetOpcode())) {
// Its address describes where the pointer is stored, not where it points.
// Following that address can wrongly select an unrelated storage buffer.
if (indirect_pointer) *indirect_pointer = true;
continue;
}
if (inst->GetOpcode() == IR::Opcode::GetCbufU32 ||
inst->GetOpcode() == IR::Opcode::GetCbufU32x2) {
// A dynamically indexed descriptor cannot be identified by tracking its index.
continue;
}
for (size_t arg = inst->NumArgs(); arg--;) enqueue(inst->Arg(arg));
}
return std::nullopt;
}
std::optional<StorageBufferAddr> Track(const IR::Value& value, const Bias* bias,
bool* indirect_pointer = nullptr) {
TrackingState state;
return TrackImpl(value, bias, indirect_pointer, state);
}
/// Collects the storage buffer used by a global memory instruction and the instruction itself
@ -387,10 +496,16 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
std::optional<StorageBufferAddr> storage_buffer{Track(low_addr, &nvn_bias)};
if (!storage_buffer) {
// If it fails, track without a bias
storage_buffer = Track(low_addr, nullptr);
bool indirect_pointer{};
storage_buffer = Track(low_addr, nullptr, &indirect_pointer);
if (!storage_buffer) {
// If that also fails, use NVN fallbacks
LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks");
if (indirect_pointer) {
LOG_INFO(Shader, "Storage buffer pointer loaded from memory; "
"using runtime global memory lookup");
} else {
LOG_WARNING(Shader, "Storage buffer failed to track, using global memory fallbacks");
}
return;
}
LOG_WARNING(Shader, "Storage buffer tracked without bias, index {} offset {}",

View File

@ -4,6 +4,8 @@
#include <algorithm>
#include <bit>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <boost/container/small_vector.hpp>
@ -13,10 +15,14 @@
#include "yuzu_shader_recompiler/frontend/ir/ir_emitter.h"
#include "yuzu_shader_recompiler/host_translate_info.h"
#include "yuzu_shader_recompiler/ir_opt/passes.h"
#include "yuzu_shader_recompiler/ir_opt/bindless_descriptor.h"
#include "yuzu_shader_recompiler/shader_info.h"
namespace Shader::Optimization {
namespace {
constexpr u32 DESCRIPTOR_SIZE = 8;
constexpr u32 DESCRIPTOR_SIZE_SHIFT = static_cast<u32>(std::countr_zero(DESCRIPTOR_SIZE));
struct ConstBufferAddr {
u32 index;
u32 offset;
@ -26,6 +32,7 @@ struct ConstBufferAddr {
u32 secondary_shift_left;
IR::U32 dynamic_offset;
u32 count;
u32 size_shift{DESCRIPTOR_SIZE_SHIFT};
bool has_secondary;
};
@ -37,8 +44,76 @@ struct TextureInst {
using TextureInstVector = boost::container::small_vector<TextureInst, 24>;
constexpr u32 DESCRIPTOR_SIZE = 8;
constexpr u32 DESCRIPTOR_SIZE_SHIFT = static_cast<u32>(std::countr_zero(DESCRIPTOR_SIZE));
// Bindless handles may be fields of material records rather than consecutive handles.
// Track alignment through SSA merges so indexing and descriptor uploads use the same stride.
u32 OffsetAlignment(const IR::Value& value, std::unordered_set<const IR::Inst*>& visiting,
std::unordered_map<const IR::Inst*, u32>& alignments, u32 depth = 0) {
if (value.IsImmediate()) {
return static_cast<u32>(std::countr_zero(value.U32()));
}
const IR::Inst* inst{value.InstRecursive()};
if (depth >= 128 || !inst) {
return 0;
}
if (const auto cached = alignments.find(inst); cached != alignments.end()) {
return cached->second;
}
if (!visiting.insert(inst).second) {
return 0;
}
const auto alignment = [&] {
switch (inst->GetOpcode()) {
case IR::Opcode::UndefU32:
// Undefined predecessors impose no constraint on the defined value.
return 32U;
case IR::Opcode::ShiftLeftLogical32:
if (inst->Arg(1).IsImmediate() && inst->Arg(1).U32() < 32) {
return std::min(32U, inst->Arg(1).U32() +
OffsetAlignment(inst->Arg(0), visiting, alignments, depth + 1));
}
return 0U;
case IR::Opcode::Phi: {
u32 result{32};
for (size_t i = 0; i < inst->NumArgs(); ++i) {
result = std::min(result, OffsetAlignment(inst->Arg(i), visiting, alignments, depth + 1));
}
return result;
}
case IR::Opcode::BitwiseAnd32:
// AND guarantees every zero low bit guaranteed by either operand.
return std::max(OffsetAlignment(inst->Arg(0), visiting, alignments, depth + 1),
OffsetAlignment(inst->Arg(1), visiting, alignments, depth + 1));
case IR::Opcode::IAdd32:
case IR::Opcode::ISub32:
case IR::Opcode::BitwiseOr32:
case IR::Opcode::BitwiseXor32:
// Addition/subtraction wrap in U32, preserving the common power-of-two divisor.
return std::min(OffsetAlignment(inst->Arg(0), visiting, alignments, depth + 1),
OffsetAlignment(inst->Arg(1), visiting, alignments, depth + 1));
case IR::Opcode::SelectU32:
return std::min(OffsetAlignment(inst->Arg(1), visiting, alignments, depth + 1),
OffsetAlignment(inst->Arg(2), visiting, alignments, depth + 1));
default:
return 0U;
}
}();
visiting.erase(inst);
// SSA is a graph: repeated phi/select predecessors must not multiply traversal work.
alignments.emplace(inst, alignment);
return alignment;
}
u32 DynamicDescriptorSizeShift(const ConstBufferAddr& addr,
std::unordered_set<const IR::Inst*>& visiting,
std::unordered_map<const IR::Inst*, u32>& alignments) {
if (addr.count <= 1) {
return DESCRIPTOR_SIZE_SHIFT;
}
const u32 alignment{OffsetAlignment(addr.dynamic_offset, visiting, alignments)};
// Alignment is a guaranteed divisor of every possible byte offset, not a minimum
// eight-byte record size. A U32 handle can occupy a four-byte field (or be unaligned).
return std::min(alignment, 31U);
}
IR::Opcode IndexedInstruction(const IR::Inst& inst) {
switch (inst.GetOpcode()) {
@ -596,6 +671,33 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
to_replace.push_back(MakeInst(env, block, inst));
}
}
// All source expressions are still unchanged here. Share SSA results across accesses.
std::unordered_set<const IR::Inst*> alignment_visiting;
std::unordered_map<const IR::Inst*, u32> offset_alignments;
for (auto& texture : to_replace) {
if (texture.cbuf.count <= 1) continue;
const u32 buffer_size = env.ReadCbufSize(texture.cbuf.index);
if (buffer_size == 0) {
throw NotImplementedException("Bindless constant buffer size is unavailable");
}
const u32 shift =
DynamicDescriptorSizeShift(texture.cbuf, alignment_visiting, offset_alignments);
texture.cbuf.size_shift = shift;
// Enumerate the entire buffer at this field's alignment phase. Guest U32 additions
// can wrap and select valid handles before the original immediate base.
const u32 phase = texture.cbuf.offset & ((1U << shift) - 1);
const u32 adjustment = texture.cbuf.offset - phase;
if (adjustment != 0) {
IR::IREmitter ir{*texture.block, IR::Block::InstructionList::s_iterator_to(*texture.inst)};
texture.cbuf.dynamic_offset = IR::U32{ir.IAdd(texture.cbuf.dynamic_offset,
ir.Imm32(adjustment))};
}
texture.cbuf.offset = phase;
texture.cbuf.count = BindlessDescriptorCount(buffer_size, phase, shift);
if (texture.cbuf.count == 0) {
throw NotImplementedException("Bindless constant buffer contains no complete handle");
}
}
// Sort instructions to visit textures by constant buffer index, then by offset
std::ranges::sort(to_replace, [](const auto& lhs, const auto& rhs) {
return lhs.cbuf.offset < rhs.cbuf.offset;
@ -615,6 +717,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
inst->ReplaceOpcode(IndexedInstruction(*inst));
const auto& cbuf{texture_inst.cbuf};
const u32 size_shift{cbuf.size_shift};
auto flags{inst->Flags<IR::TextureInstInfo>()};
bool is_multisample{false};
switch (inst->GetOpcode()) {
@ -681,7 +784,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.cbuf_index = cbuf.index,
.cbuf_offset = cbuf.offset,
.count = cbuf.count,
.size_shift = DESCRIPTOR_SIZE_SHIFT,
.size_shift = size_shift,
});
} else {
index = descriptors.Add(ImageDescriptor{
@ -693,7 +796,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.cbuf_index = cbuf.index,
.cbuf_offset = cbuf.offset,
.count = cbuf.count,
.size_shift = DESCRIPTOR_SIZE_SHIFT,
.size_shift = size_shift,
});
}
break;
@ -709,7 +812,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_cbuf_offset = cbuf.secondary_offset,
.secondary_shift_left = cbuf.secondary_shift_left,
.count = cbuf.count,
.size_shift = DESCRIPTOR_SIZE_SHIFT,
.size_shift = size_shift,
});
} else {
index = descriptors.Add(TextureDescriptor{
@ -724,7 +827,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_cbuf_offset = cbuf.secondary_offset,
.secondary_shift_left = cbuf.secondary_shift_left,
.count = cbuf.count,
.size_shift = DESCRIPTOR_SIZE_SHIFT,
.size_shift = size_shift,
});
}
break;
@ -735,9 +838,9 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
if (cbuf.count > 1) {
const auto insert_point{IR::Block::InstructionList::s_iterator_to(*inst)};
IR::IREmitter ir{*texture_inst.block, insert_point};
const IR::U32 shift{ir.Imm32(std::countr_zero(DESCRIPTOR_SIZE))};
inst->SetArg(0, ir.UMin(ir.ShiftRightArithmetic(cbuf.dynamic_offset, shift),
ir.Imm32(DESCRIPTOR_SIZE - 1)));
const IR::U32 shift{ir.Imm32(size_shift)};
inst->SetArg(0, ir.UMin(ir.ShiftRightLogical(cbuf.dynamic_offset, shift),
ir.Imm32(cbuf.count - 1)));
} else {
inst->SetArg(0, IR::Value{});
}
@ -750,6 +853,15 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
}
}
}
// Count actual bindings after deduplication, rather than texture instructions.
// Never map a valid guest index to another handle to satisfy host resource limits.
const u64 sampled_count = u64{NumDescriptors(program.info.texture_descriptors)} +
NumDescriptors(program.info.texture_buffer_descriptors);
if (std::ranges::any_of(to_replace, [](const auto& texture) {
return !texture.cbuf.dynamic_offset.IsEmpty();
}) && sampled_count > host_info.max_bindless_descriptors_per_stage) {
throw NotImplementedException("Sampled texture bindings exceed the host descriptor budget");
}
}
void JoinTextureInfo(Info& base, Info& source) {

View File

@ -11,6 +11,7 @@ struct Profile {
u32 supported_spirv{0x00010000};
bool unified_descriptor_binding{};
bool support_descriptor_aliasing{};
bool support_sampled_image_array_nonuniform_indexing{};
// MoltenVK needs one canonical uniform view; storage-buffer aliases remain available.
bool disable_uniform_buffer_descriptor_aliasing{};
bool support_int8{};

View File

@ -42,6 +42,7 @@
<ClInclude Include="backend\spirv\emit_spirv.h" />
<ClInclude Include="backend\spirv\emit_spirv_instructions.h" />
<ClInclude Include="backend\spirv\spirv_emit_context.h" />
<ClInclude Include="ir_opt\bindless_descriptor.h" />
<ClInclude Include="environment.h" />
<ClInclude Include="exception.h" />
<ClInclude Include="frontend\ir\abstract_syntax_list.h" />

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
@ -237,7 +237,12 @@
<Filter>Header Files\frontend\maxwell\translate\impl</Filter>
</ClInclude>
<ClInclude Include="ir_opt\passes.h">
<Filter>Header Files\ir_opt</Filter>
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ir_opt\bindless_descriptor.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>

View File

@ -197,6 +197,10 @@ add_library(video_core STATIC
renderer_vulkan/present/window_adapt_pass.h
renderer_vulkan/blit_image.cpp
renderer_vulkan/blit_image.h
renderer_vulkan/descriptor_lookup.h
renderer_vulkan/descriptor_limits.h
renderer_vulkan/descriptor_payload.h
renderer_vulkan/cbuf_size_dependencies.h
renderer_vulkan/fixed_pipeline_state.cpp
renderer_vulkan/fixed_pipeline_state.h
renderer_vulkan/maxwell_to_vk.cpp
@ -277,6 +281,7 @@ add_library(video_core STATIC
texture_cache/image_base.h
texture_cache/image_info.cpp
texture_cache/image_info.h
texture_cache/mip_levels.h
texture_cache/image_view_base.cpp
texture_cache/image_view_base.h
texture_cache/image_view_info.cpp

View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <bit>
#include <limits>
#include <unordered_map>
#include "yuzu_common/common_types.h"
namespace Vulkan {
// Updated by the GPU thread and disk-cache reader, never by pipeline worker jobs.
class CbufSizeDependencies {
public:
void Observe(u64 shader, u32 mask) {
if (shader != 0) {
const auto [it, inserted] = masks.try_emplace(shader, mask);
const u32 combined = it->second | mask;
if (inserted || combined != it->second) {
it->second = combined;
++revision;
}
}
}
u32 Mask(u64 shader) const {
if (shader == 0) {
return 0;
}
const auto it = masks.find(shader);
// Until translation has reported its dependencies, specialize conservatively.
return it != masks.end() ? it->second : std::numeric_limits<u32>::max();
}
u64 Revision() const noexcept { return revision; }
template <size_t NumBanks>
static void Normalize(std::array<u32, NumBanks>& sizes, u32 mask) {
static_assert(NumBanks <= 32);
for (size_t bank = 0; bank < NumBanks; ++bank) {
if ((mask & (1U << bank)) == 0) {
sizes[bank] = 0;
}
}
}
private:
std::unordered_map<u64, u32> masks;
u64 revision{};
};
// Retain only dependency metadata, never bound sizes or guest addresses. Required banks
// are read on every draw, including enable/disable and size changes with the same shader.
template <size_t NumStages, size_t NumBanks>
class GraphicsCbufSizeState {
public:
GraphicsCbufSizeState() { stage_masks.fill(std::numeric_limits<u32>::max()); }
template <typename ReadSize>
void Refresh(std::array<std::array<u32, NumBanks>, NumStages>& sizes,
const std::array<u64, NumStages + 1>& hashes,
const CbufSizeDependencies& dependencies, ReadSize&& read_size) {
static_assert(NumBanks < 32);
if (revision != dependencies.Revision() || shader_hashes != hashes) {
shader_hashes = hashes;
revision = dependencies.Revision();
for (size_t stage = 0; stage < NumStages; ++stage) {
const u32 mask = (dependencies.Mask(hashes[stage + 1]) |
(stage == 0 ? dependencies.Mask(hashes[0]) : 0)) & ((1U << NumBanks) - 1);
if (mask != stage_masks[stage]) {
sizes[stage].fill(0);
stage_masks[stage] = mask;
}
}
}
for (size_t stage = 0; stage < NumStages; ++stage) {
u32 mask = stage_masks[stage];
while (mask != 0) {
const auto bank = std::countr_zero(mask);
sizes[stage][bank] = read_size(stage, bank);
mask &= mask - 1;
}
}
}
private:
std::array<u64, NumStages + 1> shader_hashes{};
std::array<u32, NumStages> stage_masks{};
u64 revision{std::numeric_limits<u64>::max()};
};
} // namespace Vulkan

View File

@ -0,0 +1,99 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <vulkan/vulkan_core.h>
namespace Vulkan {
namespace DescriptorLimitsDetail {
struct DescriptorUsage {
uint64_t samplers{}, sampled_images{}, storage_images{}, uniform_buffers{}, storage_buffers{};
uint64_t resources{};
bool Add(VkDescriptorType type, uint32_t count) {
switch (type) {
case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
uniform_buffers += count;
break;
case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER:
storage_buffers += count;
break;
case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
sampled_images += count;
break;
case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
storage_images += count;
break;
case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
samplers += count;
sampled_images += count;
break;
default:
return false;
}
// Combined image samplers count as one resource, but against both type limits.
resources += count;
return true;
}
};
} // namespace DescriptorLimitsDetail
// Check the actual pipeline layout once at construction, including widened stage visibility.
// No per-draw cost, fixed stage count, and 64-bit sums for untrusted descriptor counts.
inline const char* DescriptorLimitViolation(
const VkPhysicalDeviceLimits& limits, std::span<const VkDescriptorSetLayoutBinding> bindings,
uint32_t color_attachments = 0) {
constexpr std::array stages{
VK_SHADER_STAGE_VERTEX_BIT,
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT,
VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT,
VK_SHADER_STAGE_GEOMETRY_BIT,
VK_SHADER_STAGE_FRAGMENT_BIT,
VK_SHADER_STAGE_COMPUTE_BIT,
};
DescriptorLimitsDetail::DescriptorUsage total;
std::array<DescriptorLimitsDetail::DescriptorUsage, stages.size()> usage{};
for (const auto& binding : bindings) {
if (!total.Add(binding.descriptorType, binding.descriptorCount)) {
return "Unsupported descriptor type in pipeline layout";
}
for (std::size_t stage = 0; stage < stages.size(); ++stage) {
if ((binding.stageFlags & stages[stage]) != 0) {
usage[stage].Add(binding.descriptorType, binding.descriptorCount);
}
}
}
if (total.samplers > limits.maxDescriptorSetSamplers ||
total.sampled_images > limits.maxDescriptorSetSampledImages ||
total.storage_images > limits.maxDescriptorSetStorageImages ||
total.uniform_buffers > limits.maxDescriptorSetUniformBuffers ||
total.storage_buffers > limits.maxDescriptorSetStorageBuffers) {
return "Pipeline exceeds descriptor set resource limits";
}
for (std::size_t stage = 0; stage < stages.size(); ++stage) {
const auto& count = usage[stage];
const uint64_t attachments = stages[stage] == VK_SHADER_STAGE_FRAGMENT_BIT
? color_attachments : 0;
if (count.samplers > limits.maxPerStageDescriptorSamplers ||
count.sampled_images > limits.maxPerStageDescriptorSampledImages ||
count.storage_images > limits.maxPerStageDescriptorStorageImages ||
count.uniform_buffers > limits.maxPerStageDescriptorUniformBuffers ||
count.storage_buffers > limits.maxPerStageDescriptorStorageBuffers ||
count.resources + attachments > limits.maxPerStageResources) {
return "Pipeline exceeds per-stage resource limits";
}
if (stages[stage] == VK_SHADER_STAGE_FRAGMENT_BIT &&
attachments + count.storage_images + count.storage_buffers >
limits.maxFragmentCombinedOutputResources) {
return "Pipeline exceeds combined fragment output resource limit";
}
}
return nullptr;
}
} // namespace Vulkan

View File

@ -0,0 +1,109 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstring>
#include <limits>
#include <utility>
#include "yuzu_common/common_types.h"
namespace Vulkan {
// Lives for one shader stage of one draw/dispatch. Never retains guest pointers across draws,
// rebindings or address-space changes. Discontiguous ranges keep the original scalar read path.
template <size_t NumBanks>
class DescriptorCbufReader {
public:
template <typename Memory>
struct BoundBuffer {
Memory* memory;
GPUVAddr address;
u32 size;
const u8* span;
u32 Read(u32 offset) const {
if (span && offset <= size - sizeof(u32)) {
u32 value;
std::memcpy(&value, span + offset, sizeof(value));
return value;
}
return memory->template Read<u32>(address + offset);
}
};
// Prepare once per descriptor array. A fragmented bank takes the scalar path directly,
// without repeating bank/size/span queries for every array element.
template <typename Memory>
BoundBuffer<Memory> Bind(Memory& memory, u32 bank, GPUVAddr address, u32 size, u32 count) {
const u8* span{};
if (count > 64 && bank < NumBanks && size >= sizeof(u32)) {
const u32 bit = 1U << bank;
if ((queried_banks & bit) == 0) {
spans[bank] = memory.GetSpan(address, size);
queried_banks |= bit;
}
span = spans[bank];
}
return {&memory, address, size, span};
}
template <typename Memory>
u32 Read(Memory& memory, u32 bank, GPUVAddr address, u32 size, u32 offset, u32 count) {
return Bind(memory, bank, address, size, count).Read(offset);
}
private:
static_assert(NumBanks <= 32);
std::array<const u8*, NumBanks> spans; // Only read after its queried bit is set.
u32 queried_banks{};
};
// Reset after synchronizing descriptor tables, before each draw/dispatch. Caches lookups only
// within that operation, so pool modifications and sampler rebindings always remain visible.
template <typename Value, size_t NumSlots = 256>
class DrawDescriptorCache {
public:
DrawDescriptorCache() { Reset(); }
void Reset() {
consecutive_misses = 0;
enabled = cooldown == 0;
if (!enabled) {
--cooldown;
return;
}
indices.fill(std::numeric_limits<u32>::max());
}
bool Enabled() const noexcept { return enabled; }
template <typename Lookup>
Value Get(u32 index, Lookup&& lookup) {
if (!enabled || index == std::numeric_limits<u32>::max()) {
return lookup(index);
}
const size_t slot = index & (NumSlots - 1);
if (indices[slot] != index) {
values[slot] = lookup(index);
indices[slot] = index;
if (++consecutive_misses >= 64) {
// Correctness is unchanged: bypass useless caching and probe again later.
enabled = false;
cooldown = 16;
}
} else {
consecutive_misses = 0;
}
return values[slot];
}
private:
static_assert(NumSlots != 0 && (NumSlots & (NumSlots - 1)) == 0);
u32 consecutive_misses{}, cooldown{};
bool enabled{true};
std::array<u32, NumSlots> indices{};
std::array<Value, NumSlots> values{};
};
} // namespace Vulkan

View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include <stdexcept>
#include <vector>
namespace Vulkan {
// CPU descriptor data is read by the scheduler worker after the draw is recorded.
// Keep pending uploads stable, and drain that worker before recycling their storage.
template <typename Entry, size_t FrameCapacity, size_t Frames = 8>
class DescriptorPayload {
public:
void TickFrame() {
frame = (frame + 1) % Frames;
used = 0;
overflow_active = false;
}
template <typename WaitWorker>
void Acquire(size_t count, WaitWorker&& wait_worker) {
if (overflow_active) {
// The ring was drained when the previous overflow upload began.
used = 0;
overflow_active = false;
}
if (count > FrameCapacity) {
wait_worker();
overflow.resize(count);
upload = overflow.data();
overflow_active = true;
} else {
if (count > FrameCapacity - used) {
wait_worker();
used = 0;
}
upload = ring.data() + frame * FrameCapacity + used;
used += count;
}
written = 0;
reserved = count;
}
void Append(const Entry& value) {
if (written >= reserved) {
throw std::length_error("Descriptor upload exceeded its reservation");
}
upload[written++] = value;
}
const Entry* Data() const noexcept { return upload; }
private:
static_assert(FrameCapacity > 0 && Frames > 0);
std::array<Entry, FrameCapacity * Frames> ring;
std::vector<Entry> overflow;
Entry* upload{};
size_t frame{}, used{}, reserved{}, written{};
bool overflow_active{};
};
} // namespace Vulkan

View File

@ -10,7 +10,9 @@
#include "yuzu_common/common_types.h"
#include "yuzu_shader_recompiler/backend/spirv/emit_spirv.h"
#include "yuzu_shader_recompiler/exception.h"
#include "yuzu_shader_recompiler/shader_info.h"
#include "yuzu_video_core/renderer_vulkan/descriptor_limits.h"
#include "yuzu_video_core/renderer_vulkan/vk_texture_cache.h"
#include "yuzu_video_core/renderer_vulkan/vk_update_descriptor.h"
#include "yuzu_video_core/texture_cache/types.h"
@ -55,10 +57,21 @@ inline bool RasterizesLines(Shader::OutputTopology output,
polygon_mode == Tegra::Engines::Maxwell3D::Regs::PolygonMode::Line);
}
inline size_t NumDescriptorUpdateEntries(const Shader::Info& info) {
return size_t{Shader::NumDescriptors(info.constant_buffer_descriptors)} +
Shader::NumDescriptors(info.storage_buffers_descriptors) +
Shader::NumDescriptors(info.texture_buffer_descriptors) +
Shader::NumDescriptors(info.image_buffer_descriptors) +
Shader::NumDescriptors(info.texture_descriptors) +
Shader::NumDescriptors(info.image_descriptors);
}
class DescriptorLayoutBuilder {
public:
DescriptorLayoutBuilder(const Device& device_, VkShaderStageFlags storage_buffer_stages_ = 0)
: device{&device_}, storage_buffer_stages{storage_buffer_stages_} {}
DescriptorLayoutBuilder(const Device& device_, VkShaderStageFlags storage_buffer_stages_ = 0,
u32 color_attachments_ = 0)
: device{&device_}, storage_buffer_stages{storage_buffer_stages_},
color_attachments{color_attachments_} {}
bool CanUsePushDescriptor() const noexcept {
return device->IsKhrPushDescriptorSupported() &&
@ -66,6 +79,11 @@ public:
}
vk::DescriptorSetLayout CreateDescriptorSetLayout(bool use_push_descriptor) const {
if (const auto* violation = DescriptorLimitViolation(
device->DescriptorLimits(), std::span{bindings.data(), bindings.size()},
color_attachments)) {
throw Shader::NotImplementedException("{}", violation);
}
if (bindings.empty()) {
return nullptr;
}
@ -161,12 +179,13 @@ private:
});
++binding;
num_descriptors += descriptors[i].count;
offset += sizeof(DescriptorUpdateEntry);
offset += sizeof(DescriptorUpdateEntry) * descriptors[i].count;
}
}
const Device* device{};
VkShaderStageFlags storage_buffer_stages{};
u32 color_attachments{};
bool is_compute{};
boost::container::small_vector<VkDescriptorSetLayoutBinding, 32> bindings;
boost::container::small_vector<VkDescriptorUpdateTemplateEntry, 32> entries;
@ -225,6 +244,7 @@ inline void PushImageDescriptors(TextureCache& texture_cache,
views += num_texture_buffers;
views += num_image_buffers;
for (const auto& desc : info.texture_descriptors) {
bool is_rescaled{};
for (u32 index = 0; index < desc.count; ++index) {
const VideoCommon::ImageViewId image_view_id{(views++)->id};
const VideoCommon::SamplerId sampler_id{*(samplers++)};
@ -236,10 +256,13 @@ inline void PushImageDescriptors(TextureCache& texture_cache,
const VkSampler vk_sampler{use_fallback_sampler ? sampler.HandleWithDefaultAnisotropy()
: sampler.Handle()};
guest_descriptor_queue.AddSampledImage(vk_image_view, vk_sampler);
rescaling.PushTexture(texture_cache.IsRescaling(image_view));
is_rescaled |= texture_cache.IsRescaling(image_view);
}
// SPIR-V allocates one scaling bit per descriptor, including array descriptors.
rescaling.PushTexture(is_rescaled);
}
for (const auto& desc : info.image_descriptors) {
bool is_rescaled{};
for (u32 index = 0; index < desc.count; ++index) {
ImageView& image_view{texture_cache.GetImageView((views++)->id)};
if (desc.is_written) {
@ -247,8 +270,9 @@ inline void PushImageDescriptors(TextureCache& texture_cache,
}
const VkImageView vk_image_view{image_view.StorageView(desc.type, desc.format)};
guest_descriptor_queue.AddImage(vk_image_view);
rescaling.PushImage(texture_cache.IsRescaling(image_view));
is_rescaled |= texture_cache.IsRescaling(image_view);
}
rescaling.PushImage(is_rescaled);
}
}

View File

@ -318,7 +318,7 @@ std::pair<VkBuffer, VkDeviceSize> Uint8Pass::Assemble(u32 num_vertices, VkBuffer
const u32 staging_size = static_cast<u32>(num_vertices * sizeof(u16));
const auto staging = staging_buffer_pool.Request(staging_size, MemoryUsage::DeviceLocal);
compute_pass_descriptor_queue.Acquire();
compute_pass_descriptor_queue.Acquire(2);
compute_pass_descriptor_queue.AddBuffer(src_buffer, src_offset, num_vertices);
compute_pass_descriptor_queue.AddBuffer(staging.buffer, staging.offset, staging_size);
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
@ -376,7 +376,7 @@ std::pair<VkBuffer, VkDeviceSize> QuadIndexedPass::Assemble(
const std::size_t staging_size = num_tri_vertices * sizeof(u32);
const auto staging = staging_buffer_pool.Request(staging_size, MemoryUsage::DeviceLocal);
compute_pass_descriptor_queue.Acquire();
compute_pass_descriptor_queue.Acquire(2);
compute_pass_descriptor_queue.AddBuffer(src_buffer, src_offset, input_size);
compute_pass_descriptor_queue.AddBuffer(staging.buffer, staging.offset, staging_size);
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
@ -417,7 +417,7 @@ void ConditionalRenderingResolvePass::Resolve(VkBuffer dst_buffer, VkBuffer src_
u32 src_offset, bool compare_to_zero) {
const size_t compare_size = compare_to_zero ? 8 : 24;
compute_pass_descriptor_queue.Acquire();
compute_pass_descriptor_queue.Acquire(2);
compute_pass_descriptor_queue.AddBuffer(src_buffer, src_offset, compare_size);
compute_pass_descriptor_queue.AddBuffer(dst_buffer, 0, sizeof(u32));
const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()};
@ -473,7 +473,7 @@ void QueriesPrefixScanPass::Run(VkBuffer accumulation_buffer, VkBuffer dst_buffe
static constexpr size_t DISPATCH_SIZE = 2048U;
size_t runs_to_do = std::min<size_t>(current_runs, DISPATCH_SIZE);
current_runs -= runs_to_do;
compute_pass_descriptor_queue.Acquire();
compute_pass_descriptor_queue.Acquire(2);
compute_pass_descriptor_queue.AddBuffer(src_buffer, 0, number_of_sums * sizeof(u64));
compute_pass_descriptor_queue.AddBuffer(dst_buffer, 0, number_of_sums * sizeof(u64));
compute_pass_descriptor_queue.AddBuffer(accumulation_buffer, 0, sizeof(u64));
@ -580,7 +580,7 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map,
const u32 num_dispatches_y = Common::DivCeil(swizzle.num_tiles.height, 8U);
const u32 num_dispatches_z = image.info.resources.layers;
compute_pass_descriptor_queue.Acquire();
compute_pass_descriptor_queue.Acquire(2);
compute_pass_descriptor_queue.AddBuffer(map.buffer, input_offset,
image.guest_size_bytes - swizzle.buffer_offset);
compute_pass_descriptor_queue.AddImage(image.StorageImageView(swizzle.level));
@ -685,7 +685,7 @@ void MSAACopyPass::CopyImage(Image& dst_image, Image& src_image,
ASSERT(copy.dst_subresource.base_layer == 0);
ASSERT(copy.dst_subresource.num_layers == 1);
compute_pass_descriptor_queue.Acquire();
compute_pass_descriptor_queue.Acquire(2);
compute_pass_descriptor_queue.AddImage(
src_image.StorageImageView(copy.src_subresource.base_level));
compute_pass_descriptor_queue.AddImage(

View File

@ -4,8 +4,6 @@
#include <algorithm>
#include <vector>
#include <boost/container/small_vector.hpp>
#include "yuzu_video_core/renderer_vulkan/pipeline_helper.h"
#include "yuzu_video_core/renderer_vulkan/pipeline_statistics.h"
#include "yuzu_video_core/renderer_vulkan/vk_buffer_cache.h"
@ -34,6 +32,12 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
: device{device_},
pipeline_cache(pipeline_cache_), guest_descriptor_queue{guest_descriptor_queue_}, info{info_},
spv_module(std::move(spv_module_)) {
num_descriptor_entries = NumDescriptorUpdateEntries(info);
num_textures = Shader::NumDescriptors(info.texture_descriptors);
descriptor_views.resize(num_textures + Shader::NumDescriptors(info.texture_buffer_descriptors) +
Shader::NumDescriptors(info.image_buffer_descriptors) +
Shader::NumDescriptors(info.image_descriptors));
descriptor_samplers.resize(num_textures);
if (shader_notify) {
shader_notify->MarkShaderBuilding();
}
@ -104,7 +108,7 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
Tegra::MemoryManager& gpu_memory, Scheduler& scheduler,
BufferCache& buffer_cache, TextureCache& texture_cache) {
guest_descriptor_queue.Acquire();
guest_descriptor_queue.Acquire(num_descriptor_entries);
buffer_cache.SetComputeUniformBufferState(info.constant_buffer_mask, &uniform_buffer_sizes);
buffer_cache.UnbindComputeStorageBuffers();
@ -118,40 +122,61 @@ void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
texture_cache.SynchronizeComputeDescriptors();
static constexpr size_t max_elements = 64;
boost::container::static_vector<VideoCommon::ImageViewInOut, max_elements> views;
boost::container::static_vector<VideoCommon::SamplerId, max_elements> samplers;
auto& views = descriptor_views;
auto& samplers = descriptor_samplers;
size_t view_index{};
size_t sampler_index{};
if (num_textures > 64) {
if (!sampler_lookup) {
sampler_lookup = std::make_unique<DrawDescriptorCache<VideoCommon::SamplerId>>();
}
sampler_lookup->Reset();
}
const bool memoize_samplers = sampler_lookup && sampler_lookup->Enabled();
const auto lookup_sampler = [&](u32 index) {
return texture_cache.GetComputeSamplerId(index);
};
const auto& qmd{kepler_compute.launch_description};
const auto& cbufs{qmd.const_buffer_config};
DescriptorCbufReader<8> cbuf_reader;
const auto bind_cbuf = [&](u32 bank, u32 count) {
return cbuf_reader.Bind(gpu_memory, bank, cbufs[bank].Address(), cbufs[bank].size.Value(), count);
};
const bool via_header_index{qmd.linked_tsc != 0};
const auto read_handle{[&](const auto& desc, u32 index) {
const auto prepare_handle{[&](const auto& desc) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
const GPUVAddr addr{cbufs[desc.cbuf_index].Address() + offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
const auto primary = bind_cbuf(desc.cbuf_index, desc.count);
auto secondary = primary;
bool has_secondary{};
u32 secondary_offset{}, primary_shift{}, secondary_shift{};
if constexpr (requires { desc.has_secondary; }) {
if (desc.has_secondary) {
ASSERT(((qmd.const_buffer_enable_mask >> desc.secondary_cbuf_index) & 1) != 0);
const u32 secondary_offset{desc.secondary_cbuf_offset + index_offset};
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].Address() +
secondary_offset};
const u32 lhs_raw{gpu_memory.Read<u32>(addr) << desc.shift_left};
const u32 rhs_raw{gpu_memory.Read<u32>(separate_addr) << desc.secondary_shift_left};
return TexturePair(lhs_raw | rhs_raw, via_header_index);
secondary = bind_cbuf(desc.secondary_cbuf_index, desc.count);
has_secondary = true;
secondary_offset = desc.secondary_cbuf_offset;
primary_shift = desc.shift_left;
secondary_shift = desc.secondary_shift_left;
}
}
return TexturePair(gpu_memory.Read<u32>(addr), via_header_index);
return [=](u32 index) {
const u32 delta = index << desc.size_shift;
const u32 raw = primary.Read(desc.cbuf_offset + delta);
return TexturePair(has_secondary ? (raw << primary_shift) |
(secondary.Read(secondary_offset + delta) << secondary_shift) : raw,
via_header_index);
};
}};
const auto add_image{[&](const auto& desc, bool blacklist) {
const auto read_handle = prepare_handle(desc);
for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)};
views.push_back({
const auto handle{read_handle(index)};
views[view_index++] = {
.index = handle.first,
.blacklist = blacklist,
.id = {},
});
};
}
}};
for (const auto& desc : info.texture_buffer_descriptors) {
@ -161,17 +186,22 @@ void ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
add_image(desc, false);
}
for (const auto& desc : info.texture_descriptors) {
const auto read_handle = prepare_handle(desc);
for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)};
views.push_back({handle.first});
const auto handle{read_handle(index)};
views[view_index++] = {handle.first};
VideoCommon::SamplerId sampler = texture_cache.GetComputeSamplerId(handle.second);
samplers.push_back(sampler);
const VideoCommon::SamplerId sampler = memoize_samplers
? sampler_lookup->Get(handle.second, lookup_sampler)
: lookup_sampler(handle.second);
samplers[sampler_index++] = sampler;
}
}
for (const auto& desc : info.image_descriptors) {
add_image(desc, desc.is_written);
}
ASSERT(view_index == views.size());
ASSERT(sampler_index == samplers.size());
texture_cache.FillComputeImageViews(std::span(views.data(), views.size()));
buffer_cache.UnbindComputeTextureBuffers();

View File

@ -5,11 +5,15 @@
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <boost/container/small_vector.hpp>
#include "yuzu_common/common_types.h"
#include "yuzu_common/thread_worker.h"
#include "yuzu_shader_recompiler/shader_info.h"
#include "yuzu_video_core/renderer_vulkan/descriptor_lookup.h"
#include "yuzu_video_core/renderer_vulkan/vk_buffer_cache.h"
#include "yuzu_video_core/renderer_vulkan/vk_descriptor_pool.h"
#include "yuzu_video_core/renderer_vulkan/vk_texture_cache.h"
@ -55,6 +59,12 @@ private:
vk::PipelineCache& pipeline_cache;
GuestDescriptorQueue& guest_descriptor_queue;
Shader::Info info;
u32 num_textures{};
size_t num_descriptor_entries{};
boost::container::small_vector<VideoCommon::ImageViewInOut, 64> descriptor_views;
boost::container::small_vector<VideoCommon::SamplerId, 64> descriptor_samplers;
std::unique_ptr<DrawDescriptorCache<VideoCommon::SamplerId>> sampler_lookup;
VideoCommon::ComputeUniformBufferSizes uniform_buffer_sizes{};

View File

@ -4,7 +4,6 @@
#include <algorithm>
#include <span>
#include <boost/container/small_vector.hpp>
#include <boost/container/static_vector.hpp>
#include "yuzu_video_core/renderer_vulkan/pipeline_helper.h"
@ -33,7 +32,6 @@
namespace Vulkan {
namespace {
using boost::container::small_vector;
using boost::container::static_vector;
using Shader::ImageBufferDescriptor;
using Shader::Backend::SPIRV::RENDERAREA_LAYOUT_OFFSET;
@ -45,7 +43,7 @@ using VideoCore::Surface::PixelFormatFromDepthFormat;
using VideoCore::Surface::PixelFormatFromRenderTargetFormat;
constexpr size_t NUM_STAGES = Maxwell::MaxShaderStage;
constexpr size_t MAX_IMAGE_ELEMENTS = 64;
constexpr size_t INLINE_IMAGE_ELEMENTS = 64;
DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span<const Shader::Info> infos,
u32 num_color_attachments) {
@ -92,14 +90,19 @@ DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span<const Shader
if (stages.at(index) == VK_SHADER_STAGE_FRAGMENT_BIT) {
// Color attachments also count against maxPerStageResources.
resources += num_color_attachments;
const u64 storage_images =
u64{Shader::NumDescriptors(infos[index].image_descriptors)} +
Shader::NumDescriptors(infos[index].image_buffer_descriptors);
can_widen = storage_images + num_storage_buffers + num_color_attachments <=
device.DescriptorLimits().maxFragmentCombinedOutputResources;
}
can_widen = resources <= device.GetMaxPerStageResources();
can_widen &= resources <= device.GetMaxPerStageResources();
}
if (!can_widen) {
storage_buffer_stages = 0;
}
}
DescriptorLayoutBuilder builder{device, storage_buffer_stages};
DescriptorLayoutBuilder builder{device, storage_buffer_stages, num_color_attachments};
for (size_t index = 0; index < infos.size(); ++index) {
builder.Add(infos[index], stages.at(index));
}
@ -302,6 +305,7 @@ struct GraphicsPipeline::VtgState {
Shader::Info info;
/// Shader information of the resources bound by the guest
Shader::Info guest_info;
size_t num_descriptor_entries{};
vk::ShaderModule module;
vk::DescriptorSetLayout descriptor_set_layout;
DescriptorAllocator descriptor_allocator;
@ -370,6 +374,11 @@ GraphicsPipeline::GraphicsPipeline(
enabled_uniform_buffer_masks[stage] = info->constant_buffer_mask;
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
num_textures += Shader::NumDescriptors(info->texture_descriptors);
num_descriptor_entries += NumDescriptorUpdateEntries(*info);
num_image_elements += Shader::NumDescriptors(info->texture_buffer_descriptors);
num_image_elements += Shader::NumDescriptors(info->image_buffer_descriptors);
num_image_elements += Shader::NumDescriptors(info->texture_descriptors);
num_image_elements += Shader::NumDescriptors(info->image_descriptors);
}
if (vtg_info) {
static constexpr std::array<size_t, VtgPipelineInfo::NUM_COMPUTE_STAGES> compute_stages{
@ -382,8 +391,15 @@ GraphicsPipeline::GraphicsPipeline(
for (size_t index = 0; index < vtg->stages.size(); ++index) {
VtgState::Stage& stage{vtg->stages[index]};
stage.info = vtg_info->infos[index];
stage.num_descriptor_entries = NumDescriptorUpdateEntries(stage.info);
stage.guest_info = stage.info;
stage.guest_info.storage_buffers_descriptors.resize(guest_storage_buffers[index]);
// The compute stages retain guest images absent from the generated vertex shader.
num_textures += Shader::NumDescriptors(stage.guest_info.texture_descriptors);
num_image_elements += Shader::NumDescriptors(stage.guest_info.texture_buffer_descriptors);
num_image_elements += Shader::NumDescriptors(stage.guest_info.image_buffer_descriptors);
num_image_elements += Shader::NumDescriptors(stage.guest_info.texture_descriptors);
num_image_elements += Shader::NumDescriptors(stage.guest_info.image_descriptors);
stage.module = std::move(vtg_info->modules[index]);
vtg->uniform_masks[compute_stages[index]] = stage.info.constant_buffer_mask;
std::ranges::copy(stage.info.constant_buffer_used_sizes,
@ -397,6 +413,8 @@ GraphicsPipeline::GraphicsPipeline(
vtg->scratch = vtg_info->scratch;
vtg->staging_pool = vtg_info->staging_pool;
}
descriptor_views.resize(num_image_elements);
descriptor_samplers.resize(num_textures);
// Number of leading components the vertex shader may read from each attribute. Used to keep
// vertex formats within their binding stride without discarding data the shader reads.
const Shader::Info& vertex_info{stage_infos[0]};
@ -505,13 +523,24 @@ void GraphicsPipeline::AddTransition(GraphicsPipeline* transition,
template <typename Spec>
void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
std::array<VideoCommon::ImageViewInOut, MAX_IMAGE_ELEMENTS> views;
std::array<VideoCommon::SamplerId, MAX_IMAGE_ELEMENTS> samplers;
size_t sampler_index{};
auto& views = descriptor_views;
auto& samplers = descriptor_samplers;
size_t view_index{};
size_t sampler_index{};
texture_cache.SynchronizeGraphicsDescriptors();
if (num_textures > INLINE_IMAGE_ELEMENTS) {
if (!sampler_lookup) {
sampler_lookup = std::make_unique<DrawDescriptorCache<VideoCommon::SamplerId>>();
}
sampler_lookup->Reset();
}
const bool memoize_samplers = sampler_lookup && sampler_lookup->Enabled();
const auto lookup_sampler = [&](u32 index) {
return texture_cache.GetGraphicsSamplerId(index);
};
buffer_cache.SetUniformBuffersState(enabled_uniform_buffer_masks, &uniform_buffer_sizes);
const auto& regs{maxwell3d->regs};
@ -529,30 +558,38 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
DescriptorCbufReader<18> cbuf_reader;
const auto bind_cbuf = [&](u32 bank, u32 count) {
return cbuf_reader.Bind(*gpu_memory, bank, cbufs[bank].address, cbufs[bank].size, count);
};
const auto prepare_handle{[&](const auto& desc) {
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
const GPUVAddr addr{cbufs[desc.cbuf_index].address + offset};
if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&> ||
std::is_same_v<decltype(desc), const Shader::TextureBufferDescriptor&>) {
const auto primary = bind_cbuf(desc.cbuf_index, desc.count);
auto secondary = primary;
bool has_secondary{};
u32 secondary_offset{}, primary_shift{}, secondary_shift{};
if constexpr (requires { desc.has_secondary; }) {
if (desc.has_secondary) {
ASSERT(cbufs[desc.secondary_cbuf_index].enabled);
const u32 second_offset{desc.secondary_cbuf_offset + index_offset};
const GPUVAddr separate_addr{cbufs[desc.secondary_cbuf_index].address +
second_offset};
const u32 lhs_raw{gpu_memory->Read<u32>(addr) << desc.shift_left};
const u32 rhs_raw{gpu_memory->Read<u32>(separate_addr)
<< desc.secondary_shift_left};
const u32 raw{lhs_raw | rhs_raw};
return TexturePair(raw, via_header_index);
secondary = bind_cbuf(desc.secondary_cbuf_index, desc.count);
has_secondary = true;
secondary_offset = desc.secondary_cbuf_offset;
primary_shift = desc.shift_left;
secondary_shift = desc.secondary_shift_left;
}
}
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
return [=](u32 index) {
const u32 delta = index << desc.size_shift;
const u32 raw = primary.Read(desc.cbuf_offset + delta);
return TexturePair(has_secondary ? (raw << primary_shift) |
(secondary.Read(secondary_offset + delta) << secondary_shift) : raw,
via_header_index);
};
}};
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
const auto read_handle = prepare_handle(desc);
for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)};
const auto handle{read_handle(index)};
views[view_index++] = {
.index = handle.first,
.blacklist = blacklist,
@ -571,11 +608,14 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
}
for (const auto& desc : info.texture_descriptors) {
const auto read_handle = prepare_handle(desc);
for (u32 index = 0; index < desc.count; ++index) {
const auto handle{read_handle(desc, index)};
const auto handle{read_handle(index)};
views[view_index++] = {handle.first};
VideoCommon::SamplerId sampler{texture_cache.GetGraphicsSamplerId(handle.second)};
const VideoCommon::SamplerId sampler = memoize_samplers
? sampler_lookup->Get(handle.second, lookup_sampler)
: lookup_sampler(handle.second);
samplers[sampler_index++] = sampler;
}
}
@ -600,7 +640,9 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
if constexpr (Spec::enabled_stages[4]) {
config_stage(4);
}
texture_cache.FillGraphicsImageViews<Spec::has_images>(std::span(views.data(), view_index));
ASSERT(view_index == views.size());
ASSERT(sampler_index == samplers.size());
texture_cache.FillGraphicsImageViews<Spec::has_images>(std::span(views.data(), views.size()));
VideoCommon::ImageViewInOut* texture_buffer_it{views.data()};
const auto bind_stage_info{[&](size_t stage) LAMBDA_FORCEINLINE {
@ -657,7 +699,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
buffer_cache.UpdateGraphicsBuffers(is_indexed);
buffer_cache.BindHostGeometryBuffers(is_indexed);
guest_descriptor_queue.Acquire();
guest_descriptor_queue.Acquire(num_descriptor_entries);
RescalingPushConstant rescaling;
RenderAreaPushConstant render_area;
@ -704,8 +746,8 @@ void GraphicsPipeline::ConfigureVtg(bool is_indexed) {
VtgState& state{*vtg};
state.index_count = 0;
std::array<VideoCommon::ImageViewInOut, MAX_IMAGE_ELEMENTS> views;
std::array<VideoCommon::SamplerId, MAX_IMAGE_ELEMENTS> samplers;
auto& views = descriptor_views;
auto& samplers = descriptor_samplers;
size_t sampler_index{};
size_t view_index{};
@ -779,7 +821,9 @@ void GraphicsPipeline::ConfigureVtg(bool is_indexed) {
add_image(desc, desc.is_written);
}
}
texture_cache.FillGraphicsImageViews<true>(std::span(views.data(), view_index));
ASSERT(view_index == views.size());
ASSERT(sampler_index == samplers.size());
texture_cache.FillGraphicsImageViews<true>(std::span(views.data(), views.size()));
VideoCommon::ImageViewInOut* texture_buffer_it{views.data()};
for (const auto& [stage, info_pointer] : stages) {
@ -932,7 +976,7 @@ void GraphicsPipeline::ConfigureVtg(bool is_indexed) {
const auto run_stage{[&](size_t index, size_t guest_stage,
std::span<const VtgState::Buffer> buffers, u32 invocations) {
VtgState::Stage& stage{state.stages[index]};
guest_descriptor_queue.Acquire();
guest_descriptor_queue.Acquire(stage.num_descriptor_entries);
buffer_cache.BindHostStageUniformAndStorageBuffers(guest_stage);
for (const VtgState::Buffer& buffer : buffers) {
guest_descriptor_queue.AddBuffer(buffer.buffer, buffer.offset, buffer.size);
@ -1010,7 +1054,7 @@ void GraphicsPipeline::ConfigureVtg(bool is_indexed) {
run_stage(VtgPipelineInfo::GEOMETRY, 3, geometry_buffers, geometry_invocations);
// The generated vertex shader reads the vertices written by the geometry stage
guest_descriptor_queue.Acquire();
guest_descriptor_queue.Acquire(num_descriptor_entries);
guest_descriptor_queue.AddBuffer(geometry_buffer, geometry_offset, geometry_size);
RescalingPushConstant rescaling;
@ -1551,7 +1595,15 @@ void GraphicsPipeline::Validate() {
num_images += Shader::NumDescriptors(info.texture_descriptors);
num_images += Shader::NumDescriptors(info.image_descriptors);
}
ASSERT(num_images <= MAX_IMAGE_ELEMENTS);
if (vtg) {
for (const auto& stage : vtg->stages) {
num_images += Shader::NumDescriptors(stage.guest_info.texture_buffer_descriptors);
num_images += Shader::NumDescriptors(stage.guest_info.image_buffer_descriptors);
num_images += Shader::NumDescriptors(stage.guest_info.texture_descriptors);
num_images += Shader::NumDescriptors(stage.guest_info.image_descriptors);
}
}
ASSERT(num_images == num_image_elements);
}
} // namespace Vulkan

View File

@ -13,11 +13,14 @@
#include <type_traits>
#include <utility>
#include <boost/container/small_vector.hpp>
#include "yuzu_common/thread_worker.h"
#include "yuzu_shader_recompiler/shader_info.h"
#include "yuzu_shader_recompiler/vtg_as_compute.h"
#include "yuzu_video_core/engines/maxwell_3d.h"
#include "yuzu_video_core/renderer_vulkan/fixed_pipeline_state.h"
#include "yuzu_video_core/renderer_vulkan/descriptor_lookup.h"
#include "yuzu_video_core/renderer_vulkan/vk_buffer_cache.h"
#include "yuzu_video_core/renderer_vulkan/vk_descriptor_pool.h"
#include "yuzu_video_core/renderer_vulkan/vk_texture_cache.h"
@ -31,6 +34,7 @@ namespace Vulkan {
struct GraphicsPipelineCacheKey {
std::array<u64, 6> unique_hashes;
std::array<std::array<u32, 18>, 5> cbuf_sizes;
FixedPipelineState state;
size_t Hash() const noexcept;
@ -42,7 +46,7 @@ struct GraphicsPipelineCacheKey {
}
size_t Size() const noexcept {
return sizeof(unique_hashes) + state.Size();
return sizeof(unique_hashes) + sizeof(cbuf_sizes) + state.Size();
}
};
static_assert(std::has_unique_object_representations_v<GraphicsPipelineCacheKey>);
@ -219,6 +223,13 @@ private:
std::array<u32, 5> enabled_uniform_buffer_masks{};
VideoCommon::UniformBufferSizes uniform_buffer_sizes{};
u32 num_textures{};
u32 num_image_elements{};
size_t num_descriptor_entries{};
// Used only by the GPU thread; queued commands use GuestDescriptorQueue's own copies.
boost::container::small_vector<VideoCommon::ImageViewInOut, 64> descriptor_views;
boost::container::small_vector<VideoCommon::SamplerId, 64> descriptor_samplers;
std::unique_ptr<DrawDescriptorCache<VideoCommon::SamplerId>> sampler_lookup;
vk::DescriptorSetLayout descriptor_set_layout;
DescriptorAllocator descriptor_allocator;

View File

@ -6,6 +6,8 @@
#include <fstream>
#include <memory>
#include <thread>
#include <unordered_set>
#include "yuzu_common/scope_exit.h"
#include <vector>
#include "yuzu_common/bit_cast.h"
@ -54,7 +56,7 @@ using VideoCommon::FileEnvironment;
using VideoCommon::GenericEnvironment;
using VideoCommon::GraphicsEnvironment;
constexpr u32 CACHE_VERSION = 11;
constexpr u32 CACHE_VERSION = 14;
constexpr std::array<char, 8> VULKAN_CACHE_MAGIC_NUMBER{'y', 'u', 'z', 'u', 'v', 'k', 'c', 'h'};
template <typename Container>
@ -471,6 +473,8 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
.supported_spirv = device.SupportedSpirvVersion(),
.unified_descriptor_binding = true,
.support_descriptor_aliasing = device.IsDescriptorAliasingSupported(),
.support_sampled_image_array_nonuniform_indexing =
device.IsSampledImageArrayNonUniformIndexingSupported(),
.disable_uniform_buffer_descriptor_aliasing =
device.IsDescriptorAliasingSupported() && device.GetDriverID() == VK_DRIVER_ID_MOLTENVK,
.support_int8 = device.IsInt8Supported(),
@ -548,6 +552,7 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
.min_ssbo_alignment = static_cast<u32>(device.GetStorageBufferAlignment()),
.support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(),
.support_conditional_barrier = device.SupportsConditionalBarriers(),
.max_bindless_descriptors_per_stage = device.MaxBindlessDescriptorsPerStage(),
};
if (device.GetMaxVertexInputAttributes() < Maxwell::NumVertexAttributes) {
@ -584,6 +589,11 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
return nullptr;
}
graphics_key.state.Refresh(*maxwell3d, dynamic_features);
graphics_cbuf_size_state.Refresh(graphics_key.cbuf_sizes, graphics_key.unique_hashes,
cbuf_size_dependencies, [&](size_t stage, size_t bank) {
const auto& buffer = maxwell3d->state.shader_stages[stage].const_buffers[bank];
return buffer.enabled ? buffer.size : 0;
});
if (current_pipeline) {
GraphicsPipeline* const next{current_pipeline->Next(graphics_key)};
@ -601,11 +611,17 @@ ComputePipeline* PipelineCache::CurrentComputePipeline() {
return nullptr;
}
const auto& qmd{kepler_compute->launch_description};
const ComputePipelineCacheKey key{
ComputePipelineCacheKey key{
.unique_hash = shader->unique_hash,
.shared_memory_size = qmd.shared_alloc,
.workgroup_size{qmd.block_dim_x, qmd.block_dim_y, qmd.block_dim_z},
};
const u32 size_mask = qmd.const_buffer_enable_mask.Value() &
cbuf_size_dependencies.Mask(shader->unique_hash);
for (size_t bank = 0; bank < key.cbuf_sizes.size(); ++bank) {
key.cbuf_sizes[bank] = ((size_mask >> bank) & 1) != 0
? qmd.const_buffer_config[bank].size.Value() : 0;
}
const auto [pair, is_new]{compute_cache.try_emplace(key)};
auto& pipeline{pair->second};
if (!is_new) {
@ -641,14 +657,29 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
bool has_loaded{};
std::unique_ptr<PipelineStatistics> statistics;
} state;
// Cache parsing/allocation can throw after work capturing this stack state is queued.
// Drain active work before state and the progress callback can leave scope.
SCOPE_EXIT { workers.WaitForRequests(); };
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
state.statistics = std::make_unique<PipelineStatistics>(device);
}
// Earlier records included sizes for every bound bank, even when the shader never
// queried them. Normalize and deduplicate them without discarding the existing shader cache.
std::unordered_set<ComputePipelineCacheKey> loaded_compute_keys;
std::unordered_set<GraphicsPipelineCacheKey> loaded_graphics_keys;
const auto load_compute{[&](std::ifstream& file, FileEnvironment env) {
ComputePipelineCacheKey key;
file.read(reinterpret_cast<char*>(&key), sizeof(key));
cbuf_size_dependencies.Observe(key.unique_hash, env.CbufSizeMask());
CbufSizeDependencies::Normalize(key.cbuf_sizes,
cbuf_size_dependencies.Mask(key.unique_hash));
if (!loaded_compute_keys.insert(key).second) {
return;
}
workers.QueueWork([this, key, env_ = std::move(env), &state, &callback]() mutable {
ShaderPools pools;
auto pipeline{CreateComputePipeline(pools, key, env_, state.statistics.get(), false)};
@ -667,7 +698,7 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
std::vector<FileEnvironment> envs) {
workers.QueueWork([this, key, envs_ = std::move(envs), &state, &callback]() mutable {
ShaderPools pools;
boost::container::static_vector<Shader::Environment*, 5> env_ptrs;
boost::container::static_vector<Shader::Environment*, Maxwell::MaxShaderProgram> env_ptrs;
for (auto& env : envs_) {
env_ptrs.push_back(&env);
}
@ -697,6 +728,29 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
GraphicsPipelineCacheKey key;
file.read(reinterpret_cast<char*>(&key), sizeof(key));
size_t env_index{};
for (u64 hash : key.unique_hashes) {
if (hash != 0) {
if (env_index >= envs.size()) {
LOG_ERROR(Render_Vulkan, "Cached graphics shader environment count mismatch");
return;
}
cbuf_size_dependencies.Observe(hash, envs[env_index++].CbufSizeMask());
}
}
if (env_index != envs.size()) {
LOG_ERROR(Render_Vulkan, "Cached graphics shader environment count mismatch");
return;
}
for (size_t stage = 0; stage < key.cbuf_sizes.size(); ++stage) {
const u32 mask = cbuf_size_dependencies.Mask(key.unique_hashes[stage + 1]) |
(stage == 0 ? cbuf_size_dependencies.Mask(key.unique_hashes[0]) : 0);
CbufSizeDependencies::Normalize(key.cbuf_sizes[stage], mask);
}
if (!loaded_graphics_keys.insert(key).second) {
return;
}
if ((key.state.extended_dynamic_state != 0) !=
dynamic_features.has_extended_dynamic_state ||
(key.state.extended_dynamic_state_2 != 0) !=
@ -1061,6 +1115,10 @@ std::shared_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() {
std::shared_ptr<GraphicsPipeline> pipeline{
CreateGraphicsPipeline(main_pools, graphics_key, environments.Span(), nullptr, true,
is_hle && twin ? &skip_build : nullptr)};
for (size_t index = 0; index < graphics_key.unique_hashes.size(); ++index) {
cbuf_size_dependencies.Observe(graphics_key.unique_hashes[index],
environments.envs[index].CbufSizeMask());
}
if (reuse_twin) {
twin->SetSharedKey(graphics_key);
pipeline = twin;
@ -1092,6 +1150,7 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
main_pools.ReleaseContents();
auto pipeline{CreateComputePipeline(main_pools, key, env, nullptr, true)};
cbuf_size_dependencies.Observe(key.unique_hash, env.CbufSizeMask());
if (!pipeline || pipeline_cache_filename.empty()) {
return pipeline;
}

View File

@ -24,6 +24,7 @@
#include "yuzu_video_core/engines/maxwell_3d.h"
#include "yuzu_video_core/host1x/gpu_device_memory_manager.h"
#include "yuzu_video_core/renderer_vulkan/fixed_pipeline_state.h"
#include "yuzu_video_core/renderer_vulkan/cbuf_size_dependencies.h"
#include "yuzu_video_core/renderer_vulkan/vk_buffer_cache.h"
#include "yuzu_video_core/renderer_vulkan/vk_compute_pipeline.h"
#include "yuzu_video_core/renderer_vulkan/vk_graphics_pipeline.h"
@ -50,6 +51,7 @@ struct ComputePipelineCacheKey {
u64 unique_hash;
u32 shared_memory_size;
std::array<u32, 3> workgroup_size;
std::array<u32, 8> cbuf_sizes;
size_t Hash() const noexcept;
@ -159,6 +161,8 @@ private:
GraphicsPipelineCacheKey graphics_key{};
GraphicsPipeline* current_pipeline{};
CbufSizeDependencies cbuf_size_dependencies;
GraphicsCbufSizeState<5, 18> graphics_cbuf_size_state;
std::unordered_map<ComputePipelineCacheKey, std::unique_ptr<ComputePipeline>> compute_cache;
// Shared: an HLE macro key without replacements maps to the pipeline of its non-HLE twin.

View File

@ -13,32 +13,19 @@
namespace Vulkan {
UpdateDescriptorQueue::UpdateDescriptorQueue(const Device& device_, Scheduler& scheduler_)
: device{device_}, scheduler{scheduler_} {
payload_start = payload.data();
payload_cursor = payload.data();
}
: device{device_}, scheduler{scheduler_} {}
UpdateDescriptorQueue::~UpdateDescriptorQueue() = default;
void UpdateDescriptorQueue::TickFrame() {
if (++frame_index >= FRAMES_IN_FLIGHT) {
frame_index = 0;
}
payload_start = payload.data() + frame_index * FRAME_PAYLOAD_SIZE;
payload_cursor = payload_start;
payload.TickFrame();
}
void UpdateDescriptorQueue::Acquire() {
// Minimum number of entries required.
// This is the maximum number of entries a single draw call might use.
static constexpr size_t MIN_ENTRIES = 0x400;
if (std::distance(payload_start, payload_cursor) + MIN_ENTRIES >= FRAME_PAYLOAD_SIZE) {
LOG_WARNING(Render_Vulkan, "Payload overflow, waiting for worker thread");
void UpdateDescriptorQueue::Acquire(size_t num_entries) {
payload.Acquire(num_entries, [&] {
LOG_DEBUG(Render_Vulkan, "Recycling descriptor payload after waiting for worker");
scheduler.WaitWorker();
payload_cursor = payload_start;
}
upload_start = payload_cursor;
});
}
} // namespace Vulkan

View File

@ -3,7 +3,7 @@
#pragma once
#include <array>
#include "yuzu_video_core/renderer_vulkan/descriptor_payload.h"
#include "yuzu_video_core/vulkan_common/vulkan_wrapper.h"
@ -33,7 +33,6 @@ class UpdateDescriptorQueue final {
// provide up to 3 swapchain images.
static constexpr size_t FRAMES_IN_FLIGHT = 8;
static constexpr size_t FRAME_PAYLOAD_SIZE = 0x20000;
static constexpr size_t PAYLOAD_SIZE = FRAME_PAYLOAD_SIZE * FRAMES_IN_FLIGHT;
public:
explicit UpdateDescriptorQueue(const Device& device_, Scheduler& scheduler_);
@ -41,49 +40,46 @@ public:
void TickFrame();
void Acquire();
void Acquire(size_t num_entries);
const DescriptorUpdateEntry* UpdateData() const noexcept {
return upload_start;
return payload.Data();
}
void AddSampledImage(VkImageView image_view, VkSampler sampler) {
*(payload_cursor++) = VkDescriptorImageInfo{
payload.Append(VkDescriptorImageInfo{
.sampler = sampler,
.imageView = image_view,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
};
});
}
void AddImage(VkImageView image_view) {
*(payload_cursor++) = VkDescriptorImageInfo{
payload.Append(VkDescriptorImageInfo{
.sampler = VK_NULL_HANDLE,
.imageView = image_view,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL,
};
});
}
void AddBuffer(VkBuffer buffer, VkDeviceSize offset, VkDeviceSize size) {
*(payload_cursor++) = VkDescriptorBufferInfo{
payload.Append(VkDescriptorBufferInfo{
.buffer = buffer,
.offset = offset,
.range = size,
};
});
}
void AddTexelBuffer(VkBufferView texel_buffer) {
*(payload_cursor++) = texel_buffer;
payload.Append(texel_buffer);
}
private:
const Device& device;
Scheduler& scheduler;
size_t frame_index{0};
DescriptorUpdateEntry* payload_cursor = nullptr;
DescriptorUpdateEntry* payload_start = nullptr;
const DescriptorUpdateEntry* upload_start = nullptr;
std::array<DescriptorUpdateEntry, PAYLOAD_SIZE> payload;
DescriptorPayload<DescriptorUpdateEntry, FRAME_PAYLOAD_SIZE, FRAMES_IN_FLIGHT> payload;
};
// TODO: should these be separate classes instead?

View File

@ -35,6 +35,25 @@ static u64 MakeCbufKey(u32 index, u32 offset) {
return (static_cast<u64>(index) << 32) | offset;
}
static u32 CbufSizeQueryMask(const std::unordered_map<u64, u32>& values) {
u32 mask{};
for (const auto& [key, value] : values) {
const u32 bank = static_cast<u32>(key >> 32);
if (static_cast<u32>(key) == UINT32_MAX && bank < 32) {
mask |= 1U << bank;
}
}
return mask;
}
u32 GenericEnvironment::CbufSizeMask() const noexcept {
return CbufSizeQueryMask(cbuf_values);
}
u32 FileEnvironment::CbufSizeMask() const noexcept {
return CbufSizeQueryMask(cbuf_values);
}
static Shader::TextureType ConvertTextureType(
const std::optional<Tegra::Texture::TICEntry>& optional_entry) {
if (!optional_entry) {
@ -341,6 +360,14 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_,
maxwell3d->engine_state == Tegra::Engines::Maxwell3D::EngineHint::OnHLEMacro;
}
u32 GraphicsEnvironment::ReadCbufSize(u32 cbuf_index) {
const auto& cbuf{maxwell3d->state.shader_stages[stage_index].const_buffers[cbuf_index]};
const u32 size = cbuf.enabled ? cbuf.size : 0;
// Reserved offset outside the guest address space; keeps cache layout unchanged.
cbuf_values.emplace(MakeCbufKey(cbuf_index, UINT32_MAX), size);
return size;
}
u32 GraphicsEnvironment::ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) {
const auto& cbuf{maxwell3d->state.shader_stages[stage_index].const_buffers[cbuf_index]};
ASSERT(cbuf.enabled);
@ -423,6 +450,14 @@ ComputeEnvironment::ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_com
workgroup_size = {qmd.block_dim_x, qmd.block_dim_y, qmd.block_dim_z};
}
u32 ComputeEnvironment::ReadCbufSize(u32 cbuf_index) {
const auto& qmd{kepler_compute->launch_description};
const bool enabled = ((qmd.const_buffer_enable_mask.Value() >> cbuf_index) & 1) != 0;
const u32 size = enabled ? qmd.const_buffer_config[cbuf_index].size.Value() : 0;
cbuf_values.emplace(MakeCbufKey(cbuf_index, UINT32_MAX), size);
return size;
}
u32 ComputeEnvironment::ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) {
const auto& qmd{kepler_compute->launch_description};
ASSERT(((qmd.const_buffer_enable_mask.Value() >> cbuf_index) & 1) != 0);
@ -482,6 +517,33 @@ void FileEnvironment::Deserialize(std::ifstream& file) {
.read(reinterpret_cast<char*>(&read_highest), sizeof(read_highest))
.read(reinterpret_cast<char*>(&viewport_transform_state), sizeof(viewport_transform_state))
.read(reinterpret_cast<char*>(&stage), sizeof(stage));
// A crash can leave the last cache entry incomplete. Validate serialized sizes before
// allocating memory; otherwise arbitrary bytes can turn into a huge allocation request.
if (!file || stage > Shader::Stage::VertexA || code_size % sizeof(u64) != 0) {
throw std::ios_base::failure("Invalid shader cache environment header");
}
const auto position = file.tellg();
file.seekg(0, std::ios::end);
const auto end = file.tellg();
file.seekg(position);
if (!file || position < 0 || end < position) {
throw std::ios_base::failure("Invalid shader cache file bounds");
}
u64 remaining = static_cast<u64>(end - position);
if (code_size > remaining) {
throw std::ios_base::failure("Invalid shader cache code size");
}
remaining -= code_size;
const auto consume_entries = [&remaining](u64 count, size_t entry_size) {
if (count > remaining / entry_size) {
throw std::ios_base::failure("Invalid shader cache entry count");
}
remaining -= count * entry_size;
};
consume_entries(num_texture_types, sizeof(u32) + sizeof(Shader::TextureType));
consume_entries(num_texture_pixel_formats, sizeof(u32) + sizeof(Shader::TexturePixelFormat));
consume_entries(num_cbuf_values, sizeof(u64) + sizeof(u32));
consume_entries(num_cbuf_replacement_values, sizeof(u64) + sizeof(Shader::ReplaceConstant));
code.resize(Common::DivCeil(code_size, sizeof(u64)));
file.read(reinterpret_cast<char*>(code.data()), code_size);
for (size_t i = 0; i < num_texture_types; ++i) {
@ -523,6 +585,9 @@ void FileEnvironment::Deserialize(std::ifstream& file) {
file.read(reinterpret_cast<char*>(&gp_passthrough_mask), sizeof(gp_passthrough_mask));
}
}
if (!file) {
throw std::ios_base::failure("Truncated shader cache environment");
}
is_proprietary_driver = texture_bound == 2;
}
@ -537,6 +602,11 @@ u64 FileEnvironment::ReadInstruction(u32 address) {
return code[(address - read_lowest) / sizeof(u64)];
}
u32 FileEnvironment::ReadCbufSize(u32 cbuf_index) {
const auto it = cbuf_values.find(MakeCbufKey(cbuf_index, UINT32_MAX));
return it != cbuf_values.end() ? it->second : 0;
}
u32 FileEnvironment::ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) {
const auto it{cbuf_values.find(MakeCbufKey(cbuf_index, cbuf_offset))};
if (it == cbuf_values.end()) {
@ -666,11 +736,17 @@ void LoadPipelines(
}
u32 num_envs{};
file.read(reinterpret_cast<char*>(&num_envs), sizeof(num_envs));
if (num_envs == 0 || num_envs > 6) {
throw std::ios_base::failure("Invalid shader cache stage count");
}
std::vector<FileEnvironment> envs(num_envs);
for (FileEnvironment& env : envs) {
env.Deserialize(file);
}
if (envs.front().ShaderStage() == Shader::Stage::Compute) {
if (envs.size() != 1) {
throw std::ios_base::failure("Invalid compute shader cache stage count");
}
load_compute(file, std::move(envs.front()));
} else {
load_graphics(file, std::move(envs));

View File

@ -70,6 +70,9 @@ public:
return !cbuf_replacements.empty();
}
// Banks whose bound sizes were actually queried during shader translation.
[[nodiscard]] u32 CbufSizeMask() const noexcept;
protected:
std::optional<u64> TryFindSize();
@ -117,6 +120,8 @@ public:
u32 ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) override;
u32 ReadCbufSize(u32 cbuf_index) override;
Shader::TextureType ReadTextureType(u32 handle) override;
Shader::TexturePixelFormat ReadTexturePixelFormat(u32 handle) override;
@ -143,6 +148,8 @@ public:
u32 ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) override;
u32 ReadCbufSize(u32 cbuf_index) override;
Shader::TextureType ReadTextureType(u32 handle) override;
Shader::TexturePixelFormat ReadTexturePixelFormat(u32 handle) override;
@ -177,6 +184,10 @@ public:
[[nodiscard]] u32 ReadCbufValue(u32 cbuf_index, u32 cbuf_offset) override;
[[nodiscard]] u32 ReadCbufSize(u32 cbuf_index) override;
[[nodiscard]] u32 CbufSizeMask() const noexcept;
[[nodiscard]] Shader::TextureType ReadTextureType(u32 handle) override;
[[nodiscard]] Shader::TexturePixelFormat ReadTexturePixelFormat(u32 handle) override;

View File

@ -8,6 +8,7 @@
#include "yuzu_video_core/surface.h"
#include "yuzu_video_core/texture_cache/format_lookup_table.h"
#include "yuzu_video_core/texture_cache/image_info.h"
#include "yuzu_video_core/texture_cache/mip_levels.h"
#include "yuzu_video_core/texture_cache/samples_helper.h"
#include "yuzu_video_core/texture_cache/types.h"
#include "yuzu_video_core/texture_cache/util.h"
@ -122,6 +123,9 @@ ImageInfo::ImageInfo(const TICEntry& config) noexcept {
GetFormatType(format) != SurfaceType::ColorTexture;
downscaleable = size.height > DownscaleHeightThreshold;
}
// Preserve the guest layer stride above, including any excess mip levels
// declared by the descriptor, before limiting the host image's mip chain.
resources.levels = MipLevelCount(config);
}
ImageInfo::ImageInfo(const Maxwell3D::Regs::RenderTargetConfig& ct,

View File

@ -5,6 +5,7 @@
#include "yuzu_common/yuzu_assert.h"
#include "yuzu_video_core/texture_cache/image_view_info.h"
#include "yuzu_video_core/texture_cache/mip_levels.h"
#include "yuzu_video_core/texture_cache/texture_cache_base.h"
#include "yuzu_video_core/texture_cache/types.h"
#include "yuzu_video_core/texture_cache/util.h"
@ -30,11 +31,14 @@ ImageViewInfo::ImageViewInfo(const TICEntry& config, s32 base_layer) noexcept
: format{PixelFormatFromTIC(config)}, x_source{CastSwizzle(config.x_source)},
y_source{CastSwizzle(config.y_source)}, z_source{CastSwizzle(config.z_source)},
w_source{CastSwizzle(config.w_source)} {
const s32 last_level = static_cast<s32>(MipLevelCount(config)) - 1;
range.base = SubresourceBase{
.level = static_cast<s32>(config.res_min_mip_level),
.level = std::min(static_cast<s32>(config.res_min_mip_level), last_level),
.layer = base_layer,
};
range.extent.levels = config.res_max_mip_level - config.res_min_mip_level + 1;
const s32 max_level = std::clamp(static_cast<s32>(config.res_max_mip_level),
range.base.level, last_level);
range.extent.levels = max_level - range.base.level + 1;
switch (config.texture_type) {
case TextureType::Texture1D:

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <bit>
#include "yuzu_video_core/textures/texture.h"
namespace VideoCommon {
// Array layers do not contribute to mip dimensions. Linear, buffer and
// multisampled images cannot have a mip chain in the host API.
[[nodiscard]] constexpr u32 MipLevelCount(const Tegra::Texture::TICEntry& config) {
using Tegra::Texture::TextureType;
if (config.IsPitchLinear() || config.IsBuffer() ||
config.texture_type == TextureType::Texture1DBuffer ||
config.msaa_mode != Tegra::Texture::MsaaMode::Msaa1x1) {
return 1;
}
u32 max_dimension = config.Width();
if (config.texture_type != TextureType::Texture1D &&
config.texture_type != TextureType::Texture1DArray) {
max_dimension = std::max(max_dimension, config.Height());
}
if (config.texture_type == TextureType::Texture3D) {
max_dimension = std::max(max_dimension, config.Depth());
}
return std::min(config.max_mip_level.Value() + 1,
static_cast<u32>(std::bit_width(max_dimension)));
}
} // namespace VideoCommon

View File

@ -10,7 +10,8 @@
namespace VideoCommon {
constexpr size_t NUM_RT = 8;
constexpr size_t MAX_MIP_LEVELS = 14;
// The TIC mip level field is four bits and encodes up to 16 levels.
constexpr size_t MAX_MIP_LEVELS = 16;
constexpr Common::SlotId CORRUPT_ID{0xfffffffe};

View File

@ -31,6 +31,7 @@ VK_DEFINE_HANDLE(VmaAllocator)
FEATURE(KHR, VariablePointer, VARIABLE_POINTERS, variable_pointer)
#define FOR_EACH_VK_FEATURE_1_2(FEATURE) \
FEATURE(EXT, DescriptorIndexing, DESCRIPTOR_INDEXING, descriptor_indexing) \
FEATURE(EXT, HostQueryReset, HOST_QUERY_RESET, host_query_reset) \
FEATURE(KHR, 8BitStorage, 8BIT_STORAGE, bit8_storage) \
FEATURE(KHR, TimelineSemaphore, TIMELINE_SEMAPHORE, timeline_semaphore)
@ -337,6 +338,11 @@ public:
return GetDriverID() != VK_DRIVER_ID_QUALCOMM_PROPRIETARY;
}
/// Returns true if sampled texture array indices may vary between shader invocations.
bool IsSampledImageArrayNonUniformIndexingSupported() const {
return features.descriptor_indexing.shaderSampledImageArrayNonUniformIndexing != VK_FALSE;
}
/// Returns true if the device supports float64 natively.
bool IsFloat64Supported() const {
return features.features.shaderFloat64;
@ -377,6 +383,18 @@ public:
return MoltenVK::PushDescriptorLimit(GetDriverID(), properties.push_descriptor.maxPushDescriptors);
}
const VkPhysicalDeviceLimits& DescriptorLimits() const noexcept {
return properties.properties.limits;
}
/// Single-stage upper bound; the complete pipeline layout checks actual resource usage.
u32 MaxBindlessDescriptorsPerStage() const {
const auto& limits = DescriptorLimits();
return std::min({limits.maxPerStageDescriptorSampledImages,
limits.maxPerStageDescriptorSamplers, limits.maxDescriptorSetSampledImages,
limits.maxDescriptorSetSamplers, limits.maxPerStageResources});
}
/// Returns true if formatless image load is supported.
bool IsFormatlessImageLoadSupported() const {
return features.features.shaderStorageImageReadWithoutFormat;

View File

@ -153,6 +153,10 @@
<ClInclude Include="renderer_vulkan\vk_buffer_cache.h" />
<ClInclude Include="renderer_vulkan\vk_command_pool.h" />
<ClInclude Include="renderer_vulkan\vk_compute_pass.h" />
<ClInclude Include="renderer_vulkan\descriptor_lookup.h" />
<ClInclude Include="renderer_vulkan\descriptor_limits.h" />
<ClInclude Include="renderer_vulkan\descriptor_payload.h" />
<ClInclude Include="renderer_vulkan\cbuf_size_dependencies.h" />
<ClInclude Include="renderer_vulkan\vk_compute_pipeline.h" />
<ClInclude Include="renderer_vulkan\vk_descriptor_pool.h" />
<ClInclude Include="renderer_vulkan\vk_fence_manager.h" />
@ -192,6 +196,7 @@
<ClInclude Include="texture_cache\format_lookup_table.h" />
<ClInclude Include="texture_cache\image_base.h" />
<ClInclude Include="texture_cache\image_info.h" />
<ClInclude Include="texture_cache\mip_levels.h" />
<ClInclude Include="texture_cache\image_view_base.h" />
<ClInclude Include="texture_cache\image_view_info.h" />
<ClInclude Include="texture_cache\render_targets.h" />

View File

@ -474,6 +474,18 @@
<ClInclude Include="renderer_vulkan\vk_compute_pass.h">
<Filter>Header Files\renderer_vulkan</Filter>
</ClInclude>
<ClInclude Include="renderer_vulkan\descriptor_limits.h">
<Filter>Header Files\renderer_vulkan</Filter>
</ClInclude>
<ClInclude Include="renderer_vulkan\descriptor_payload.h">
<Filter>Header Files\renderer_vulkan</Filter>
</ClInclude>
<ClInclude Include="renderer_vulkan\descriptor_lookup.h">
<Filter>Header Files\renderer_vulkan</Filter>
</ClInclude>
<ClInclude Include="renderer_vulkan\cbuf_size_dependencies.h">
<Filter>Header Files\renderer_vulkan</Filter>
</ClInclude>
<ClInclude Include="renderer_vulkan\vk_compute_pipeline.h">
<Filter>Header Files\renderer_vulkan</Filter>
</ClInclude>
@ -579,6 +591,9 @@
<ClInclude Include="texture_cache\image_info.h">
<Filter>Header Files\texture_cache</Filter>
</ClInclude>
<ClInclude Include="texture_cache\mip_levels.h">
<Filter>Header Files\texture_cache</Filter>
</ClInclude>
<ClInclude Include="texture_cache\image_view_base.h">
<Filter>Header Files\texture_cache</Filter>
</ClInclude>