From 720c7d227c1872fce909b5943157ecb31481594e Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Thu, 22 Oct 2026 00:01:15 +0300 Subject: [PATCH] Fixed Civilization VII - added TIR mixed samples support --- .../renderer_vulkan/fixed_pipeline_state.cpp | 8 +- .../renderer_vulkan/vk_texture_cache.cpp | 133 +++++++++++++++++- .../renderer_vulkan/vk_texture_cache.h | 30 ++++ src/video_core/texture_cache/texture_cache.h | 21 ++- 4 files changed, 185 insertions(+), 7 deletions(-) diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp index 0ced890929..71f35e18d4 100644 --- a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp +++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp @@ -129,7 +129,13 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe topology.Assign(can_collapse_topology_class ? TOPOLOGY_CLASS_REPRESENTATIVE_LUT[static_cast(topology_)] : topology_); - msaa_mode.Assign(regs.anti_alias_samples_mode); + // Target-independent rasterization rasterizes (and depth tests) at the raster sample count + if (regs.tir_mode == Maxwell::TIRMode::RasterNTargetM && + regs.anti_alias_raster != Maxwell::AntiAliasRaster::Mode1x1) { + msaa_mode.Assign(static_cast(regs.anti_alias_raster)); + } else { + msaa_mode.Assign(regs.anti_alias_samples_mode); + } attachment0_dual_source_blend.Assign(ComputeAttachment0DualSourceBlend(regs) ? 1 : 0); raw2 = 0; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 583cb6edaf..36e0857c86 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -1089,6 +1089,58 @@ VkImageView TextureCacheRuntime::GetOrCreateResolveShadow(VkImage msaa_image, Vk return *shadow.view; } +VkImageView TextureCacheRuntime::GetOrCreateTirColorScratch(VkImage color_image, VkFormat format, + VkExtent2D extent, u32 layers, + VkSampleCountFlagBits samples) { + TirColorScratch& scratch = tir_color_scratch[color_image]; + if (scratch.image && scratch.format == format && scratch.extent.width == extent.width && + scratch.extent.height == extent.height && scratch.layers == layers && + scratch.samples == samples) { + return *scratch.view; + } + if (scratch.image) { + scheduler.Finish(); + } + scratch.image = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {extent.width, extent.height, 1}, + .mipLevels = 1, + .arrayLayers = layers, + .samples = samples, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }); + scratch.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = *scratch.image, + .viewType = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, + .format = format, + .components{}, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = layers, + }, + }); + scratch.format = format; + scratch.extent = extent; + scratch.layers = layers; + scratch.samples = samples; + return *scratch.view; +} + const TextureCacheRuntime::ResolveShadow* TextureCacheRuntime::GetValidResolveShadow( VkImage msaa_image) const { const auto it = resolve_shadows.find(msaa_image); @@ -2970,6 +3022,26 @@ Framebuffer::Framebuffer(TextureCacheRuntime& runtime, ImageView* color_buffer, Framebuffer::~Framebuffer() = default; +/// Single-sampled color targets with a multisampled depth buffer, as used by target-independent +/// rasterization. Vulkan requires matching sample counts for these attachments. +static bool IsMixedSamplesTarget(std::span color_buffers, + const ImageView* depth_buffer) { + if (depth_buffer == nullptr || depth_buffer->Samples() == VK_SAMPLE_COUNT_1_BIT) { + return false; + } + bool has_color = false; + for (const ImageView* const color_buffer : color_buffers) { + if (color_buffer == nullptr) { + continue; + } + if (color_buffer->Samples() != VK_SAMPLE_COUNT_1_BIT) { + return false; + } + has_color = true; + } + return has_color; +} + void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime, std::span color_buffers, ImageView* depth_buffer, bool is_rescaled_) { @@ -3024,6 +3096,13 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime, } else { renderpass_key.depth_format = PixelFormat::Invalid; } + if (IsMixedSamplesTarget(color_buffers, depth_buffer)) { + const VkExtent2D extent{(std::min)(render_area.width, width), + (std::min)(render_area.height, height)}; + CreateMixedSamplesFramebuffer(runtime, color_buffers, attachments, renderpass_key, + static_cast((std::max)(num_layers, 1)), extent); + return; + } renderpass_key.samples = samples; const bool do_resolve_color = ENABLE_MSAA_TILER_RESOLVE && samples != VK_SAMPLE_COUNT_1_BIT && num_colors > 0 && runtime.device.IsTiler(); @@ -3040,10 +3119,7 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime, discard_msaa_depth_stencil = ENABLE_MSAA_RESOLVE_CONSUME && ENABLE_MSAA_DEPTH_STENCIL_DISCARD && do_resolve_depth_stencil; - renderpass = runtime.render_pass_cache.Get(renderpass_key); - render_pass_key = renderpass_key; - render_pass_cache = &runtime.render_pass_cache; - runtime_ptr = &runtime; + UseRenderPass(runtime, renderpass_key); render_area.width = (std::min)(render_area.width, width); render_area.height = (std::min)(render_area.height, height); @@ -3075,6 +3151,53 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime, } num_color_buffers = static_cast(num_colors); + CreateVkFramebuffer(runtime, attachments, static_cast((std::max)(num_layers, 1))); +} + +void Framebuffer::CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime, + std::span color_buffers, + std::span attachments, + RenderPassKey renderpass_key, u32 layers, + VkExtent2D extent) { + // attachments holds the real color targets followed by the depth buffer + const size_t num_colors = attachments.size() - 1; + boost::container::small_vector mixed_attachments; + for (const ImageView* const color_buffer : color_buffers) { + if (!color_buffer) { + continue; + } + const VkFormat vk_format = MaxwellToVK::SurfaceFormat(runtime.device, FormatType::Optimal, + true, color_buffer->format) + .format; + mixed_attachments.push_back(runtime.GetOrCreateTirColorScratch( + color_buffer->ImageHandle(), vk_format, extent, layers, samples)); + } + mixed_attachments.push_back(attachments[num_colors]); + for (size_t index = 0; index < num_colors; ++index) { + mixed_attachments.push_back(attachments[index]); + } + // Multisampled scratch color + depth, resolving color into the real single-sampled targets + renderpass_key.samples = samples; + renderpass_key.resolve_color = true; + renderpass_key.resolve_depth_stencil = false; + discard_msaa_color = false; + discard_msaa_depth_stencil = false; + UseRenderPass(runtime, renderpass_key); + render_area = extent; + num_color_buffers = static_cast(num_colors); + CreateVkFramebuffer(runtime, mixed_attachments, layers); +} + +void Framebuffer::UseRenderPass(TextureCacheRuntime& runtime, + const RenderPassKey& renderpass_key) { + renderpass = runtime.render_pass_cache.Get(renderpass_key); + render_pass_key = renderpass_key; + render_pass_cache = &runtime.render_pass_cache; + runtime_ptr = &runtime; +} + +void Framebuffer::CreateVkFramebuffer(TextureCacheRuntime& runtime, + std::span attachments, u32 layers) { framebuffer = runtime.device.GetLogical().CreateFramebuffer({ .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .pNext = nullptr, @@ -3084,7 +3207,7 @@ void Framebuffer::CreateFramebuffer(TextureCacheRuntime& runtime, .pAttachments = attachments.data(), .width = render_area.width, .height = render_area.height, - .layers = static_cast((std::max)(num_layers, 1)), + .layers = layers, }); } diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 2e9736f707..cdfe56ab05 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -133,6 +133,11 @@ public: [[nodiscard]] const ResolveShadow* GetValidResolveShadow(VkImage msaa_image) const; + /// Multisampled stand-in for a single-sampled color target rendered with a TIR depth buffer + [[nodiscard]] VkImageView GetOrCreateTirColorScratch(VkImage color_image, VkFormat format, + VkExtent2D extent, u32 layers, + VkSampleCountFlagBits samples); + void InvalidateResolveShadow(VkImage msaa_image); void MarkResolveShadowUpToDate(VkImage msaa_image); @@ -187,6 +192,15 @@ public: std::vector msaa_scratch_images; ::Common::unordered_map resolve_shadows; + struct TirColorScratch { + vk::Image image; + vk::ImageView view; + VkFormat format = VK_FORMAT_UNDEFINED; + VkExtent2D extent{}; + u32 layers = 0; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + }; + ::Common::unordered_map tir_color_scratch; std::vector> pending_resolve_shadows; }; @@ -279,6 +293,22 @@ public: void MarkResolveShadowsUpToDate() const; private: + /// Builds a framebuffer for single-sampled color targets drawn with a multisampled depth + /// buffer (target-independent rasterization). Color is rendered into multisampled scratch + /// images that resolve into the real targets at the end of the pass. + void CreateMixedSamplesFramebuffer(TextureCacheRuntime& runtime, + std::span color_buffers, + std::span attachments, + RenderPassKey renderpass_key, u32 layers, + VkExtent2D extent); + + /// Fetches the render pass for the key from the cache and remembers it for variants. + void UseRenderPass(TextureCacheRuntime& runtime, const RenderPassKey& renderpass_key); + + /// Creates the Vulkan framebuffer for the current render pass and render area. + void CreateVkFramebuffer(TextureCacheRuntime& runtime, + std::span attachments, u32 layers); + static constexpr size_t NUM_MEMOIZED_RENDER_PASS_VARIANTS = 8; vk::Framebuffer framebuffer; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index e92e54f8ce..5dd866451e 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1928,7 +1928,26 @@ ImageViewId TextureCache

