diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-31 16:03:16 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-31 16:03:16 -0400 |
| commit | 561f87abec2f76b3ca40c59277752ed4d3b6bf16 (patch) | |
| tree | f64d85c11e42b225f0b7d62a87391f8dff22f4a8 | |
| parent | 77ea16e2f26edb443341a12e8169018fc1fa648e (diff) | |
shader: compute texture handles read from storage buffers for each draw
Switch 2 shaders can read a texture handle from a storage buffer (Switch
Sports' scenery: a per-object entry of a table whose pointer is in the driver
constant buffer). The bindless handle search looked through the load into its
address and returned the table pointer's constant buffer word as the handle,
which named an arbitrary descriptor; the game repoints that one at transient
render targets every few seconds, so the island and menu lighting flipped
between correct, dark and red.
The search no longer looks into memory loads. A handle it cannot trace is now
built into a small program over constant buffer words, storage buffer words and
integer operations (Shader::Info::handle_programs), which the pipelines evaluate
when they bind the texture. Handles a single evaluation cannot give (one that
changes in a loop) keep the previous search.
| -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 |
