diff options
| -rw-r--r-- | src/shader_recompiler/environment.h | 1 | ||||
| -rw-r--r-- | src/shader_recompiler/ir_opt/texture_pass.cpp | 432 | ||||
| -rw-r--r-- | src/shader_recompiler/shader_info.h | 116 | ||||
| -rw-r--r-- | src/video_core/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/video_core/renderer_opengl/gl_compute_pipeline.cpp | 15 | ||||
| -rw-r--r-- | src/video_core/renderer_opengl/gl_graphics_pipeline.cpp | 14 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_compute_pipeline.cpp | 15 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp | 14 | ||||
| -rw-r--r-- | src/video_core/shader_handle_program.h | 39 |
9 files changed, 641 insertions, 6 deletions
diff --git a/src/shader_recompiler/environment.h b/src/shader_recompiler/environment.h index 67486430c8..f47900ef4b 100644 --- a/src/shader_recompiler/environment.h +++ b/src/shader_recompiler/environment.h @@ -66,6 +66,7 @@ struct ConstBufferAddr { IR::U32 dynamic_offset; u32 count; bool has_secondary; + u32 handle_program{~0U}; ///< index in Info::handle_programs for a handle computed per draw }; class Environment { diff --git a/src/shader_recompiler/ir_opt/texture_pass.cpp b/src/shader_recompiler/ir_opt/texture_pass.cpp index fae69ab91b..f413cf108a 100644 --- a/src/shader_recompiler/ir_opt/texture_pass.cpp +++ b/src/shader_recompiler/ir_opt/texture_pass.cpp @@ -5,6 +5,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> +#include <queue> +#include <unordered_map> +#include <unordered_set> +#include <vector> #include <bit> #include <optional> #include "common/container/unordered_map.h" @@ -340,15 +344,20 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr } // Cached variants of existing helpers +// A handle computed per draw has no descriptor to read at translation time: assume an ordinary +// floating-point 2D texture, the instruction's own type deciding the rest. static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) { + if (cbuf.handle_program != ~0U) return TextureType::Color2D; return env.ReadTextureType(GetTextureHandleCached(env, cbuf)); } static inline TexturePixelFormat ReadTexturePixelFormatCached(Environment& env, const ConstBufferAddr& cbuf) { + if (cbuf.handle_program != ~0U) return TexturePixelFormat::A8B8G8R8_UNORM; return env.ReadTexturePixelFormat(GetTextureHandleCached(env, cbuf)); } static inline bool IsTexturePixelFormatIntegerCached(Environment& env, const ConstBufferAddr& cbuf) { + if (cbuf.handle_program != ~0U) return false; return env.IsTexturePixelFormatInteger(GetTextureHandleCached(env, cbuf)); } @@ -366,12 +375,387 @@ static inline std::optional<ConstBufferAddr> TrackCached(const IR::Value& v, Env std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block); +bool IsMemoryLoad(IR::Opcode opcode) { + switch (opcode) { + case IR::Opcode::LoadGlobalU8: case IR::Opcode::LoadGlobalS8: case IR::Opcode::LoadGlobalU16: + case IR::Opcode::LoadGlobalS16: case IR::Opcode::LoadGlobal32: case IR::Opcode::LoadGlobal64: + case IR::Opcode::LoadGlobal128: case IR::Opcode::LoadStorageU8: case IR::Opcode::LoadStorageS8: + case IR::Opcode::LoadStorageU16: case IR::Opcode::LoadStorageS16: case IR::Opcode::LoadStorage32: + case IR::Opcode::LoadStorage64: case IR::Opcode::LoadStorage128: case IR::Opcode::LoadLocal: + case IR::Opcode::LoadSharedU8: case IR::Opcode::LoadSharedS8: case IR::Opcode::LoadSharedU16: + case IR::Opcode::LoadSharedS16: case IR::Opcode::LoadSharedU32: case IR::Opcode::LoadSharedU64: + case IR::Opcode::LoadSharedU128: + return true; + default: + return false; + } +} + std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) { - return IR::BreadthFirstSearch(value, [&env, &host_info, block](const IR::Inst* inst) { - return TryGetConstBuffer(inst, env, host_info, block); - }, [&](IR::Value operand) { return ResolveConditionalPhi(operand, block); }); + // A value loaded from memory is not a constant buffer word, whatever its address is built + // from: the search does not look into a load's operands (a handle read from a storage + // buffer would otherwise be taken from the buffer's address in the constant buffer). + const IR::Value start{ResolveConditionalPhi(value, block)}; + if (start.IsImmediate()) { + return std::nullopt; + } + std::unordered_set<const IR::Inst*> visited; + std::queue<const IR::Inst*> queue; + queue.push(start.InstRecursive()); + visited.insert(start.InstRecursive()); + while (!queue.empty()) { + const IR::Inst* const inst{queue.front()}; + queue.pop(); + if (const std::optional result = TryGetConstBuffer(inst, env, host_info, block)) { + return result; + } + if (IsMemoryLoad(inst->GetOpcode())) { + continue; + } + for (size_t arg = inst->NumArgs(); arg--;) { + const IR::Value arg_value{ResolveConditionalPhi(inst->Arg(arg), block)}; + if (arg_value.IsImmediate()) { + continue; + } + const IR::Inst* const arg_inst{arg_value.InstRecursive()}; + if (visited.insert(arg_inst).second) { + queue.push(arg_inst); + } + } + } + return std::nullopt; +} + +// Builds a handle program (shader_info.h) for a value computed from constant buffer and storage +// buffer words; false when the value depends on anything else. +thread_local IR::Opcode g_handle_failure{IR::Opcode::Void}; // the first operation not supported + +bool IsUndefined(IR::Opcode opcode) { + return opcode == IR::Opcode::UndefU1 || opcode == IR::Opcode::UndefU8 || + opcode == IR::Opcode::UndefU16 || opcode == IR::Opcode::UndefU32 || + opcode == IR::Opcode::UndefU64; +} + +// Values already emitted by the program being built, by the slot that keeps them. +thread_local std::unordered_map<const IR::Inst*, u32>* g_handle_slots{}; + +bool BuildHandleNode(const IR::Value& value, std::vector<u32>& code, int& budget, + const IR::Block* block); + +bool BuildHandleProgram(const IR::Value& value, std::vector<u32>& code, int& budget, + const IR::Block* block) { + using Op = HandleOp; + if (value.IsImmediate()) { + return BuildHandleNode(value, code, budget, block); + } + std::unordered_map<const IR::Inst*, u32> local_slots; + const bool outermost = g_handle_slots == nullptr; + if (outermost) { + g_handle_slots = &local_slots; + } + const IR::Inst* const inst{value.InstRecursive()}; + bool result; + if (const auto it = g_handle_slots->find(inst); it != g_handle_slots->end()) { + if (it->second == ~0U) { + // Reached again while building it: a loop carries the value, which no single + // evaluation can give. + g_handle_failure = IR::Opcode::Phi; + result = false; + } else { + code.push_back(static_cast<u32>(Op::Load)); + code.push_back(it->second); + result = true; + } + } else { + g_handle_slots->emplace(inst, ~0U); + const size_t start = code.size(); + result = BuildHandleNode(value, code, budget, block); + g_handle_slots->erase(inst); + if (!result) { + code.resize(start); + } + if (result && inst->UseCount() > 1 && g_handle_slots->size() < 256) { + const u32 slot = static_cast<u32>( + std::ranges::count_if(*g_handle_slots, [](const auto& entry) { return entry.second != ~0U; })); + g_handle_slots->emplace(inst, slot); + code.push_back(static_cast<u32>(Op::Tee)); + code.push_back(slot); + } + } + if (outermost) { + g_handle_slots = nullptr; + } + return result; } +bool BuildHandleNode(const IR::Value& value, std::vector<u32>& code, int& budget, + const IR::Block* block) { + if (--budget < 0) { + return false; + } + using Op = HandleOp; + const auto emit = [&](Op op) { code.push_back(static_cast<u32>(op)); }; + if (value.IsImmediate()) { + emit(Op::Immediate); + code.push_back(value.Type() == IR::Type::U1 ? (value.U1() ? 1U : 0U) : value.U32()); + return true; + } + const IR::Inst* const inst{value.InstRecursive()}; + const auto build = [&](size_t arg) { return BuildHandleProgram(inst->Arg(arg), code, budget, block); }; + const auto binary = [&](Op op) { + if (!build(0) || !build(1)) return false; + emit(op); + return true; + }; + const auto unary = [&](Op op) { + if (!build(0)) return false; + emit(op); + return true; + }; + // A word of a constant buffer read or of a storage buffer load, at byte offset +extra. + const auto load_word = [&](const IR::Inst* source, u32 extra) { + const auto add_extra = [&] { + if (extra != 0) { + emit(Op::Immediate); + code.push_back(extra); + emit(Op::Add); + } + }; + switch (source->GetOpcode()) { + case IR::Opcode::GetCbufU32: + case IR::Opcode::GetCbufU32x2: { + const IR::Value index{source->Arg(0)}; + const IR::Value offset{source->Arg(1)}; + if (!index.IsImmediate()) return false; + if (offset.IsImmediate()) { + emit(Op::Cbuf); + code.push_back(index.U32()); + code.push_back(offset.U32() + extra); + return true; + } + if (!BuildHandleProgram(offset, code, budget, block)) return false; + add_extra(); + emit(Op::CbufDynamic); + code.push_back(index.U32()); + return true; + } + case IR::Opcode::LoadStorage32: + case IR::Opcode::LoadStorage64: + case IR::Opcode::LoadStorage128: { + const IR::Value binding{source->Arg(0)}; + if (!binding.IsImmediate()) return false; + if (!BuildHandleProgram(source->Arg(1), code, budget, block)) return false; + add_extra(); + emit(Op::Storage); + code.push_back(binding.U32()); + return true; + } + default: + return false; + } + }; + switch (inst->GetOpcode()) { + case IR::Opcode::Identity: + case IR::Opcode::ConditionRef: + case IR::Opcode::ShuffleIndex: // a value made uniform (R2UR): the same in every lane + case IR::Opcode::VoteAny: // of a condition on per-draw words only, the same in every lane + case IR::Opcode::VoteAll: + return build(0); + case IR::Opcode::GetCbufU32: + case IR::Opcode::LoadStorage32: + return load_word(inst, 0); + case IR::Opcode::CompositeExtractU32x2: { + const IR::Value element{inst->Arg(1)}; + if (!element.IsImmediate() || inst->Arg(0).IsImmediate()) return false; + return load_word(inst->Arg(0).InstRecursive(), element.U32() * 4); + } + case IR::Opcode::Phi: { + // Keep the incoming value that is defined; of two, the one made uniform (the + // register-to-uniform move a conditional block makes for the handle). + const IR::Value resolved{ResolveConditionalPhi(value, block)}; + if (resolved.IsImmediate() || resolved.InstRecursive() != inst) { + return BuildHandleProgram(resolved, code, budget, block); + } + std::optional<size_t> chosen; + for (size_t arg = 0; arg < inst->NumArgs(); ++arg) { + const IR::Value incoming{inst->Arg(arg)}; + if (!incoming.IsImmediate() && IsUndefined(incoming.InstRecursive()->GetOpcode())) { + continue; + } + IR::Value probe{incoming}; + bool shuffled = false; + for (int depth = 0; depth < 8 && !probe.IsImmediate(); ++depth) { + const IR::Inst* const probe_inst{probe.InstRecursive()}; + if (probe_inst->GetOpcode() == IR::Opcode::ShuffleIndex) { + shuffled = true; + break; + } + if (probe_inst->GetOpcode() != IR::Opcode::Identity) break; + probe = probe_inst->Arg(0); + } + if (!chosen || shuffled) chosen = arg; + } + if (chosen && build(*chosen)) { + return true; + } + for (size_t arg = 0; arg < inst->NumArgs(); ++arg) { + const IR::Value incoming{inst->Arg(arg)}; + if (arg == chosen || (!incoming.IsImmediate() && IsUndefined(incoming.InstRecursive()->GetOpcode()))) { + continue; + } + if (build(arg)) { + return true; + } + } + return false; + } + case IR::Opcode::IAdd32: return binary(Op::Add); + case IR::Opcode::ISub32: return binary(Op::Sub); + case IR::Opcode::IMul32: return binary(Op::Mul); + case IR::Opcode::BitwiseAnd32: return binary(Op::And); + case IR::Opcode::BitwiseOr32: return binary(Op::Or); + case IR::Opcode::BitwiseXor32: return binary(Op::Xor); + case IR::Opcode::ShiftLeftLogical32: return binary(Op::Shl); + case IR::Opcode::ShiftRightLogical32: return binary(Op::Shr); + case IR::Opcode::ShiftRightArithmetic32: return binary(Op::Sar); + case IR::Opcode::INeg32: return unary(Op::Neg); + case IR::Opcode::BitwiseNot32: return unary(Op::Not); + case IR::Opcode::UMin32: return binary(Op::UMin); + case IR::Opcode::UMax32: return binary(Op::UMax); + case IR::Opcode::SMin32: return binary(Op::SMin); + case IR::Opcode::SMax32: return binary(Op::SMax); + case IR::Opcode::SLessThan: return binary(Op::SLt); + case IR::Opcode::SLessThanEqual: return binary(Op::SLe); + case IR::Opcode::SGreaterThan: return binary(Op::SGt); + case IR::Opcode::SGreaterThanEqual: return binary(Op::SGe); + case IR::Opcode::ULessThan: return binary(Op::ULt); + case IR::Opcode::ULessThanEqual: return binary(Op::ULe); + case IR::Opcode::UGreaterThan: return binary(Op::UGt); + case IR::Opcode::UGreaterThanEqual: return binary(Op::UGe); + case IR::Opcode::IEqual: return binary(Op::Eq); + case IR::Opcode::INotEqual: return binary(Op::Ne); + case IR::Opcode::LogicalAnd: return binary(Op::LAnd); + case IR::Opcode::LogicalOr: return binary(Op::LOr); + case IR::Opcode::LogicalXor: return binary(Op::LXor); + case IR::Opcode::LogicalNot: return unary(Op::LNot); + case IR::Opcode::GetZeroFromOp: // the flag a producing operation sets on its result + if (!build(0)) return false; + emit(Op::Immediate); + code.push_back(0); + emit(Op::Eq); + return true; + case IR::Opcode::GetSignFromOp: + if (!build(0)) return false; + emit(Op::Immediate); + code.push_back(0); + emit(Op::SLt); + return true; + case IR::Opcode::GetCarryFromOp: { + // The carry of a 32-bit addition: the sum below an operand. + const IR::Value op{inst->Arg(0)}; + if (op.IsImmediate() || op.InstRecursive()->GetOpcode() != IR::Opcode::IAdd32) { + g_handle_failure = inst->GetOpcode(); + return false; + } + const IR::Inst* const add{op.InstRecursive()}; + if (!BuildHandleProgram(op, code, budget, block) || + !BuildHandleProgram(add->Arg(0), code, budget, block)) { + return false; + } + emit(Op::ULt); + return true; + } + case IR::Opcode::BitCastU32F32: + case IR::Opcode::BitCastF32U32: + return build(0); + case IR::Opcode::UndefU1: + case IR::Opcode::UndefU32: + // Read before any write on some path: no value to compute, take zero. + emit(Op::Immediate); + code.push_back(0); + return true; + case IR::Opcode::BitFieldUExtract: { + // (base >> offset) & ((1 << count) - 1), the shift by 32 giving the all-ones mask. + const auto mask = [&] { + emit(Op::Immediate); + code.push_back(1); + if (!build(2)) return false; + emit(Op::Shl); + emit(Op::Immediate); + code.push_back(1); + emit(Op::Sub); + return true; + }; + if (!build(0) || !build(1)) return false; + emit(Op::Shr); + if (!mask()) return false; + emit(Op::And); + return true; + } + case IR::Opcode::BitFieldSExtract: { + // (base << (32 - offset - count)) >> (32 - count), arithmetic. + if (!build(0)) return false; + emit(Op::Immediate); + code.push_back(32); + if (!build(1)) return false; + emit(Op::Sub); + if (!build(2)) return false; + emit(Op::Sub); + emit(Op::Shl); + emit(Op::Immediate); + code.push_back(32); + if (!build(2)) return false; + emit(Op::Sub); + emit(Op::Sar); + return true; + } + case IR::Opcode::BitFieldInsert: { + // (base & ~mask) | ((insert << offset) & mask), mask = ((1 << count) - 1) << offset. + const auto mask = [&] { + emit(Op::Immediate); + code.push_back(1); + if (!build(3)) return false; + emit(Op::Shl); + emit(Op::Immediate); + code.push_back(1); + emit(Op::Sub); + if (!build(2)) return false; + emit(Op::Shl); + return true; + }; + if (!build(0) || !mask()) return false; + emit(Op::Not); + emit(Op::And); + if (!build(1) || !build(2)) return false; + emit(Op::Shl); + if (!mask()) return false; + emit(Op::And); + emit(Op::Or); + return true; + } + case IR::Opcode::UClamp32: + case IR::Opcode::SClamp32: { + const bool is_signed = inst->GetOpcode() == IR::Opcode::SClamp32; + if (!build(0) || !build(1)) return false; + emit(is_signed ? Op::SMax : Op::UMax); + if (!build(2)) return false; + emit(is_signed ? Op::SMin : Op::UMin); + return true; + } + case IR::Opcode::SelectU1: + case IR::Opcode::SelectU32: + if (!build(0) || !build(1) || !build(2)) return false; + emit(Op::Select); + return true; + default: + g_handle_failure = inst->GetOpcode(); + return false; + } +} + +// Programs built while making the texture instructions of the pass running on this thread. +thread_local std::vector<std::vector<u32>>* g_handle_programs{}; + std::optional<u32> TryGetConstant(IR::Value& value, Environment& env) { const IR::Inst* inst = value.InstRecursive(); if (inst->GetOpcode() != IR::Opcode::GetCbufU32) { @@ -525,10 +909,44 @@ TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const H if (IsBindless(inst)) { const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info, block)}; - if (!track_addr) { - throw NotImplementedException("Failed to track bindless texture constant buffer"); - } else { + if (track_addr) { addr = *track_addr; + } else { + // Read from a storage buffer (a handle table): computed for each draw. + std::vector<u32> code; + int budget = 4096; + g_handle_failure = IR::Opcode::Void; + if (!g_handle_programs || !BuildHandleProgram(inst.Arg(0), code, budget, block)) { + // Not computable for a whole draw (a handle that changes in a loop): keep the + // earlier search through loads, which at least gives the shader a texture. + const std::optional<ConstBufferAddr> legacy{IR::BreadthFirstSearch(inst.Arg(0), + [&env, &host_info, block](const IR::Inst* candidate) { + return TryGetConstBuffer(candidate, env, host_info, block); + }, [&](IR::Value operand) { return ResolveConditionalPhi(operand, block); })}; + if (!legacy) { + throw NotImplementedException("Failed to track bindless texture constant buffer (stopped at {}, budget {})", + g_handle_failure, budget); + } + return TextureInst{.cbuf = *legacy, .inst = &inst, .block = block}; + } + auto& programs{*g_handle_programs}; + auto it{std::ranges::find(programs, code)}; + if (it == programs.end()) { + programs.push_back(std::move(code)); + it = programs.end() - 1; + } + addr = ConstBufferAddr{ + .index = 0, + .offset = 0, + .shift_left = 0, + .secondary_index = 0, + .secondary_offset = 0, + .secondary_shift_left = 0, + .dynamic_offset = {}, + .count = 1, + .has_secondary = false, + .handle_program = static_cast<u32>(it - programs.begin()), + }; } } else { addr = ConstBufferAddr{ @@ -724,6 +1142,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo env.handle_cache.clear(); env.track_cache.clear(); TextureInstVector to_replace; + g_handle_programs = &program.info.handle_programs; for (IR::Block* const block : program.post_order_blocks) { env.track_cache.clear(); for (IR::Inst& inst : block->Instructions()) { @@ -881,6 +1300,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo .secondary_shift_left = cbuf.secondary_shift_left, .count = count, .size_shift = size_shift, + .handle_program = cbuf.handle_program, }); flags.is_integer.Assign(is_integer ? 1 : 0); } diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h index 71309e040e..e2fcf278b9 100644 --- a/src/shader_recompiler/shader_info.h +++ b/src/shader_recompiler/shader_info.h @@ -7,6 +7,9 @@ #pragma once #include <array> +#include <optional> +#include <span> +#include <vector> #include <bitset> #include <map> @@ -219,6 +222,9 @@ struct TextureDescriptor { u32 secondary_shift_left; u32 count; u32 size_shift; + /// Index in Info::handle_programs when the handle is computed at draw time (read from a + /// storage buffer), ~0 when it is the constant buffer word above. + u32 handle_program{~0U}; auto operator<=>(const TextureDescriptor&) const = default; }; @@ -337,6 +343,9 @@ struct Info { TextureBufferDescriptors texture_buffer_descriptors; ImageBufferDescriptors image_buffer_descriptors; TextureDescriptors texture_descriptors; + /// Texture handles the shader reads from storage buffers, as programs over constant buffer + /// and storage buffer words (see EvaluateHandleProgram), evaluated for each draw. + std::vector<std::vector<u32>> handle_programs; ImageDescriptors image_descriptors; }; @@ -349,4 +358,111 @@ u32 NumDescriptors(const Descriptors& descriptors) { return num; } + +/// Operations of a handle program, a stack machine over 32-bit words. Operands follow the +/// operation code in the program; values are popped right operand first. +enum class HandleOp : u32 { + Immediate, ///< value + Cbuf, ///< index, offset: the constant buffer word + CbufDynamic, ///< index: the word at the popped byte offset + Storage, ///< binding: the storage buffer word at the popped byte offset + Add, Sub, Mul, And, Or, Xor, Shl, Shr, Sar, Neg, Not, + UMin, UMax, SMin, SMax, + SLt, SLe, SGt, SGe, ULt, ULe, UGt, UGe, Eq, Ne, + LAnd, LOr, LXor, LNot, + Select, ///< condition, true value, false value + Tee, ///< slot: keeps the top value in the slot (shared subexpressions) + Load, ///< slot: the value kept there +}; + +/// Runs a handle program; read_cbuf(index, offset) and read_storage(binding, offset) return the +/// words it reads. Returns nothing for a malformed program. +template <typename ReadCbuf, typename ReadStorage> +std::optional<u32> EvaluateHandleProgram(std::span<const u32> code, ReadCbuf&& read_cbuf, + ReadStorage&& read_storage) { + std::array<u32, 64> stack{}; + std::array<u32, 256> slots{}; + size_t size = 0; + const auto pop = [&]() -> u32 { return size > 0 ? stack[--size] : 0; }; + const auto push = [&](u32 value) { + if (size < stack.size()) stack[size++] = value; + }; + for (size_t pc = 0; pc < code.size();) { + const HandleOp op = static_cast<HandleOp>(code[pc++]); + const auto operand = [&]() -> u32 { return pc < code.size() ? code[pc++] : 0; }; + if (op == HandleOp::Immediate) { + push(operand()); + continue; + } + if (op == HandleOp::Cbuf) { + const u32 index = operand(); + push(read_cbuf(index, operand())); + continue; + } + if (op == HandleOp::CbufDynamic) { + const u32 index = operand(); + push(read_cbuf(index, pop())); + continue; + } + if (op == HandleOp::Storage) { + const u32 binding = operand(); + push(read_storage(binding, pop())); + continue; + } + if (op == HandleOp::Neg || op == HandleOp::Not || op == HandleOp::LNot) { + const u32 a = pop(); + push(op == HandleOp::Neg ? 0U - a : op == HandleOp::Not ? ~a : (a == 0 ? 1U : 0U)); + continue; + } + if (op == HandleOp::Tee) { + const u32 slot = operand(); + if (slot < slots.size() && size > 0) slots[slot] = stack[size - 1]; + continue; + } + if (op == HandleOp::Load) { + const u32 slot = operand(); + push(slot < slots.size() ? slots[slot] : 0U); + continue; + } + if (op == HandleOp::Select) { + const u32 false_value = pop(), true_value = pop(), condition = pop(); + push(condition != 0 ? true_value : false_value); + continue; + } + const u32 b = pop(), a = pop(); + const s32 sa = static_cast<s32>(a), sb = static_cast<s32>(b); + switch (op) { + case HandleOp::Add: push(a + b); break; + case HandleOp::Sub: push(a - b); break; + case HandleOp::Mul: push(a * b); break; + case HandleOp::And: push(a & b); break; + case HandleOp::Or: push(a | b); break; + case HandleOp::Xor: push(a ^ b); break; + case HandleOp::Shl: push(b >= 32 ? 0U : a << b); break; + case HandleOp::Shr: push(b >= 32 ? 0U : a >> b); break; + case HandleOp::Sar: push(static_cast<u32>(sa >> (b >= 32 ? 31 : b))); break; + case HandleOp::UMin: push(a < b ? a : b); break; + case HandleOp::UMax: push(a > b ? a : b); break; + case HandleOp::SMin: push(static_cast<u32>(sa < sb ? sa : sb)); break; + case HandleOp::SMax: push(static_cast<u32>(sa > sb ? sa : sb)); break; + case HandleOp::SLt: push(sa < sb); break; + case HandleOp::SLe: push(sa <= sb); break; + case HandleOp::SGt: push(sa > sb); break; + case HandleOp::SGe: push(sa >= sb); break; + case HandleOp::ULt: push(a < b); break; + case HandleOp::ULe: push(a <= b); break; + case HandleOp::UGt: push(a > b); break; + case HandleOp::UGe: push(a >= b); break; + case HandleOp::Eq: push(a == b); break; + case HandleOp::Ne: push(a != b); break; + case HandleOp::LAnd: push(a != 0 && b != 0); break; + case HandleOp::LOr: push(a != 0 || b != 0); break; + case HandleOp::LXor: push((a != 0) != (b != 0)); break; + default: return std::nullopt; + } + } + if (size != 1) return std::nullopt; + return stack[0]; +} + } // namespace Shader diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 5ed43fc903..b3ef88c5f1 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -235,6 +235,7 @@ add_library(video_core STATIC shader_cache.h shader_environment.cpp shader_environment.h + shader_handle_program.h shader_notify.cpp shader_notify.h smaa_area_tex.h diff --git a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp index f0a7baf9aa..eb134e86b5 100644 --- a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp +++ b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp @@ -12,6 +12,7 @@ #include "video_core/renderer_opengl/gl_compute_pipeline.h" #include "video_core/renderer_opengl/gl_shader_manager.h" #include "video_core/renderer_opengl/gl_shader_util.h" +#include "video_core/shader_handle_program.h" namespace OpenGL { @@ -105,6 +106,20 @@ void ComputePipeline::Configure() { const auto& cbufs{qmd.const_buffer_config}; const bool via_header_index{qmd.linked_tsc != 0}; const auto read_handle{[&](const auto& desc, u32 index) { + if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this dispatch. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, *gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && + ((qmd.const_buffer_enable_mask >> cbuf) & 1) != 0 + ? cbufs[cbuf].Address() + : GPUVAddr{}; + }), + via_header_index); + } + } 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}; diff --git a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp index ecd6544b10..81950279d5 100644 --- a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp +++ b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp @@ -17,6 +17,7 @@ #include "video_core/renderer_opengl/gl_shader_manager.h" #include "video_core/renderer_opengl/gl_shader_util.h" #include "video_core/renderer_opengl/gl_state_tracker.h" +#include "video_core/shader_handle_program.h" #include "video_core/shader_notify.h" #include "video_core/texture_cache/texture_cache.h" @@ -308,6 +309,19 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { } const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers}; const auto read_handle{[&](const auto& desc, u32 index) { + if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this draw. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, *gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && cbufs[cbuf].enabled + ? cbufs[cbuf].address + : GPUVAddr{}; + }), + via_header_index); + } + } ASSERT(cbufs[desc.cbuf_index].enabled); const u32 index_offset{index << desc.size_shift}; const u32 offset{desc.cbuf_offset + index_offset}; diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index 29356cfcec..ee3d94899e 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -18,6 +18,7 @@ #include "video_core/renderer_vulkan/vk_pipeline_cache.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/renderer_vulkan/vk_update_descriptor.h" +#include "video_core/shader_handle_program.h" #include "video_core/shader_notify.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" @@ -172,6 +173,20 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute, const auto& cbufs{qmd.const_buffer_config}; const bool via_header_index{qmd.linked_tsc != 0}; const auto read_handle{[&](const auto& desc, u32 index) { + if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this dispatch. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && + ((qmd.const_buffer_enable_mask >> cbuf) & 1) != 0 + ? cbufs[cbuf].Address() + : GPUVAddr{}; + }), + via_header_index); + } + } 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}; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 94bace432c..3282e85484 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -24,6 +24,7 @@ #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/renderer_vulkan/vk_texture_cache.h" #include "video_core/renderer_vulkan/vk_update_descriptor.h" +#include "video_core/shader_handle_program.h" #include "video_core/shader_notify.h" #include "video_core/texture_cache/texture_cache.h" #include "video_core/vulkan_common/vulkan_device.h" @@ -382,6 +383,19 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) { } const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers}; const auto read_handle{[&](const auto& desc, u32 index) { + if constexpr (std::is_same_v<decltype(desc), const Shader::TextureDescriptor&>) { + if (desc.handle_program != ~0U) { + // Read by the shader from a storage buffer: computed for this draw. + return TexturePair(VideoCommon::EvaluateTextureHandle( + info, desc.handle_program, *gpu_memory, + [&](u32 cbuf) -> GPUVAddr { + return cbuf < cbufs.size() && cbufs[cbuf].enabled + ? cbufs[cbuf].address + : GPUVAddr{}; + }), + via_header_index); + } + } ASSERT(cbufs[desc.cbuf_index].enabled); const u32 index_offset{index << desc.size_shift}; const u32 offset{desc.cbuf_offset + index_offset}; diff --git a/src/video_core/shader_handle_program.h b/src/video_core/shader_handle_program.h new file mode 100644 index 0000000000..3d35524cc8 --- /dev/null +++ b/src/video_core/shader_handle_program.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/common_types.h" +#include "shader_recompiler/shader_info.h" +#include "video_core/memory_manager.h" + +namespace VideoCommon { + +/// The texture handle a shader reads from a storage buffer, computed from the current constant +/// buffer and storage buffer words (Shader::Info::handle_programs). cbuf_address(index) is the +/// bound constant buffer's address, 0 when none is bound. +template <typename CbufAddress> +u32 EvaluateTextureHandle(const Shader::Info& info, u32 program, Tegra::MemoryManager& gpu_memory, + CbufAddress&& cbuf_address) { + return Shader::EvaluateHandleProgram( + info.handle_programs[program], + [&](u32 index, u32 offset) -> u32 { + const GPUVAddr base{cbuf_address(index)}; + return base != 0 ? gpu_memory.Read<u32>(base + offset) : 0U; + }, + [&](u32 binding, u32 offset) -> u32 { + if (binding >= info.storage_buffers_descriptors.size()) { + return 0U; + } + const auto& descriptor{info.storage_buffers_descriptors[binding]}; + const GPUVAddr cbuf{cbuf_address(descriptor.cbuf_index)}; + if (cbuf == 0) { + return 0U; + } + const GPUVAddr base{gpu_memory.Read<u64>(cbuf + descriptor.cbuf_offset)}; + return base != 0 ? gpu_memory.Read<u32>(base + offset) : 0U; + }) + .value_or(0U); +} + +} // namespace VideoCommon |
