// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include #include "common/assert.h" #include "common/cityhash.h" #include "common/common_types.h" #include "common/div_ceil.h" #include "common/fs/fs.h" #include "common/fs/path_util.h" #include "common/logging.h" #include "common/settings.h" #include #include "shader_recompiler/environment.h" #include "shader_recompiler/exception.h" #include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" #include "shader_recompiler/frontend/ampere/program_override.h" #include "shader_recompiler/frontend/ampere/program_capture.h" #include "shader_recompiler/frontend/ampere/program_address_map.h" #include "video_core/engines/kepler_compute.h" #include "video_core/memory_manager.h" #include "video_core/shader_environment.h" #include "video_core/texture_cache/format_lookup_table.h" #include "video_core/textures/texture.h" namespace VideoCommon { constexpr std::array MAGIC_NUMBER{'y', 'u', 'z', 'u', 'c', 'a', 'c', 'h'}; constexpr size_t INST_SIZE = sizeof(u64); using Maxwell = Tegra::Engines::Maxwell3D::Regs; static u64 MakeCbufKey(u32 index, u32 offset) { return (static_cast(index) << 32) | offset; } static Shader::TextureType ConvertTextureType(const Tegra::Texture::TICEntry& entry) { switch (entry.texture_type) { case Tegra::Texture::TextureType::Texture1D: return Shader::TextureType::Color1D; case Tegra::Texture::TextureType::Texture2D: case Tegra::Texture::TextureType::Texture2DNoMipmap: return entry.normalized_coords ? Shader::TextureType::Color2D : Shader::TextureType::Color2DRect; case Tegra::Texture::TextureType::Texture3D: return Shader::TextureType::Color3D; case Tegra::Texture::TextureType::TextureCubemap: return Shader::TextureType::ColorCube; case Tegra::Texture::TextureType::Texture1DArray: return Shader::TextureType::ColorArray1D; case Tegra::Texture::TextureType::Texture2DArray: return Shader::TextureType::ColorArray2D; case Tegra::Texture::TextureType::Texture1DBuffer: return Shader::TextureType::Buffer; case Tegra::Texture::TextureType::TextureCubeArray: return Shader::TextureType::ColorArrayCube; default: LOG_ERROR(Shader, "Invalid texture_type={}. Falling back to texture_type={}", static_cast(entry.texture_type.Value()), Shader::TextureType::Color2D); return Shader::TextureType::Color2D; } } static Shader::TexturePixelFormat ConvertTexturePixelFormat(const Tegra::Texture::TICEntry& entry) { return static_cast( PixelFormatFromTextureInfo(entry.format, entry.r_type, entry.g_type, entry.b_type, entry.a_type, entry.srgb_conversion)); } static std::string_view StageToPrefix(Shader::Stage stage) { switch (stage) { case Shader::Stage::VertexB: return "vs"; case Shader::Stage::TessellationControl: return "tc"; case Shader::Stage::TessellationEval: return "te"; case Shader::Stage::Geometry: return "gs"; case Shader::Stage::Fragment: return "fs"; case Shader::Stage::Compute: return "cs"; case Shader::Stage::VertexA: return "va"; default: return "uk"; } } static void DumpImpl(u64 /*pipeline_hash*/, u64 shader_hash, std::span code, [[maybe_unused]] u32 read_highest, [[maybe_unused]] u32 read_lowest, u32 initial_offset, Shader::Stage stage) { const auto dump_dir{Common::FS::GetEdenPath(Common::FS::EdenPath::DumpDir)}; if (!Common::FS::CreateDir(dump_dir)) { LOG_ERROR(Common_Filesystem, "Failed to create dump directory"); return; } const auto prefix = StageToPrefix(stage); const auto name{dump_dir / fmt::format("{:016x}_{:016x}_{}.ash", Settings::GetCurrentProgramID(), shader_hash, prefix)}; std::fstream shader_file(name, std::ios::out | std::ios::binary); ASSERT(initial_offset % sizeof(u64) == 0); const size_t jump_index = initial_offset / sizeof(u64); const size_t code_size = code.size_bytes() - initial_offset; shader_file.write(reinterpret_cast(&code[jump_index]), code_size); // + 1 instruction, due to the fact that we skip the final self branch instruction in the code, // but we need to consider it for padding, otherwise nvdisasm rages. const size_t padding_needed = (32 - ((code_size + INST_SIZE) % 32)) % 32; for (size_t i = 0; i < INST_SIZE + padding_needed; i++) { shader_file.put(0); } } GenericEnvironment::GenericEnvironment(Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_, u32 start_address_) : gpu_memory{&gpu_memory_}, program_base{program_base_} { start_address = start_address_; } GenericEnvironment::~GenericEnvironment() = default; u32 GenericEnvironment::TextureBoundBuffer() const { return texture_bound; } u32 GenericEnvironment::LocalMemorySize() const { return local_memory_size; } u32 GenericEnvironment::SharedMemorySize() const { return shared_memory_size; } std::array GenericEnvironment::WorkgroupSize() const { return workgroup_size; } u64 GenericEnvironment::ReadInstruction(u32 address) { read_lowest = (std::min)(read_lowest, address); read_highest = (std::max)(read_highest, address); if (address >= cached_lowest && address < cached_highest) { return code[(address - cached_lowest) / INST_SIZE]; } if (native_source_size) { throw Shader::RuntimeError("Lowered native instruction address out of range: {:#x}", address); } has_unbound_instructions = true; return gpu_memory->Read(program_base + address); } u32 GenericEnvironment::TranslateProgramAddress(u32 address) const { if (const auto translated = Shader::Ampere::NativeProgramAddress(code, address)) return *translated; if (native_source_size || Shader::Ampere::HasNativeAddressMap(code)) throw Shader::RuntimeError("Invalid native indirect branch address {:#x}", address); return address; } std::optional GenericEnvironment::Analyze() { if (native_source_size) { // Include the converted header and a version tag to isolate this experimental ABI. return Common::CityHash64(reinterpret_cast(code.data()), code.size() * sizeof(u64)) ^ 0x4f554e4345000002ULL; } const std::optional size{TryFindSize()}; if (!size) { return std::nullopt; } cached_lowest = start_address; cached_highest = start_address + static_cast(*size); return Common::CityHash64(reinterpret_cast(code.data()), *size); } void GenericEnvironment::SetCachedSize(size_t size_bytes) { if (native_source_size) { if (size_bytes != CachedSizeBytes()) { throw Shader::RuntimeError("Native shader changed while constructing pipeline"); } return; } cached_lowest = start_address; cached_highest = start_address + static_cast(size_bytes); code.resize(CachedSizeWords()); gpu_memory->ReadBlock(program_base + cached_lowest, code.data(), code.size() * sizeof(u64)); } size_t GenericEnvironment::CachedSizeWords() const noexcept { return CachedSizeBytes() / INST_SIZE; } size_t GenericEnvironment::CachedSizeBytes() const noexcept { return static_cast(cached_highest) - cached_lowest + INST_SIZE; } size_t GenericEnvironment::ReadSizeBytes() const noexcept { return read_highest - read_lowest + INST_SIZE; } bool GenericEnvironment::CanBeSerialized() const noexcept { return !has_unbound_instructions; } u64 GenericEnvironment::CalculateHash() const { const size_t size{ReadSizeBytes()}; const auto data{std::make_unique(size)}; gpu_memory->ReadBlock(program_base + read_lowest, data.get(), size); return Common::CityHash64(data.get(), size); } void GenericEnvironment::Dump(u64 pipeline_hash, u64 shader_hash) { DumpImpl(pipeline_hash, shader_hash, code, read_highest, read_lowest, initial_offset, stage); } void GenericEnvironment::Serialize(std::ofstream& file) const { const u64 code_size{static_cast(CachedSizeBytes())}; const u64 num_texture_types{static_cast(texture_types.size())}; const u64 num_texture_pixel_formats{static_cast(texture_pixel_formats.size())}; const u64 num_cbuf_values{static_cast(cbuf_values.size())}; const u64 num_cbuf_replacement_values{static_cast(cbuf_replacements.size())}; file.write(reinterpret_cast(&code_size), sizeof(code_size)) .write(reinterpret_cast(&num_texture_types), sizeof(num_texture_types)) .write(reinterpret_cast(&num_texture_pixel_formats), sizeof(num_texture_pixel_formats)) .write(reinterpret_cast(&num_cbuf_values), sizeof(num_cbuf_values)) .write(reinterpret_cast(&num_cbuf_replacement_values), sizeof(num_cbuf_replacement_values)) .write(reinterpret_cast(&local_memory_size), sizeof(local_memory_size)) .write(reinterpret_cast(&texture_bound), sizeof(texture_bound)) .write(reinterpret_cast(&start_address), sizeof(start_address)) .write(reinterpret_cast(&cached_lowest), sizeof(cached_lowest)) .write(reinterpret_cast(&cached_highest), sizeof(cached_highest)) .write(reinterpret_cast(&viewport_transform_state), sizeof(viewport_transform_state)) .write(reinterpret_cast(&stage), sizeof(stage)) .write(reinterpret_cast(code.data()), code_size); for (const auto& [key, type] : texture_types) { file.write(reinterpret_cast(&key), sizeof(key)) .write(reinterpret_cast(&type), sizeof(type)); } for (const auto& [key, format] : texture_pixel_formats) { file.write(reinterpret_cast(&key), sizeof(key)) .write(reinterpret_cast(&format), sizeof(format)); } for (const auto& [key, type] : cbuf_values) { file.write(reinterpret_cast(&key), sizeof(key)) .write(reinterpret_cast(&type), sizeof(type)); } for (const auto& [key, type] : cbuf_replacements) { file.write(reinterpret_cast(&key), sizeof(key)) .write(reinterpret_cast(&type), sizeof(type)); } if (stage == Shader::Stage::Compute) { file.write(reinterpret_cast(&workgroup_size), sizeof(workgroup_size)) .write(reinterpret_cast(&shared_memory_size), sizeof(shared_memory_size)); } else { file.write(reinterpret_cast(&sph), sizeof(sph)); if (stage == Shader::Stage::Geometry) { file.write(reinterpret_cast(&gp_passthrough_mask), sizeof(gp_passthrough_mask)); } } } std::optional GenericEnvironment::TryFindSize() { static constexpr size_t BLOCK_SIZE = 0x1000; static constexpr size_t MAXIMUM_SIZE = 0x100000; static constexpr u64 SELF_BRANCH_A = 0xE2400FFFFF87000FULL; static constexpr u64 SELF_BRANCH_B = 0xE2400FFFFF07000FULL; static constexpr u64 EXIT_VALUE = 0xE30000000007000FULL; code.resize(MAXIMUM_SIZE / INST_SIZE); GPUVAddr guest_addr{program_base + start_address}; size_t offset{0}; size_t size{BLOCK_SIZE}; while (size <= MAXIMUM_SIZE) { u64* const data = code.data() + offset / INST_SIZE; gpu_memory->ReadBlock(guest_addr, data, BLOCK_SIZE); for (size_t index = 0; index < BLOCK_SIZE; index += INST_SIZE) { const u64 inst = data[index / INST_SIZE]; if (inst == SELF_BRANCH_A || inst == SELF_BRANCH_B) { return offset + index; } if (!is_proprietary_driver && inst == EXIT_VALUE) { return offset + index + INST_SIZE; } } guest_addr += BLOCK_SIZE; size += BLOCK_SIZE; offset += BLOCK_SIZE; } return std::nullopt; } Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit, bool via_header_index, u32 raw) { const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)}; const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit, gpu_memory->UsesNativeTiling())}; ASSERT(index <= tic_limit); const GPUVAddr descriptor_addr{tic_addr + index * sizeof(Tegra::Texture::TICEntry)}; Tegra::Texture::TICEntry entry; gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry)); return entry; } namespace { // Reads a native program a 4 KiB page at a time instead of one 16-byte instruction at a time. // Each page is read with ReadBlock, so GPU-written code is flushed first, once per page instead // of once per instruction. An instruction that is not inside a fully mapped page is left to the // caller. class NativeInstructionReader { public: using Instruction = Shader::Ampere::Instruction; static constexpr GPUVAddr PageSize = 0x1000; explicit NativeInstructionReader(Tegra::MemoryManager& gpu_memory_) : gpu_memory{gpu_memory_} {} /// Copies the instruction at `address` into `out`, loading its page first if needed. /// Returns false when the instruction is not inside a fully mapped page. bool TryRead(GPUVAddr address, Instruction& out) { if (!Holds(address)) { LoadFrom(address); } if (!Holds(address)) { return false; } out = page[(address - page_begin) / sizeof(Instruction)]; return true; } private: bool Holds(GPUVAddr address) const { return address >= page_begin && address + sizeof(Instruction) <= page_end; } // Loads from `address` up to the next page boundary; loads nothing if that range is not // fully mapped. void LoadFrom(GPUVAddr address) { const GPUVAddr next_page = (address | (PageSize - 1)) + 1; const GPUVAddr length = next_page - address; page_begin = address; page_end = address; if (length >= sizeof(Instruction) && gpu_memory.IsFullyMappedRange(address, length)) { gpu_memory.ReadBlock(address, page.data(), length); page_end = next_page; } } Tegra::MemoryManager& gpu_memory; std::array page; GPUVAddr page_begin{}; GPUVAddr page_end{}; }; } // Anonymous namespace // A native program is captured and lowered once, when the shader cache first meets it. The // environments built later for its pipelines receive that result instead of lowering again. void GenericEnvironment::InitializeNativeCode(std::shared_ptr lowered, u32 source_header_size) { if (!lowered) { lowered = LowerNativeCode(source_header_size); } if (lowered->texture_bound != ~0U) { // Native TEX carries its cbuf index explicitly; TEXS obtains it from the environment. texture_bound = lowered->texture_bound; } native_source_size = lowered->source_size; code = lowered->code; cached_lowest = 0; cached_highest = static_cast((code.size() - 1) * sizeof(u64)); lowered_native = std::move(lowered); } std::shared_ptr GenericEnvironment::LowerNativeCode(u32 source_header_size) { using Shader::Ampere::Instruction; const u32 native_header_size = source_header_size; constexpr u32 maximum_instructions = Shader::Ampere::ProgramCaptureState::MaximumInstructions; std::vector native; Shader::Ampere::ProgramCaptureState capture; NativeInstructionReader reader{*gpu_memory}; for (u32 index = 0; index < maximum_instructions; ++index) { const GPUVAddr address = program_base + native_header_size + index * sizeof(Instruction); Instruction instruction; if (!reader.TryRead(address, instruction)) { // Not inside a fully mapped page: check and read this one instruction on its own. if (!gpu_memory->IsFullyMappedRange(address, sizeof(Instruction))) { // The program may end before a branch target past its last EXIT (the capture // can't tell); stop at the mapping's end and let the lowering judge what was // captured. if (!native.empty()) break; throw Shader::RuntimeError("Unmapped native shader instruction at {:#x}", address); } gpu_memory->ReadBlock(address, &instruction, sizeof(instruction)); } native.push_back(instruction); const auto status = capture.Observe(instruction, index); if (status == Shader::Ampere::ProgramCaptureState::Result::InvalidTarget) { if (const char* dir = std::getenv("EDEN_NX2_SHADER_DUMP")) { const auto path = fmt::format("{}/rejected-{:x}.bin", dir, program_base + native_header_size); if (std::FILE* file = std::fopen(path.c_str(), "wb")) { std::fwrite(native.data(), sizeof(Instruction), std::min(native.size(), 4096), file); std::fclose(file); } } throw Shader::RuntimeError("Native shader branch outside capture bounds at {:#x} +{:#x} [{:08x} {:08x} {:08x} {:08x}]", program_base + native_header_size, index * 16, instruction.words[0], instruction.words[1], instruction.words[2], instruction.words[3]); } if (status == Shader::Ampere::ProgramCaptureState::Result::Complete) break; } auto lowered = Shader::Ampere::LowerToMaxwell(native); // EDEN_NX2_REJECT=,...: treat these programs as rejected (to find which newly // lowered program changes a game's behaviour without rebuilding). if (static const std::string reject_list = [] { const char* text = std::getenv("EDEN_NX2_REJECT"); return text ? "," + std::string{text} + "," : std::string{}; }(); !reject_list.empty() && reject_list.find(fmt::format(",{:x},", program_base)) != std::string::npos) { throw Shader::RuntimeError("Native shader at {:#x} rejected by EDEN_NX2_REJECT", program_base); } // EDEN_NX2_SHADER_DUMP=: keep every captured native program (named by its key) so the // lowering can be tested offline against all of a game's shaders. if (static const char* const dump_directory = std::getenv("EDEN_NX2_SHADER_DUMP"); dump_directory != nullptr) { const auto folder = std::filesystem::path{dump_directory}; const auto path = folder / fmt::format("{:016x}-{}-{:x}.bin", Shader::Ampere::NativeProgramKey(native), source_header_size == 0 ? "cs" : "gfx", program_base); if (!std::filesystem::exists(path)) { std::filesystem::create_directories(folder); std::ofstream dump(path, std::ios::binary); dump.write(reinterpret_cast(native.data()), native.size() * sizeof(Shader::Ampere::Instruction)); } } if (!lowered) { if (const char* directory = std::getenv("EDEN_NATIVE_SHADER_OVERRIDE_DIR")) { const auto key = fmt::format("{:016x}", Shader::Ampere::NativeProgramKey(native)); const auto folder = std::filesystem::path{directory}; std::filesystem::create_directories(folder); const auto source_path = folder / (key + ".source.bin"); const auto override_path = folder / (key + ".override.bin"); { std::ofstream dump(source_path, std::ios::binary); dump.write(reinterpret_cast(native.data()), native.size() * sizeof(Shader::Ampere::Instruction)); } { // Retain mapped bytes beyond the first EXIT for investigating // native CALL targets in compiler library subroutines. std::ofstream mapped(folder / (key + ".mapped.bin"), std::ios::binary); std::array block{}; for (u32 offset = 0; offset < 0x10000; offset += 0x1000) { if (!gpu_memory->IsFullyMappedRange(program_base + offset, block.size())) break; gpu_memory->ReadBlock(program_base + offset, block.data(), block.size()); mapped.write(reinterpret_cast(block.data()), block.size()); } } LOG_WARNING(Shader, "Native shader paused: key={}, offset={:#x}, reason={}, source={}", key, lowered.error_offset, lowered.error, source_path.string()); const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes{30}; while (!gpu_memory->IsShuttingDown() && std::chrono::steady_clock::now() < deadline) { if (std::filesystem::exists(override_path)) { std::ifstream file(override_path, std::ios::binary | std::ios::ate); const auto length = file.tellg(); if (length <= 0 || length > 16 * 1024 * 1024) { throw Shader::RuntimeError("Invalid native shader override size"); } std::vector bytes(static_cast(length)); file.seekg(0); file.read(reinterpret_cast(bytes.data()), bytes.size()); if (!file) throw Shader::RuntimeError("Native shader override read failed"); const u32 source_bytes = Shader::Ampere::ProgramOverrideSourceSize(bytes); if (source_bytes > native.size() * sizeof(Shader::Ampere::Instruction)) { if (!gpu_memory->IsFullyMappedRange(program_base + native_header_size, source_bytes)) throw Shader::RuntimeError("Unmapped extended native shader source"); native.resize(source_bytes / sizeof(Shader::Ampere::Instruction)); gpu_memory->ReadBlock(program_base + native_header_size, native.data(), source_bytes); } lowered = Shader::Ampere::DecodeProgramOverride(bytes, native); if (!lowered) throw Shader::RuntimeError("Native shader override rejected: {}", lowered.error); LOG_WARNING(Shader, "Native shader resumed: key={}, instructions={}", key, lowered.instructions.size()); break; } std::this_thread::sleep_for(std::chrono::milliseconds{100}); } } } if (!lowered) { const size_t failed = lowered.error_offset / sizeof(Shader::Ampere::Instruction); const auto words = failed < native.size() ? native[failed].words : std::array{}; throw Shader::RuntimeError( "Native shader at {:#x}, instruction {:#x}: {} [op {:03x} {:08x} {:08x} {:08x} {:08x}]", program_base, lowered.error_offset, lowered.error, words[0] & 0xfff, words[0], words[1], words[2], words[3]); } auto result = std::make_shared(); result->texture_bound = lowered.required_texture_buffer; result->source_size = native_header_size + lowered.source_size; auto& lowered_code = result->code; lowered_code.resize(sizeof(sph) / sizeof(u64)); std::memcpy(lowered_code.data(), &sph, sizeof(sph)); for (const u64 instruction : lowered.instructions) { if ((lowered_code.size() * sizeof(u64)) % 32 == 0) { lowered_code.push_back(0); // Maxwell scheduling word, absent from native instructions. } lowered_code.push_back(instruction); } // Keep the original padding ABI, then retain the native address map as // unreachable data. Existing disk-cache serialization carries it with code. lowered_code.push_back(0); Shader::Ampere::AppendNativeAddressMap(lowered_code, lowered.source_instruction_offsets); LOG_DEBUG(Shader, "Lowered native shader at {:#x}: {} source bytes, {} instructions", program_base, result->source_size, lowered.instructions.size()); return result; } GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, Tegra::MemoryManager& gpu_memory_, Maxwell::ShaderType program, GPUVAddr program_base_, u32 start_address_, std::shared_ptr lowered_native_) : GenericEnvironment{gpu_memory_, program_base_, start_address_}, maxwell3d{&maxwell3d_} { gpu_memory->ReadBlock(program_base + start_address, &sph, sizeof(sph)); initial_offset = sizeof(sph); texture_bound = maxwell3d->regs.bindless_texture_const_buffer_slot; const auto& regs{maxwell3d->regs}; const bool native{maxwell3d->GetNativeMacroEngine().native_turing}; // Task and mesh programs: the task in the vertex slot, its mesh program in the tessellation // slot (without a task, the mesh program is in the vertex slot). const bool mesh_slot{native && regs.IsMeshEnabled() && (program == Maxwell::ShaderType::VertexB || program == Maxwell::ShaderType::Tessellation)}; if (native) { if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel && !mesh_slot) { throw Shader::RuntimeError("Unsupported native graphics shader stage {}", static_cast(program)); } if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) { const u32 native_stage = program == Maxwell::ShaderType::Pixel ? 4 : 0; const auto& table = maxwell3d->state.shader_stages[native_stage].const_buffers[1]; if (table.enabled && table.size && table.size <= Tegra::Engines::Maxwell3D::Regs::MaxConstBufferSize) { std::vector bytes(table.size); gpu_memory->ReadBlock(table.address, bytes.data(), bytes.size()); std::ofstream dump(fmt::format("native-graphics-cbuf-{}-1.bin", static_cast(program)), std::ios::binary); dump.write(reinterpret_cast(bytes.data()), bytes.size()); } } InitializeNativeCode(std::move(lowered_native_)); } gp_passthrough_mask = maxwell3d->regs.post_vtg_shader_attrib_skip_mask; switch (program) { case Maxwell::ShaderType::VertexA: stage = Shader::Stage::VertexA; stage_index = 0; break; case Maxwell::ShaderType::VertexB: stage = Shader::Stage::VertexB; stage_index = 0; break; case Maxwell::ShaderType::TessellationInit: stage = Shader::Stage::TessellationControl; stage_index = 1; break; case Maxwell::ShaderType::Tessellation: stage = Shader::Stage::TessellationEval; stage_index = 2; break; case Maxwell::ShaderType::Geometry: stage = Shader::Stage::Geometry; stage_index = 3; break; case Maxwell::ShaderType::Pixel: stage = Shader::Stage::Fragment; stage_index = 4; break; default: ASSERT_MSG(false, "Invalid program={}", program); break; } if (mesh_slot) { const bool has_task{regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation)}; stage = program == Maxwell::ShaderType::VertexB && has_task ? Shader::Stage::Task : Shader::Stage::Mesh; mesh_registers = {regs.reg_array[Maxwell::MeshShaderA], regs.reg_array[Maxwell::MeshShaderB], regs.reg_array[Maxwell::MeshInitShader]}; if (stage == Shader::Stage::Mesh && regs.IsShaderConfigEnabled(Maxwell::ShaderType::Geometry)) { const auto& geometry{regs.pipelines[static_cast(Maxwell::ShaderType::Geometry)]}; const GPUVAddr address{(static_cast(geometry.reserved[0] & 0xff) << 32) | geometry.reserved[1]}; gpu_memory->ReadBlock(address, &mesh_primitive_header, sizeof(mesh_primitive_header)); } } const u64 local_size{sph.LocalMemorySize()}; ASSERT(local_size <= (std::numeric_limits::max)()); local_memory_size = static_cast(local_size) + sph.common3.shader_local_memory_crs_size; is_proprietary_driver = maxwell3d->GetNativeMacroEngine().native_turing || texture_bound == 2; has_hle_engine_state = maxwell3d->engine_state == Tegra::Engines::Maxwell3D::EngineHint::OnHLEMacro; } 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); u32 value{}; if (cbuf_offset < cbuf.size) { value = gpu_memory->Read(cbuf.address + cbuf_offset); } cbuf_values.emplace(MakeCbufKey(cbuf_index, cbuf_offset), value); return value; } std::optional GraphicsEnvironment::GetReplaceConstBuffer(u32 bank, u32 offset) { if (!has_hle_engine_state) { return std::nullopt; } const u64 key = (static_cast(bank) << 32) | static_cast(offset); auto it = maxwell3d->replace_table.find(key); if (it == maxwell3d->replace_table.end()) { return std::nullopt; } const auto converted_value = [](Tegra::Engines::Maxwell3D::HLEReplacementAttributeType name) { switch (name) { case Tegra::Engines::Maxwell3D::HLEReplacementAttributeType::BaseVertex: return Shader::ReplaceConstant::BaseVertex; case Tegra::Engines::Maxwell3D::HLEReplacementAttributeType::BaseInstance: return Shader::ReplaceConstant::BaseInstance; case Tegra::Engines::Maxwell3D::HLEReplacementAttributeType::DrawID: return Shader::ReplaceConstant::DrawID; default: UNREACHABLE(); } }(it->second); cbuf_replacements.emplace(key, converted_value); return converted_value; } Shader::TextureType GraphicsEnvironment::ReadTextureType(u32 handle) { const auto& regs{maxwell3d->regs}; const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding}; auto entry = ReadTextureInfo(regs.tex_header.Address(), regs.tex_header.limit, via_header_index, handle); const Shader::TextureType result{ConvertTextureType(entry)}; texture_types.emplace(handle, result); return result; } Shader::TexturePixelFormat GraphicsEnvironment::ReadTexturePixelFormat(u32 handle) { const auto& regs{maxwell3d->regs}; const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding}; auto entry = ReadTextureInfo(regs.tex_header.Address(), regs.tex_header.limit, via_header_index, handle); const Shader::TexturePixelFormat result(ConvertTexturePixelFormat(entry)); texture_pixel_formats.emplace(handle, result); return result; } bool GraphicsEnvironment::IsTexturePixelFormatInteger(u32 handle) { return VideoCore::Surface::IsPixelFormatInteger( static_cast(ReadTexturePixelFormat(handle))); } u32 GraphicsEnvironment::ReadViewportTransformState() { const auto& regs{maxwell3d->regs}; viewport_transform_state = regs.viewport_scale_offset_enabled; return viewport_transform_state; } ComputeEnvironment::ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_compute_, Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_, u32 start_address_, std::shared_ptr lowered_native_) : GenericEnvironment{gpu_memory_, program_base_, start_address_}, kepler_compute{ &kepler_compute_} { const auto& qmd{kepler_compute->launch_description}; stage = Shader::Stage::Compute; local_memory_size = qmd.local_pos_alloc + qmd.local_crs_alloc; texture_bound = kepler_compute->regs.tex_cb_index; is_proprietary_driver = texture_bound == 2; shared_memory_size = qmd.shared_alloc; workgroup_size = {qmd.block_dim_x, qmd.block_dim_y, qmd.block_dim_z}; if (kepler_compute->IsNativeMode()) { initial_offset = sizeof(sph); is_proprietary_driver = true; InitializeNativeCode(std::move(lowered_native_), 0); start_address = sizeof(sph); } } 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); const auto& cbuf{qmd.const_buffer_config[cbuf_index]}; u32 value{}; if (cbuf_offset < cbuf.size) { value = gpu_memory->Read(cbuf.Address() + cbuf_offset); } cbuf_values.emplace(MakeCbufKey(cbuf_index, cbuf_offset), value); return value; } Shader::TextureType ComputeEnvironment::ReadTextureType(u32 handle) { const auto& regs{kepler_compute->regs}; const auto& qmd{kepler_compute->launch_description}; auto entry = ReadTextureInfo(regs.tic.Address(), regs.tic.limit, qmd.linked_tsc != 0, handle); const Shader::TextureType result{ConvertTextureType(entry)}; texture_types.emplace(handle, result); return result; } Shader::TexturePixelFormat ComputeEnvironment::ReadTexturePixelFormat(u32 handle) { const auto& regs{kepler_compute->regs}; const auto& qmd{kepler_compute->launch_description}; auto entry = ReadTextureInfo(regs.tic.Address(), regs.tic.limit, qmd.linked_tsc != 0, handle); const Shader::TexturePixelFormat result(ConvertTexturePixelFormat(entry)); texture_pixel_formats.emplace(handle, result); return result; } bool ComputeEnvironment::IsTexturePixelFormatInteger(u32 handle) { return VideoCore::Surface::IsPixelFormatInteger( static_cast(ReadTexturePixelFormat(handle))); } u32 ComputeEnvironment::ReadViewportTransformState() { return viewport_transform_state; } void FileEnvironment::Deserialize(std::ifstream& file) { u64 code_size{}; u64 num_texture_types{}; u64 num_texture_pixel_formats{}; u64 num_cbuf_values{}; u64 num_cbuf_replacement_values{}; file.read(reinterpret_cast(&code_size), sizeof(code_size)) .read(reinterpret_cast(&num_texture_types), sizeof(num_texture_types)) .read(reinterpret_cast(&num_texture_pixel_formats), sizeof(num_texture_pixel_formats)) .read(reinterpret_cast(&num_cbuf_values), sizeof(num_cbuf_values)) .read(reinterpret_cast(&num_cbuf_replacement_values), sizeof(num_cbuf_replacement_values)) .read(reinterpret_cast(&local_memory_size), sizeof(local_memory_size)) .read(reinterpret_cast(&texture_bound), sizeof(texture_bound)) .read(reinterpret_cast(&start_address), sizeof(start_address)) .read(reinterpret_cast(&read_lowest), sizeof(read_lowest)) .read(reinterpret_cast(&read_highest), sizeof(read_highest)) .read(reinterpret_cast(&viewport_transform_state), sizeof(viewport_transform_state)) .read(reinterpret_cast(&stage), sizeof(stage)); code.resize(Common::DivCeil(code_size, sizeof(u64))); file.read(reinterpret_cast(code.data()), code_size); for (size_t i = 0; i < num_texture_types; ++i) { u32 key; Shader::TextureType type; file.read(reinterpret_cast(&key), sizeof(key)) .read(reinterpret_cast(&type), sizeof(type)); texture_types.emplace(key, type); } for (size_t i = 0; i < num_texture_pixel_formats; ++i) { u32 key; Shader::TexturePixelFormat format; file.read(reinterpret_cast(&key), sizeof(key)) .read(reinterpret_cast(&format), sizeof(format)); texture_pixel_formats.emplace(key, format); } for (size_t i = 0; i < num_cbuf_values; ++i) { u64 key; u32 value; file.read(reinterpret_cast(&key), sizeof(key)) .read(reinterpret_cast(&value), sizeof(value)); cbuf_values.emplace(key, value); } for (size_t i = 0; i < num_cbuf_replacement_values; ++i) { u64 key; Shader::ReplaceConstant value; file.read(reinterpret_cast(&key), sizeof(key)) .read(reinterpret_cast(&value), sizeof(value)); cbuf_replacements.emplace(key, value); } if (stage == Shader::Stage::Compute) { file.read(reinterpret_cast(&workgroup_size), sizeof(workgroup_size)) .read(reinterpret_cast(&shared_memory_size), sizeof(shared_memory_size)); initial_offset = Shader::Ampere::HasNativeAddressMap(code) ? sizeof(sph) : 0; } else { file.read(reinterpret_cast(&sph), sizeof(sph)); initial_offset = sizeof(sph); if (stage == Shader::Stage::Geometry) { file.read(reinterpret_cast(&gp_passthrough_mask), sizeof(gp_passthrough_mask)); } } // Native programs fold bank 1 reads as they did when they were built. is_proprietary_driver = texture_bound == 2 || Shader::Ampere::HasNativeAddressMap(code); } void FileEnvironment::Dump(u64 pipeline_hash, u64 shader_hash) { DumpImpl(pipeline_hash, shader_hash, code, read_highest, read_lowest, initial_offset, stage); } u32 FileEnvironment::TranslateProgramAddress(u32 address) const { if (const auto translated = Shader::Ampere::NativeProgramAddress(code, address)) return *translated; if (Shader::Ampere::HasNativeAddressMap(code)) throw Shader::RuntimeError("Invalid cached native indirect branch address {:#x}", address); return address; } u64 FileEnvironment::ReadInstruction(u32 address) { if (address < read_lowest || address > read_highest) { throw Shader::LogicError("Out of bounds address {}", address); } return code[(address - read_lowest) / sizeof(u64)]; } 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()) { throw Shader::LogicError("Uncached read texture type"); } return it->second; } Shader::TextureType FileEnvironment::ReadTextureType(u32 handle) { const auto it{texture_types.find(handle)}; if (it == texture_types.end()) { throw Shader::LogicError("Uncached read texture type"); } return it->second; } Shader::TexturePixelFormat FileEnvironment::ReadTexturePixelFormat(u32 handle) { const auto it{texture_pixel_formats.find(handle)}; if (it == texture_pixel_formats.end()) { throw Shader::LogicError("Uncached read texture pixel format"); } return it->second; } bool FileEnvironment::IsTexturePixelFormatInteger(u32 handle) { return VideoCore::Surface::IsPixelFormatInteger( static_cast(ReadTexturePixelFormat(handle))); } u32 FileEnvironment::ReadViewportTransformState() { return viewport_transform_state; } u32 FileEnvironment::LocalMemorySize() const { return local_memory_size; } u32 FileEnvironment::SharedMemorySize() const { return shared_memory_size; } u32 FileEnvironment::TextureBoundBuffer() const { return texture_bound; } std::array FileEnvironment::WorkgroupSize() const { return workgroup_size; } std::optional FileEnvironment::GetReplaceConstBuffer(u32 bank, u32 offset) { const u64 key = (static_cast(bank) << 32) | static_cast(offset); auto it = cbuf_replacements.find(key); if (it == cbuf_replacements.end()) { return std::nullopt; } return it->second; } void SerializePipeline(std::span key, std::span envs, const std::filesystem::path& filename, u32 cache_version) try { std::ofstream file(filename, std::ios::binary | std::ios::ate | std::ios::app); file.exceptions(std::ifstream::failbit); if (!file.is_open()) { LOG_ERROR(Common_Filesystem, "Failed to open pipeline cache file {}", Common::FS::PathToUTF8String(filename)); return; } if (file.tellp() == 0) { // Write header file.write(MAGIC_NUMBER.data(), MAGIC_NUMBER.size()) .write(reinterpret_cast(&cache_version), sizeof(cache_version)); } if (!std::ranges::all_of(envs, &GenericEnvironment::CanBeSerialized)) { return; } const u32 num_envs{static_cast(envs.size())}; file.write(reinterpret_cast(&num_envs), sizeof(num_envs)); for (const GenericEnvironment* const env : envs) { env->Serialize(file); } file.write(key.data(), key.size_bytes()); } catch (const std::ios_base::failure& e) { LOG_ERROR(Common_Filesystem, "{}", e.what()); if (!Common::FS::RemoveFile(filename)) { LOG_ERROR(Common_Filesystem, "Failed to delete pipeline cache file {}", Common::FS::PathToUTF8String(filename)); } } void LoadPipelines( std::stop_token stop_loading, const std::filesystem::path& filename, u32 expected_cache_version, Common::UniqueFunction load_compute, Common::UniqueFunction> load_graphics) try { std::ifstream file(filename, std::ios::binary | std::ios::ate); if (!file.is_open()) { return; } file.exceptions(std::ifstream::failbit); const auto end{file.tellg()}; file.seekg(0, std::ios::beg); std::array magic_number; u32 cache_version; file.read(magic_number.data(), magic_number.size()) .read(reinterpret_cast(&cache_version), sizeof(cache_version)); if (magic_number != MAGIC_NUMBER || cache_version != expected_cache_version) { file.close(); if (Common::FS::RemoveFile(filename)) { if (magic_number != MAGIC_NUMBER) { LOG_ERROR(Common_Filesystem, "Invalid pipeline cache file"); } if (cache_version != expected_cache_version) { LOG_INFO(Common_Filesystem, "Deleting old pipeline cache"); } } else { LOG_ERROR(Common_Filesystem, "Invalid pipeline cache file and failed to delete it in \"{}\"", Common::FS::PathToUTF8String(filename)); } return; } while (file.tellg() != end) { if (stop_loading.stop_requested()) { return; } u32 num_envs{}; file.read(reinterpret_cast(&num_envs), sizeof(num_envs)); std::vector envs(num_envs); for (FileEnvironment& env : envs) { env.Deserialize(file); } if (envs.front().ShaderStage() == Shader::Stage::Compute) { load_compute(file, std::move(envs.front())); } else { load_graphics(file, std::move(envs)); } } } catch (const std::ios_base::failure& e) { LOG_ERROR(Common_Filesystem, "{}", e.what()); if (!Common::FS::RemoveFile(filename)) { LOG_ERROR(Common_Filesystem, "Failed to delete pipeline cache file {}", Common::FS::PathToUTF8String(filename)); } } } // namespace VideoCommon