From 049c9633edd9b3f7eaed6b4b3a18a4ead0cd18b7 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:31:41 -0400 Subject: nx2: read texture handles that hold a header byte offset Switch Sports' scenery shaders take their 64-bit texture handles from a uniform register, and the texture part of those handles is the header's byte offset in the pool (index * 32), not its index: 0x96e46000 names header 8960 (a 640x152 2D texture), while the plain reading, 286720, runs far past the 33067-entry pool. Eden skipped them at draw time (null view) and built the shaders for a garbage header, so the island's ground, cliffs and buildings drew black. When a handle's index falls past the pool but index / 32 lands inside it, take that header, both when building the shader and when binding the view. Handles that already resolve keep their meaning. --- src/video_core/shader_environment.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/video_core/shader_environment.cpp') diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index 9d189ad4f0..532b1f97f4 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -315,8 +315,9 @@ std::optional GenericEnvironment::TryFindSize() { 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)}; - ASSERT(handle.first <= tic_limit); - const GPUVAddr descriptor_addr{tic_addr + handle.first * sizeof(Tegra::Texture::TICEntry)}; + const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit)}; + 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; -- cgit v1.2.3 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/dirty_flags.cpp | 2 ++ src/video_core/engines/maxwell_3d.h | 13 ++++++++ src/video_core/renderer_vulkan/maxwell_to_vk.cpp | 4 +++ .../renderer_vulkan/vk_graphics_pipeline.cpp | 35 ++++++++++++++++++---- .../renderer_vulkan/vk_graphics_pipeline.h | 5 ++++ .../renderer_vulkan/vk_pipeline_cache.cpp | 15 +++++++++- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 20 +++++++++++++ src/video_core/renderer_vulkan/vk_rasterizer.h | 1 + src/video_core/shader_cache.cpp | 27 +++++++++++++++++ src/video_core/shader_cache.h | 2 ++ src/video_core/shader_environment.cpp | 25 ++++++++++++++-- src/video_core/vulkan_common/vulkan_device.cpp | 9 ++++++ src/video_core/vulkan_common/vulkan_device.h | 7 +++++ src/video_core/vulkan_common/vulkan_wrapper.cpp | 1 + src/video_core/vulkan_common/vulkan_wrapper.h | 5 ++++ 15 files changed, 163 insertions(+), 8 deletions(-) (limited to 'src/video_core/shader_environment.cpp') diff --git a/src/video_core/dirty_flags.cpp b/src/video_core/dirty_flags.cpp index e6856aaca3..2f359749aa 100644 --- a/src/video_core/dirty_flags.cpp +++ b/src/video_core/dirty_flags.cpp @@ -64,6 +64,8 @@ void SetupDirtyRenderTargets(Maxwell3D::DirtyState::Tables& tables) { void SetupDirtyShaders(Maxwell3D::DirtyState::Tables& tables) { FillBlock(tables[0], OFF(pipelines), NUM(pipelines[0]) * Maxwell3D::Regs::MaxShaderProgram, Shaders); + // Task and mesh programs are translated with the mesh registers. + FillBlock(tables[1], Maxwell3D::Regs::MeshControl, 4, Shaders); } } // Anonymous namespace diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h index 0121f1a01c..3e946db1fe 100644 --- a/src/video_core/engines/maxwell_3d.h +++ b/src/video_core/engines/maxwell_3d.h @@ -2541,6 +2541,19 @@ public: return IsShaderConfigEnabled(static_cast(type)); } + // Switch 2 (NVN) task and mesh pipelines (clc597.h): SET_MESH_CONTROL, SET_MESH_SHADER_A + // and _B, SET_MESH_INIT_SHADER. While mesh control is on, the vertex slot holds the task + // program (or the mesh program when there is no task), the tessellation slot the mesh + // program and the geometry slot a pass-through header naming the per-primitive outputs. + static constexpr std::size_t MeshControl = 0x114c / 4; + static constexpr std::size_t MeshShaderA = 0x1150 / 4; + static constexpr std::size_t MeshShaderB = 0x1154 / 4; + static constexpr std::size_t MeshInitShader = 0x1158 / 4; + + bool IsMeshEnabled() const { + return (reg_array[MeshControl] & 1) != 0; + } + struct ConstantBuffer { u32 size; u32 address_high; diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp index c8e43d0650..e03e5786fe 100644 --- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp +++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp @@ -333,6 +333,10 @@ VkShaderStageFlagBits ShaderStage(Shader::Stage stage) { return VK_SHADER_STAGE_FRAGMENT_BIT; case Shader::Stage::Compute: return VK_SHADER_STAGE_COMPUTE_BIT; + case Shader::Stage::Task: + return VK_SHADER_STAGE_TASK_BIT_EXT; + case Shader::Stage::Mesh: + return VK_SHADER_STAGE_MESH_BIT_EXT; } UNIMPLEMENTED_MSG("Unimplemented shader stage={}", stage); return {}; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 94bace432c..86b6e5a983 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -54,6 +54,17 @@ using VideoCore::Surface::PixelFormatFromRenderTargetFormat; constexpr size_t NUM_STAGES = Maxwell::MaxShaderStage; +// The stage of a program: task and mesh programs sit in the vertex and tessellation slots. +VkShaderStageFlagBits StageFlag(const Shader::Info& info, VkShaderStageFlagBits slot_stage) { + if (info.is_task) { + return VK_SHADER_STAGE_TASK_BIT_EXT; + } + if (info.is_mesh) { + return VK_SHADER_STAGE_MESH_BIT_EXT; + } + return slot_stage; +} + DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span infos) { DescriptorLayoutBuilder builder{device}; for (size_t index = 0; index < infos.size(); ++index) { @@ -64,7 +75,7 @@ DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span(shader_stages.size()), .pStages = shader_stages.data(), - .pVertexInputState = &vertex_input_ci, - .pInputAssemblyState = &input_assembly_ci, - .pTessellationState = &tessellation_ci, + .pVertexInputState = is_mesh ? nullptr : &vertex_input_ci, + .pInputAssemblyState = is_mesh ? nullptr : &input_assembly_ci, + .pTessellationState = is_mesh ? nullptr : &tessellation_ci, .pViewportState = &viewport_ci, .pRasterizationState = &rasterization_ci, .pMultisampleState = &multisample_ci, diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index b1ac5a3fcc..daee7910c1 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -98,6 +98,11 @@ public: bool UsesExtendedDynamicState() const noexcept { return key.state.extended_dynamic_state != 0; } + + /// Switch 2 task/mesh pipeline: drawn with vkCmdDrawMeshTasksEXT. + [[nodiscard]] bool IsMeshPipeline() const noexcept { + return stage_infos[0].is_task || stage_infos[0].is_mesh; + } GraphicsPipeline& operator=(GraphicsPipeline&&) noexcept = delete; GraphicsPipeline(GraphicsPipeline&&) noexcept = delete; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 2c64f3375b..03f390d272 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -170,6 +170,14 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program } else { info.previous_stage_stores.mask.set(); } + if (previous_program && previous_program->stage == Shader::Stage::Mesh) { + for (const u32 line : previous_program->mesh.primitive_lines) { + const auto attribute{static_cast(line)}; + if (Shader::IR::IsGeneric(attribute)) { + info.per_primitive_generics.set(Shader::IR::GenericAttributeIndex(attribute)); + } + } + } const Shader::Stage stage{program.stage}; const bool has_geometry{key.unique_hashes[4] != 0 && !programs[4].is_geometry_passthrough}; const bool gl_ndc{key.state.ndc_minus_one_to_one != 0}; @@ -230,6 +238,9 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program return Shader::TessSpacing::Equal; }(); break; + case Shader::Stage::Mesh: + info.convert_depth_mode = gl_ndc; + break; case Shader::Stage::Geometry: if (program.output_topology == Shader::OutputTopology::PointList) { info.fixed_state_point_size = point_size; @@ -374,7 +385,9 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Shader::Stage::TessellationEval) | subgroup_stage_bit(VK_SHADER_STAGE_GEOMETRY_BIT, Shader::Stage::Geometry) | subgroup_stage_bit(VK_SHADER_STAGE_FRAGMENT_BIT, Shader::Stage::Fragment) | - subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute)}; + subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute) | + subgroup_stage_bit(VK_SHADER_STAGE_TASK_BIT_EXT, Shader::Stage::Task) | + subgroup_stage_bit(VK_SHADER_STAGE_MESH_BIT_EXT, Shader::Stage::Mesh)}; if (driver_id == VK_DRIVER_ID_MESA_TURNIP) { supported_subgroup_stages &= ~(1u << static_cast(Shader::Stage::Geometry)); } diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index cb31a0e7e4..0109117344 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -279,6 +279,12 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) { report_failure("no pipeline"); return; } + // Task/mesh pipelines: the draw's vertex count is the number of task workgroups. + mesh_draw = pipeline->IsMeshPipeline(); + if (mesh_draw && !device.IsExtMeshShaderSupported()) { + report_failure("no VK_EXT_mesh_shader"); + return; + } std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; // update engine as channel may be different. pipeline->SetEngine(maxwell3d, gpu_memory); @@ -541,6 +547,12 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) { const u32 num_instances{instance_count}; const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)}; + if (mesh_draw) { + scheduler.Record([groups = draw_params.num_vertices](vk::CommandBuffer cmdbuf) { + cmdbuf.DrawMeshTasksEXT(groups, 1, 1); + }); + return; + } scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) { if (draw_params.is_indexed) { cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances, @@ -573,6 +585,14 @@ void RasterizerVulkan::DrawIndirect() { const auto& params = maxwell3d->draw_manager.indirect_state; buffer_cache.SetDrawIndirect(¶ms); PrepareDraw(params.is_indexed, [this, ¶ms] { + if (mesh_draw) { + // Indirect task/mesh draws would need vkCmdDrawMeshTasksIndirectEXT's layout. + static bool logged{}; + if (!std::exchange(logged, true)) { + LOG_WARNING(Render_Vulkan, "Indirect task/mesh draw skipped"); + } + return; + } const auto indirect_buffer = buffer_cache.GetDrawIndirectBuffer(); const auto& buffer = indirect_buffer.first; const auto& offset = indirect_buffer.second; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index ae9929ca0b..e8989dcdfc 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -230,6 +230,7 @@ private: boost::container::static_vector sampler_handles; u32 draw_counter = 0; + bool mesh_draw{}; ///< the draw being prepared uses a task/mesh pipeline }; } // namespace Vulkan 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; diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h index f4179dd435..80e375a38b 100644 --- a/src/video_core/shader_cache.h +++ b/src/video_core/shader_cache.h @@ -151,6 +151,8 @@ private: bool IsRejected(VAddr addr); /// Remembers the exception being handled as this address's rejection. void RememberRejection(VAddr addr); + /// Hash of the state task and mesh programs are translated with. + u64 MeshStateHash() const; Tegra::MaxwellDeviceMemoryManager& device_memory; diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp index 9d189ad4f0..78438769f5 100644 --- a/src/video_core/shader_environment.cpp +++ b/src/video_core/shader_environment.cpp @@ -470,8 +470,16 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::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; - if (maxwell3d->GetNativeMacroEngine().native_turing) { - if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel) { + 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")) { @@ -516,6 +524,19 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_, 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; diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 623ab7c30d..29b0c816fb 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1511,6 +1511,15 @@ void Device::RemoveUnsuitableExtensions() { VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME); } + // VK_EXT_mesh_shader: Switch 2 titles draw some geometry with task and mesh programs. Only + // the two stages are used; the multiview, shading rate and query features need more. + features.mesh_shader.multiviewMeshShader = VK_FALSE; + features.mesh_shader.primitiveFragmentShadingRateMeshShader = VK_FALSE; + features.mesh_shader.meshShaderQueries = VK_FALSE; + extensions.mesh_shader = features.mesh_shader.taskShader && features.mesh_shader.meshShader; + RemoveExtensionFeatureIfUnsuitable(extensions.mesh_shader, features.mesh_shader, + VK_EXT_MESH_SHADER_EXTENSION_NAME); + // VK_KHR_shader_quad_control extensions.shader_quad_control = features.shader_quad_control.shaderQuadControl; RemoveExtensionFeatureIfUnsuitable(extensions.shader_quad_control, features.shader_quad_control, diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index d8524f6575..0d256c1dd9 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -65,6 +65,7 @@ VK_DEFINE_HANDLE(VmaAllocator) FEATURE(EXT, 4444Formats, 4444_FORMATS, format_a4b4g4r4) \ FEATURE(EXT, IndexTypeUint8, INDEX_TYPE_UINT8, index_type_uint8) \ FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \ + FEATURE(EXT, MeshShader, MESH_SHADER, mesh_shader) \ FEATURE(EXT, PrimitiveTopologyListRestart, PRIMITIVE_TOPOLOGY_LIST_RESTART, \ primitive_topology_list_restart) \ FEATURE(EXT, ProvokingVertex, PROVOKING_VERTEX, provoking_vertex) \ @@ -610,6 +611,12 @@ FN_MAX_LIMIT_LIST return extensions.shader_quad_control && features.shader_quad_control.shaderQuadControl; } + /// Returns true if the device supports task and mesh shaders (VK_EXT_mesh_shader). + bool IsExtMeshShaderSupported() const { + return extensions.mesh_shader && features.mesh_shader.taskShader && + features.mesh_shader.meshShader; + } + /// Returns true if the device supports VK_KHR_image_format_list. bool IsKhrImageFormatListSupported() const { return extensions.image_format_list || instance_version >= VK_API_VERSION_1_2; diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp index dbaaf450a0..ac67b95707 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.cpp +++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp @@ -116,6 +116,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept { X(vkCmdDrawIndirectCount); X(vkCmdDrawIndexedIndirectCount); X(vkCmdDrawIndirectByteCountEXT); + X(vkCmdDrawMeshTasksEXT); X(vkCmdEndConditionalRenderingEXT); X(vkCmdEndQuery); X(vkCmdEndRenderPass); diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h index 3010368973..f9b3cc15df 100644 --- a/src/video_core/vulkan_common/vulkan_wrapper.h +++ b/src/video_core/vulkan_common/vulkan_wrapper.h @@ -232,6 +232,7 @@ struct DeviceDispatch : InstanceDispatch { PFN_vkCmdDrawIndirectCount vkCmdDrawIndirectCount{}; PFN_vkCmdDrawIndexedIndirectCount vkCmdDrawIndexedIndirectCount{}; PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT{}; + PFN_vkCmdDrawMeshTasksEXT vkCmdDrawMeshTasksEXT{}; PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT{}; PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT{}; PFN_vkCmdEndQuery vkCmdEndQuery{}; @@ -1319,6 +1320,10 @@ public: dld->vkCmdDrawIndexedIndirect(handle, src_buffer, src_offset, draw_count, stride); } + void DrawMeshTasksEXT(u32 group_count_x, u32 group_count_y, u32 group_count_z) const noexcept { + dld->vkCmdDrawMeshTasksEXT(handle, group_count_x, group_count_y, group_count_z); + } + void DrawIndirectCount(VkBuffer src_buffer, VkDeviceSize src_offset, VkBuffer count_buffer, VkDeviceSize count_offset, u32 draw_count, u32 stride) const noexcept { dld->vkCmdDrawIndirectCount(handle, src_buffer, src_offset, count_buffer, count_offset, -- 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_environment.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