::FindDepthBuffer() { if (gpu_addr == 0) { return ImageViewId{}; } - const ImageInfo info(regs.zeta, regs.zeta_size, regs.anti_alias_samples_mode); + // Target-independent rasterization: the depth buffer keeps the raster sample count while + // the color targets use anti_alias_samples_mode. + Tegra::Texture::MsaaMode zeta_msaa_mode = regs.anti_alias_samples_mode; + if (regs.anti_alias_raster != Tegra::Engines::Maxwell3D::Regs::AntiAliasRaster::Mode1x1) { + const auto raster_mode = static_cast(regs.anti_alias_raster); + bool use_raster = regs.tir_mode == Tegra::Engines::Maxwell3D::Regs::TIRMode::RasterNTargetM; + if (!use_raster && regs.anti_alias_samples_mode == Tegra::Texture::MsaaMode::Msaa1x1) { + // Clears can be issued before tir_mode is enabled. Only a depth buffer sized for the + // raster samples relative to the color target is treated as multisampled, otherwise + // it ping-pongs between a 1x and a multisampled image every frame. + const auto& rt0 = regs.rt[0]; + use_raster = rt0.Address() != 0 && rt0.format != Tegra::RenderTargetFormat::NONE && + regs.zeta_size.width == rt0.width * NumSamplesX(raster_mode) && + regs.zeta_size.height == rt0.height * NumSamplesY(raster_mode); + } + if (use_raster) { + zeta_msaa_mode = raster_mode; + } + } + const ImageInfo info(regs.zeta, regs.zeta_size, zeta_msaa_mode); return FindRenderTargetView(info, gpu_addr); }