From 02504aeb37cb3ee07dc421acc0570ad5ba25d53f Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:02:52 -0400 Subject: video_core: draw Switch 2 task/mesh pipelines with VK_EXT_mesh_shader While SET_MESH_CONTROL is on, a native draw binds a task program in the vertex slot, its mesh program in the tessellation slot and a header-only pass-through geometry program naming the per-primitive outputs. Translate the first two as task and mesh programs with the mesh registers (SET_MESH_SHADER_A/B, SET_MESH_INIT_SHADER) and that header, key their pipelines by them, and build pipelines with task/mesh stages and no vertex input, input assembly or tessellation state. The draw's vertex count is the number of task workgroups: record vkCmdDrawMeshTasksEXT. Enable VK_EXT_mesh_shader (task and mesh only) when the host has it; draws are skipped without it, as they were before. --- src/video_core/shader_cache.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'src/video_core/shader_cache.cpp') diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index dfe80abac3..f03df6bac5 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -14,6 +14,7 @@ #include #include "common/assert.h" +#include "common/cityhash.h" #include "shader_recompiler/frontend/maxwell/control_flow.h" #include "shader_recompiler/exception.h" #include "shader_recompiler/object_pool.h" @@ -48,6 +49,22 @@ void ShaderCache::SyncGuestHost() { ShaderCache::ShaderCache(Tegra::MaxwellDeviceMemoryManager& device_memory_) : device_memory{device_memory_} {} +// Task and mesh programs are translated with the mesh registers and the header of the pass-through +// geometry program (per-primitive outputs), so pipelines are keyed by them too. +u64 ShaderCache::MeshStateHash() const { + using Regs = Tegra::Engines::Maxwell3D::Regs; + const auto& regs{maxwell3d->regs}; + std::array state{regs.reg_array[Regs::MeshShaderA], regs.reg_array[Regs::MeshShaderB], + regs.reg_array[Regs::MeshInitShader]}; + if (regs.IsShaderConfigEnabled(Regs::ShaderType::Geometry)) { + const auto& geometry{regs.pipelines[static_cast(Regs::ShaderType::Geometry)]}; + const GPUVAddr address{(static_cast(geometry.reserved[0] & 0xff) << 32) | + geometry.reserved[1]}; + gpu_memory->ReadBlockUnsafe(address, state.data() + 3, 20 * sizeof(u32)); + } + return Common::CityHash64(reinterpret_cast(state.data()), sizeof(state)) | 1; +} + bool ShaderCache::RefreshStages(std::array& unique_hashes) try { auto& dirty{maxwell3d->dirty.flags}; if (!dirty[VideoCommon::Dirty::Shaders]) { @@ -57,6 +74,7 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { const GPUVAddr base_addr{maxwell3d->regs.program_region.Address()}; const bool native = maxwell3d->GetNativeMacroEngine().native_turing; + const bool mesh{native && maxwell3d->regs.IsMeshEnabled()}; for (size_t index = 0; index < Tegra::Engines::Maxwell3D::Regs::MaxShaderProgram; ++index) { if (!maxwell3d->regs.IsShaderConfigEnabled(index)) { unique_hashes[index] = 0; @@ -69,6 +87,11 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { unique_hashes[index] = 0; continue; } + if (mesh && program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Geometry) { + // A header without code: the mesh program's environment reads its output map. + unique_hashes[index] = 0; + continue; + } const GPUVAddr shader_addr{native ? (static_cast(shader_config.reserved[0] & 0xff) << 32) | shader_config.reserved[1] : base_addr + shader_config.offset}; @@ -111,6 +134,10 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { } shader_infos[index] = shader_info; unique_hashes[index] = shader_info->unique_hash; + if (mesh && (program == Tegra::Engines::Maxwell3D::Regs::ShaderType::VertexB || + program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Tessellation)) { + unique_hashes[index] ^= MeshStateHash(); + } } last_shaders_valid = true; return true; -- cgit v1.2.3 From 10e70639058b9adc4f7fc77e11982bb834fce86b Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Sun, 1 Nov 2026 03:20:42 +0100 Subject: nx2: capture and lower native shader programs once Native programs were captured and lowered when their ShaderInfo was made and again for every environment built for their pipelines. The ShaderInfo now keeps the lowered code and the environments adopt it. Capture also reads a 4 KiB page at a time instead of flushing caches per instruction, and cached native programs fold bank 1 reads like live ones. Ported from Wael3rd/Eglantine-MKW (0ae093058e, 65a6aa877a, df076133c4). Assisted-by: Claude Sonnet 5.5 (porting the fixes) (cherry picked from commit 3af11ac401ad3061c183281d8c98c16c1d6da456) --- src/video_core/renderer_opengl/gl_shader_cache.cpp | 3 +- .../renderer_vulkan/vk_pipeline_cache.cpp | 3 +- src/video_core/shader_cache.cpp | 7 +- src/video_core/shader_cache.h | 2 + src/video_core/shader_environment.cpp | 135 ++++++++++++++++----- src/video_core/shader_environment.h | 29 ++++- 6 files changed, 144 insertions(+), 35 deletions(-) (limited to 'src/video_core/shader_cache.cpp') diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp index 277cf222fa..ac750ab747 100644 --- a/src/video_core/renderer_opengl/gl_shader_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp @@ -578,7 +578,8 @@ std::unique_ptr ShaderCache::CreateComputePipeline( const ComputePipelineKey& key, const VideoCommon::ShaderInfo* shader) { const GPUVAddr program_base{kepler_compute->regs.code_loc.Address()}; const auto& qmd{kepler_compute->launch_description}; - ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start}; + ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start, + shader->lowered_native}; env.SetCachedSize(shader->size_bytes); main_pools.ReleaseContents(); diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 942be5351b..ff8e84b019 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -1268,7 +1268,8 @@ std::unique_ptr PipelineCache::CreateComputePipeline( }; const GPUVAddr program_base{kepler_compute->regs.code_loc.Address()}; const auto& qmd{kepler_compute->launch_description}; - ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start}; + ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start, + shader->lowered_native}; env.SetCachedSize(shader->size_bytes); main_pools.ReleaseContents(); diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp index f03df6bac5..5faed0dba9 100644 --- a/src/video_core/shader_cache.cpp +++ b/src/video_core/shader_cache.cpp @@ -112,7 +112,8 @@ bool ShaderCache::RefreshStages(std::array& unique_hashes) try { if (!shader_info) { const u32 start_address{native ? 0 : shader_config.offset}; try { - // Native programs are lowered while the environment is built. + // Native programs are lowered while the environment is built (once: the + // ShaderInfo keeps the result for the pipeline's environments). RethrowIfRejected(*cpu_shader_addr); GraphicsEnvironment env{*maxwell3d, *gpu_memory, program, native ? shader_addr : base_addr, start_address}; shader_info = MakeShaderInfo(env, *cpu_shader_addr); @@ -229,7 +230,8 @@ void ShaderCache::GetGraphicsEnvironments(GraphicsEnvironments& result, const GPUVAddr address = native ? (static_cast(config.reserved[0] & 0xff) << 32) | config.reserved[1] : base_addr; - env = GraphicsEnvironment{*maxwell3d, *gpu_memory, program, address, start_address}; + env = GraphicsEnvironment{*maxwell3d, *gpu_memory, program, address, start_address, + shader_infos[index]->lowered_native}; env.SetCachedSize(shader_infos[index]->size_bytes); result.env_ptrs[env_index++] = &env; } @@ -369,6 +371,7 @@ const ShaderInfo* ShaderCache::MakeShaderInfo(GenericEnvironment& env, VAddr cpu info->unique_hash = env.CalculateHash(); info->size_bytes = env.ReadSizeBytes(); } + info->lowered_native = env.LoweredNative(); const size_t size_bytes{env.SourceSizeBytes() ? env.SourceSizeBytes() : info->size_bytes}; const ShaderInfo* const result{info.get()}; Register(std::move(info), cpu_addr, size_bytes); diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index 80e375a38b..281a90cb6f 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -38,6 +38,8 @@ class GenericEnvironment; struct ShaderInfo { u64 unique_hash{}; size_t size_bytes{}; + /// Lowered code of a native program, reused when its pipelines are built. + std::shared_ptr lowered_native; }; class ShaderCache : public VideoCommon::ChannelSetupCaches { diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index be1dfe482b..22e661c849 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -5,6 +5,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include #include #include @@ -323,22 +324,99 @@ Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, return entry; } -void GenericEnvironment::InitializeNativeCode(u32 source_header_size) { +namespace { + +// Reads a native program a 4 KiB page at a time instead of one 16-byte instruction at a time. +// The reads do not flush the GPU caches first (ReadBlockUnsafe): the previous per-instruction +// ReadBlock did, 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.ReadBlockUnsafe(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); - 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); + 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->ReadBlockUnsafe(address, &instruction, sizeof(instruction)); } - auto& instruction = native.emplace_back(); - 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")) { @@ -440,33 +518,32 @@ void GenericEnvironment::InitializeNativeCode(u32 source_header_size) { program_base, lowered.error_offset, lowered.error, words[0] & 0xfff, words[0], words[1], words[2], words[3]); } - if (lowered.required_texture_buffer != ~0U) { - // Native TEX carries its cbuf index explicitly; TEXS obtains it from the environment. - texture_bound = lowered.required_texture_buffer; - } - native_source_size = native_header_size + lowered.source_size; - code.resize(sizeof(sph) / sizeof(u64)); - std::memcpy(code.data(), &sph, sizeof(sph)); + 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 ((code.size() * sizeof(u64)) % 32 == 0) { - code.push_back(0); // Maxwell scheduling word, absent from native instructions. + if ((lowered_code.size() * sizeof(u64)) % 32 == 0) { + lowered_code.push_back(0); // Maxwell scheduling word, absent from native instructions. } - code.push_back(instruction); + 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. - code.push_back(0); - Shader::Ampere::AppendNativeAddressMap(code, lowered.source_instruction_offsets); - cached_lowest = 0; - cached_highest = static_cast((code.size() - 1) * sizeof(u64)); + 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, native_source_size, lowered.instructions.size()); + 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_) + 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); @@ -493,7 +570,7 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, dump.write(reinterpret_cast(bytes.data()), bytes.size()); } } - InitializeNativeCode(); + InitializeNativeCode(std::move(lowered_native_)); } gp_passthrough_mask = maxwell3d->regs.post_vtg_shader_attrib_skip_mask; switch (program) { @@ -616,7 +693,8 @@ u32 GraphicsEnvironment::ReadViewportTransformState() { ComputeEnvironment::ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_compute_, Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_, - u32 start_address_) + 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}; @@ -629,7 +707,7 @@ ComputeEnvironment::ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_com if (kepler_compute->IsNativeMode()) { initial_offset = sizeof(sph); is_proprietary_driver = true; - InitializeNativeCode(0); + InitializeNativeCode(std::move(lowered_native_), 0); start_address = sizeof(sph); } } @@ -734,7 +812,8 @@ void FileEnvironment::Deserialize(std::ifstream& file) { file.read(reinterpret_cast(&gp_passthrough_mask), sizeof(gp_passthrough_mask)); } } - is_proprietary_driver = texture_bound == 2; + // 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) { diff --git a/src/video_core/shader_environment.h b/src/video_core/shader_environment.h index e49f01d95f..30d59955af 100644 --- a/src/video_core/shader_environment.h +++ b/src/video_core/shader_environment.h @@ -29,6 +29,19 @@ class Memorymanager; namespace VideoCommon { +/// A native (Switch 2) program after capture and lowering to Maxwell code. The shader cache keeps +/// it so the environments built for the program's pipelines don't capture and lower it again. +struct LoweredNativeCode { + /// Shader header, lowered instructions with Maxwell scheduling words, then the native + /// address map (what the environment's `code` used to be filled with). + std::vector code; + /// Bytes of the native program that were read, header included. + size_t source_size{}; + /// Constant buffer holding the texture handles if the program requires one, otherwise ~0U + /// (the engine's register value is used). + u32 texture_bound = ~0U; +}; + class GenericEnvironment : public Shader::Environment { public: explicit GenericEnvironment() = default; @@ -63,6 +76,10 @@ public: return native_source_size; } + [[nodiscard]] const std::shared_ptr& LoweredNative() const noexcept { + return lowered_native; + } + [[nodiscard]] bool CanBeSerialized() const noexcept; [[nodiscard]] u64 CalculateHash() const; @@ -78,8 +95,12 @@ public: protected: std::optional TryFindSize(); - void InitializeNativeCode(u32 source_header_size = 128); + /// Adopts an already lowered native program, or captures and lowers it when there is none. + void InitializeNativeCode(std::shared_ptr lowered, + u32 source_header_size = 128); + std::shared_ptr LowerNativeCode(u32 source_header_size); size_t native_source_size{}; + std::shared_ptr lowered_native; Tegra::Texture::TICEntry ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit, bool via_header_index, u32 raw); @@ -117,7 +138,8 @@ public: explicit GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, Tegra::MemoryManager& gpu_memory_, Tegra::Engines::Maxwell3D::Regs::ShaderType program, - GPUVAddr program_base_, u32 start_address_); + GPUVAddr program_base_, u32 start_address_, + std::shared_ptr lowered_native_ = {}); ~GraphicsEnvironment() override = default; @@ -143,7 +165,8 @@ public: explicit ComputeEnvironment() = default; explicit ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_compute_, Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_, - u32 start_address_); + u32 start_address_, + std::shared_ptr lowered_native_ = {}); ~ComputeEnvironment() override = default; -- cgit v1.2.3